001/* 002 * Copyright 2024-2025 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet.servlet.javax; 018 019import javax.annotation.Nonnull; 020import javax.annotation.Nullable; 021import javax.annotation.concurrent.NotThreadSafe; 022import javax.annotation.concurrent.ThreadSafe; 023import javax.servlet.Filter; 024import javax.servlet.FilterRegistration; 025import javax.servlet.RequestDispatcher; 026import javax.servlet.Servlet; 027import javax.servlet.ServletContext; 028import javax.servlet.ServletException; 029import javax.servlet.ServletRegistration; 030import javax.servlet.ServletRequest; 031import javax.servlet.ServletResponse; 032import javax.servlet.SessionCookieConfig; 033import javax.servlet.SessionTrackingMode; 034import javax.servlet.descriptor.JspConfigDescriptor; 035import java.io.File; 036import java.io.IOException; 037import java.io.InputStream; 038import java.io.PrintWriter; 039import java.io.StringWriter; 040import java.io.UncheckedIOException; 041import java.io.Writer; 042import java.net.MalformedURLException; 043import java.net.URL; 044import java.net.URLConnection; 045import java.nio.charset.Charset; 046import java.nio.charset.StandardCharsets; 047import java.nio.file.Files; 048import java.nio.file.Path; 049import java.nio.file.Paths; 050import java.util.ArrayList; 051import java.util.Collections; 052import java.util.Enumeration; 053import java.util.EventListener; 054import java.util.HashMap; 055import java.util.List; 056import java.util.Map; 057import java.util.Set; 058import java.util.jar.JarEntry; 059import java.util.jar.JarFile; 060import java.util.stream.Collectors; 061import java.util.stream.Stream; 062 063import static java.lang.String.format; 064import static java.util.Objects.requireNonNull; 065 066/** 067 * Soklet integration implementation of {@link ServletContext}. 068 * 069 * @author <a href="https://www.revetkn.com">Mark Allen</a> 070 */ 071@NotThreadSafe 072public final class SokletServletContext implements ServletContext { 073 @Nonnull 074 private final Writer logWriter; 075 @Nonnull 076 private final Map<String, Object> attributes; 077 @Nonnull 078 private int sessionTimeout; 079 @Nullable 080 private Charset requestCharset; 081 @Nullable 082 private Charset responseCharset; 083 084 @Nonnull 085 public static SokletServletContext withDefaults() { 086 return new SokletServletContext(null); 087 } 088 089 @Nonnull 090 public static SokletServletContext withLogWriter(@Nullable Writer logWriter) { 091 return new SokletServletContext(logWriter); 092 } 093 094 private SokletServletContext(@Nullable Writer logWriter) { 095 this.logWriter = logWriter == null ? new NoOpWriter() : logWriter; 096 this.attributes = new HashMap<>(); 097 this.sessionTimeout = -1; 098 this.requestCharset = StandardCharsets.UTF_8; 099 this.responseCharset = StandardCharsets.UTF_8; 100 } 101 102 @Nonnull 103 protected Writer getLogWriter() { 104 return this.logWriter; 105 } 106 107 @Nonnull 108 protected Map<String, Object> getAttributes() { 109 return this.attributes; 110 } 111 112 @ThreadSafe 113 protected static class NoOpWriter extends Writer { 114 @Override 115 public void write(@Nonnull char[] cbuf, 116 int off, 117 int len) throws IOException { 118 requireNonNull(cbuf); 119 // No-op 120 } 121 122 @Override 123 public void flush() throws IOException { 124 // No-op 125 } 126 127 @Override 128 public void close() throws IOException { 129 // No-op 130 } 131 } 132 133 // Implementation of ServletContext methods below: 134 135 @Override 136 @Nullable 137 public String getContextPath() { 138 return ""; 139 } 140 141 @Override 142 @Nullable 143 public ServletContext getContext(@Nullable String uripath) { 144 return this; 145 } 146 147 @Override 148 public int getMajorVersion() { 149 return 4; 150 } 151 152 @Override 153 public int getMinorVersion() { 154 return 0; 155 } 156 157 @Override 158 public int getEffectiveMajorVersion() { 159 return 4; 160 } 161 162 @Override 163 public int getEffectiveMinorVersion() { 164 return 0; 165 } 166 167 @Override 168 @Nullable 169 public String getMimeType(@Nullable String file) { 170 if (file == null) 171 return null; 172 173 return URLConnection.guessContentTypeFromName(file); 174 } 175 176 @Override 177 @Nonnull 178 public Set<String> getResourcePaths(@Nullable String path) { 179 // TODO: revisit https://javaee.github.io/javaee-spec/javadocs/javax/servlet/ServletContext.html#getResourcePaths-java.lang.String- 180 if (path == null || !path.startsWith("/")) 181 return java.util.Set.of(); 182 183 try { 184 String normalized = path.equals("/") ? "" : path.substring(1); 185 186 if (!normalized.endsWith("/") && !normalized.isEmpty()) 187 normalized += "/"; 188 189 Enumeration<URL> roots = 190 Thread.currentThread().getContextClassLoader().getResources(normalized); 191 192 Set<String> out = new java.util.TreeSet<>(); 193 194 while (roots.hasMoreElements()) { 195 URL url = roots.nextElement(); 196 String protocol = url.getProtocol(); 197 198 if ("file".equals(protocol)) { 199 Path p = Paths.get(url.toURI()); 200 201 try (Stream<Path> s = Files.list(p)) { 202 s.forEach(child -> { 203 String name = child.getFileName().toString(); 204 boolean dir = java.nio.file.Files.isDirectory(child); 205 out.add((path.endsWith("/") ? path : path + "/") + name + (dir ? "/" : "")); 206 }); 207 } 208 } else if ("jar".equals(protocol)) { 209 String spec = url.getFile(); // e.g. file:/app.jar!/static/ 210 int bang = spec.indexOf("!"); 211 String jarPath = spec.substring(0, bang); 212 java.net.URL jarUrl = new java.net.URL(jarPath); 213 try (JarFile jar = new JarFile(new File(jarUrl.toURI()))) { 214 String prefix = normalized; 215 jar.stream() 216 .map(JarEntry::getName) 217 .filter(n -> n.startsWith(prefix) && !n.equals(prefix)) 218 .map(n -> { 219 String remainder = n.substring(prefix.length()); 220 int slash = remainder.indexOf('/'); 221 if (slash == -1) 222 return (path.endsWith("/") ? path : path + "/") + remainder; 223 224 return (path.endsWith("/") ? path : path + "/") + remainder.substring(0, slash + 1); 225 }) 226 .forEach(out::add); 227 } 228 } 229 } 230 return out; 231 } catch (Exception ignored) { 232 return Set.of(); 233 } 234 } 235 236 @Override 237 @Nullable 238 public URL getResource(@Nullable String path) throws MalformedURLException { 239 // TODO: revisit https://javaee.github.io/javaee-spec/javadocs/javax/servlet/ServletContext.html#getResource-java.lang.String- 240 if (path == null || !path.startsWith("/")) 241 return null; 242 243 return getClass().getResource(path); // may be null 244 } 245 246 @Override 247 @Nullable 248 public InputStream getResourceAsStream(@Nullable String path) { 249 // TODO: revisit https://javaee.github.io/javaee-spec/javadocs/javax/servlet/ServletContext.html#getResourceAsStream-java.lang.String- 250 if (path == null || !path.startsWith("/")) 251 return null; 252 253 return getClass().getResourceAsStream(path); // may be null 254 } 255 256 @Override 257 @Nullable 258 public RequestDispatcher getRequestDispatcher(@Nullable String path) { 259 // TODO: revisit https://javaee.github.io/javaee-spec/javadocs/javax/servlet/ServletContext.html#getRequestDispatcher-java.lang.String- 260 if (path == null || path.isBlank()) 261 return null; 262 263 return new RequestDispatcher() { 264 @Override 265 public void forward(ServletRequest servletRequest, ServletResponse servletResponse) { 266 throw new IllegalStateException("RequestDispatcher.forward is not supported by Soklet."); 267 } 268 269 @Override 270 public void include(ServletRequest servletRequest, ServletResponse servletResponse) { 271 throw new IllegalStateException("RequestDispatcher.include is not supported by Soklet."); 272 } 273 }; 274 } 275 276 @Override 277 @Nullable 278 public RequestDispatcher getNamedDispatcher(@Nullable String name) { 279 // TODO: revisit https://javaee.github.io/javaee-spec/javadocs/javax/servlet/ServletContext.html#getNamedDispatcher-java.lang.String- 280 // This is legal according to spec, but we likely want a real instance returned 281 return null; 282 } 283 284 @Override 285 @Deprecated 286 @Nullable 287 public Servlet getServlet(@Nullable String name) throws ServletException { 288 // Deliberately null per spec b/c this method is deprecated 289 return null; 290 } 291 292 @Override 293 @Deprecated 294 @Nonnull 295 public Enumeration<Servlet> getServlets() { 296 // Deliberately empty per spec b/c this method is deprecated 297 return Collections.emptyEnumeration(); 298 } 299 300 @Override 301 @Deprecated 302 @Nonnull 303 public Enumeration<String> getServletNames() { 304 // Deliberately empty per spec b/c this method is deprecated 305 return Collections.emptyEnumeration(); 306 } 307 308 @Override 309 public void log(@Nullable String msg) { 310 if (msg == null) 311 return; 312 313 try { 314 getLogWriter().write(msg); 315 } catch (IOException e) { 316 throw new UncheckedIOException(e); 317 } 318 } 319 320 @Override 321 @Deprecated 322 public void log(@Nullable Exception exception, 323 @Nullable String msg) { 324 if (exception == null && msg == null) 325 return; 326 327 log(msg, exception); 328 } 329 330 @Override 331 public void log(@Nullable String message, 332 @Nullable Throwable throwable) { 333 List<String> components = new ArrayList<>(2); 334 335 if (message != null) 336 components.add(message); 337 338 if (throwable != null) { 339 StringWriter stringWriter = new StringWriter(); 340 PrintWriter printWriter = new PrintWriter(stringWriter); 341 throwable.printStackTrace(printWriter); 342 components.add(stringWriter.toString()); 343 } 344 345 String combinedMessage = components.stream().collect(Collectors.joining("\n")); 346 347 try { 348 getLogWriter().write(combinedMessage); 349 } catch (IOException e) { 350 throw new UncheckedIOException(e); 351 } 352 } 353 354 @Override 355 @Nullable 356 public String getRealPath(@Nullable String path) { 357 // Soklet has no concept of a physical path on the filesystem for a URL path 358 return null; 359 } 360 361 @Override 362 @Nonnull 363 public String getServerInfo() { 364 return "Soklet/Undefined"; 365 } 366 367 @Override 368 @Nullable 369 public String getInitParameter(String name) { 370 // Soklet has no concept of init parameters 371 return null; 372 } 373 374 @Override 375 @Nonnull 376 public Enumeration<String> getInitParameterNames() { 377 // Soklet has no concept of init parameters 378 return Collections.emptyEnumeration(); 379 } 380 381 @Override 382 public boolean setInitParameter(@Nullable String name, 383 @Nullable String value) { 384 throw new IllegalStateException(format("Soklet does not support %s init parameters.", 385 ServletContext.class.getSimpleName())); 386 } 387 388 @Override 389 @Nullable 390 public Object getAttribute(@Nullable String name) { 391 return name == null ? null : getAttributes().get(name); 392 } 393 394 @Override 395 @Nonnull 396 public Enumeration<String> getAttributeNames() { 397 return Collections.enumeration(getAttributes().keySet()); 398 } 399 400 @Override 401 public void setAttribute(@Nullable String name, 402 @Nullable Object object) { 403 if (name == null) 404 return; 405 406 if (object == null) 407 removeAttribute(name); 408 else 409 getAttributes().put(name, object); 410 } 411 412 @Override 413 public void removeAttribute(@Nullable String name) { 414 getAttributes().remove(name); 415 } 416 417 @Override 418 @Nullable 419 public String getServletContextName() { 420 // This is legal according to spec 421 return null; 422 } 423 424 @Override 425 @Nullable 426 public ServletRegistration.Dynamic addServlet(@Nullable String servletName, 427 @Nullable String className) { 428 throw new IllegalStateException("Soklet does not support adding Servlets"); 429 } 430 431 @Override 432 @Nullable 433 public ServletRegistration.Dynamic addServlet(@Nullable String servletName, 434 @Nullable Servlet servlet) { 435 throw new IllegalStateException("Soklet does not support adding Servlets"); 436 } 437 438 @Override 439 @Nullable 440 public ServletRegistration.Dynamic addServlet(@Nullable String servletName, 441 @Nullable Class<? extends Servlet> servletClass) { 442 throw new IllegalStateException("Soklet does not support adding Servlets"); 443 } 444 445 @Override 446 @Nullable 447 public ServletRegistration.Dynamic addJspFile(@Nullable String servletName, 448 @Nullable String jspFile) { 449 throw new IllegalStateException("Soklet does not support adding JSP files"); 450 } 451 452 @Override 453 @Nullable 454 public <T extends Servlet> T createServlet(@Nullable Class<T> clazz) throws ServletException { 455 throw new ServletException("Soklet does not support creating Servlets"); 456 } 457 458 @Override 459 @Nullable 460 public ServletRegistration getServletRegistration(@Nullable String servletName) { 461 // This is legal according to spec 462 return null; 463 } 464 465 @Override 466 @Nonnull 467 public Map<String, ? extends ServletRegistration> getServletRegistrations() { 468 return Map.of(); 469 } 470 471 @Override 472 @Nullable 473 public FilterRegistration.Dynamic addFilter(@Nullable String filterName, 474 @Nullable String className) { 475 throw new IllegalStateException("Soklet does not support adding Filters"); 476 } 477 478 @Override 479 @Nullable 480 public FilterRegistration.Dynamic addFilter(@Nullable String filterName, 481 @Nullable Filter filter) { 482 throw new IllegalStateException("Soklet does not support adding Filters"); 483 } 484 485 @Override 486 @Nullable 487 public FilterRegistration.Dynamic addFilter(@Nullable String filterName, 488 @Nullable Class<? extends Filter> filterClass) { 489 throw new IllegalStateException("Soklet does not support adding Filters"); 490 } 491 492 @Override 493 @Nullable 494 public <T extends Filter> T createFilter(@Nullable Class<T> clazz) throws ServletException { 495 throw new ServletException("Soklet does not support creating Filters"); 496 } 497 498 @Override 499 @Nullable 500 public FilterRegistration getFilterRegistration(@Nullable String filterName) { 501 // This is legal according to spec 502 return null; 503 } 504 505 @Override 506 @Nonnull 507 public Map<String, ? extends FilterRegistration> getFilterRegistrations() { 508 return Map.of(); 509 } 510 511 @Override 512 @Nullable 513 public SessionCookieConfig getSessionCookieConfig() { 514 // Diverges from spec here; Soklet has no concept of "session cookie" 515 throw new IllegalStateException("Soklet does not support session cookies"); 516 } 517 518 @Override 519 public void setSessionTrackingModes(@Nullable Set<SessionTrackingMode> sessionTrackingModes) { 520 throw new IllegalStateException("Soklet does not support session tracking"); 521 } 522 523 @Override 524 @Nonnull 525 public Set<SessionTrackingMode> getDefaultSessionTrackingModes() { 526 return Set.of(); 527 } 528 529 @Override 530 @Nonnull 531 public Set<SessionTrackingMode> getEffectiveSessionTrackingModes() { 532 return Set.of(); 533 } 534 535 @Override 536 public void addListener(@Nullable String className) { 537 throw new IllegalStateException("Soklet does not support listeners"); 538 } 539 540 @Override 541 @Nullable 542 public <T extends EventListener> void addListener(@Nullable T t) { 543 throw new IllegalStateException("Soklet does not support listeners"); 544 } 545 546 @Override 547 public void addListener(@Nullable Class<? extends EventListener> listenerClass) { 548 throw new IllegalStateException("Soklet does not support listeners"); 549 } 550 551 @Override 552 @Nullable 553 public <T extends EventListener> T createListener(@Nullable Class<T> clazz) throws ServletException { 554 throw new ServletException("Soklet does not support listeners"); 555 } 556 557 @Override 558 @Nullable 559 public JspConfigDescriptor getJspConfigDescriptor() { 560 // This is legal according to spec 561 return null; 562 } 563 564 @Override 565 @Nonnull 566 public ClassLoader getClassLoader() { 567 return this.getClass().getClassLoader(); 568 } 569 570 @Override 571 public void declareRoles(@Nullable String... strings) { 572 throw new IllegalStateException("Soklet does not support Servlet roles"); 573 } 574 575 @Override 576 @Nonnull 577 public String getVirtualServerName() { 578 return "soklet"; 579 } 580 581 @Override 582 public int getSessionTimeout() { 583 return this.sessionTimeout; 584 } 585 586 @Override 587 public void setSessionTimeout(int sessionTimeout) { 588 this.sessionTimeout = sessionTimeout; 589 } 590 591 @Override 592 @Nullable 593 public String getRequestCharacterEncoding() { 594 return this.requestCharset == null ? null : this.requestCharset.name(); 595 } 596 597 @Override 598 public void setRequestCharacterEncoding(@Nullable String encoding) { 599 this.requestCharset = encoding == null ? null : Charset.forName(encoding); 600 } 601 602 @Override 603 @Nullable 604 public String getResponseCharacterEncoding() { 605 return this.responseCharset == null ? null : this.responseCharset.name(); 606 } 607 608 @Override 609 public void setResponseCharacterEncoding(@Nullable String encoding) { 610 this.responseCharset = encoding == null ? null : Charset.forName(encoding); 611 } 612}