001/* 002 * Copyright 2024-2026 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 org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.NotThreadSafe; 023import javax.annotation.concurrent.ThreadSafe; 024import javax.servlet.Filter; 025import javax.servlet.FilterRegistration; 026import javax.servlet.RequestDispatcher; 027import javax.servlet.Servlet; 028import javax.servlet.ServletContext; 029import javax.servlet.ServletException; 030import javax.servlet.ServletRegistration; 031import javax.servlet.SessionCookieConfig; 032import javax.servlet.SessionTrackingMode; 033import javax.servlet.descriptor.JspConfigDescriptor; 034import java.io.IOException; 035import java.io.InputStream; 036import java.io.PrintWriter; 037import java.io.StringWriter; 038import java.io.UncheckedIOException; 039import java.io.Writer; 040import java.net.MalformedURLException; 041import java.net.URL; 042import java.net.URLConnection; 043import java.net.URLClassLoader; 044import java.nio.charset.Charset; 045import java.nio.charset.StandardCharsets; 046import java.nio.file.Files; 047import java.nio.file.Path; 048import java.nio.file.Paths; 049import java.util.ArrayList; 050import java.util.Collections; 051import java.util.Enumeration; 052import java.util.EventListener; 053import java.util.List; 054import java.util.Map; 055import java.util.Optional; 056import java.util.Set; 057import java.util.concurrent.ConcurrentHashMap; 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@ThreadSafe 072public final class SokletServletContext implements ServletContext { 073 @NonNull 074 private final Writer logWriter; 075 @NonNull 076 private final Object logLock; 077 @NonNull 078 private final Map<@NonNull String, @NonNull Object> attributes; 079 @Nullable 080 private final ResourceRoot resourceRoot; 081 private volatile @Nullable Integer sessionTimeout; 082 @Nullable 083 private volatile Charset requestCharset; 084 @Nullable 085 private volatile Charset responseCharset; 086 087 public static SokletServletContext fromDefaults() { 088 return builder().build(); 089 } 090 091 @NonNull 092 public static Builder builder() { 093 return new Builder(); 094 } 095 096 private SokletServletContext(@Nullable Writer logWriter, 097 @Nullable ResourceRoot resourceRoot, 098 @Nullable Integer sessionTimeout, 099 @Nullable Charset requestCharset, 100 @Nullable Charset responseCharset) { 101 this.logWriter = logWriter == null ? new NoOpWriter() : logWriter; 102 this.logLock = new Object(); 103 this.attributes = new ConcurrentHashMap<>(); 104 this.resourceRoot = resourceRoot; 105 this.sessionTimeout = sessionTimeout; 106 this.requestCharset = requestCharset; 107 this.responseCharset = responseCharset; 108 } 109 110 @NonNull 111 private Writer getLogWriter() { 112 return this.logWriter; 113 } 114 115 @NonNull 116 private Map<@NonNull String, @NonNull Object> getAttributes() { 117 return this.attributes; 118 } 119 120 @NonNull 121 private Optional<ResourceRoot> getResourceRoot() { 122 return Optional.ofNullable(this.resourceRoot); 123 } 124 125 /** 126 * Builder used to construct instances of {@link SokletServletContext}. 127 * <p> 128 * This class is intended for use by a single thread. 129 * 130 * @author <a href="https://www.revetkn.com">Mark Allen</a> 131 */ 132 @NotThreadSafe 133 public static class Builder { 134 @Nullable 135 private Writer logWriter; 136 @Nullable 137 private ResourceRoot resourceRoot; 138 @Nullable 139 private Integer sessionTimeout; 140 @Nullable 141 private Charset requestCharset; 142 @Nullable 143 private Charset responseCharset; 144 145 private Builder() { 146 this.sessionTimeout = null; 147 this.requestCharset = StandardCharsets.ISO_8859_1; 148 this.responseCharset = StandardCharsets.ISO_8859_1; 149 } 150 151 @NonNull 152 public Builder logWriter(@Nullable Writer logWriter) { 153 this.logWriter = logWriter; 154 return this; 155 } 156 157 @NonNull 158 public Builder filesystemResourceRoot(@NonNull Path resourceRoot) { 159 requireNonNull(resourceRoot); 160 this.resourceRoot = ResourceRoot.forFilesystem(resourceRoot); 161 return this; 162 } 163 164 @NonNull 165 public Builder classpathResourceRoot(@NonNull String resourceRoot) { 166 requireNonNull(resourceRoot); 167 this.resourceRoot = ResourceRoot.forClasspath(resourceRoot); 168 return this; 169 } 170 171 @NonNull 172 public Builder sessionTimeout(int sessionTimeout) { 173 this.sessionTimeout = sessionTimeout; 174 return this; 175 } 176 177 @NonNull 178 public Builder requestCharacterEncoding(@Nullable Charset charset) { 179 this.requestCharset = charset; 180 return this; 181 } 182 183 @NonNull 184 public Builder responseCharacterEncoding(@Nullable Charset charset) { 185 this.responseCharset = charset; 186 return this; 187 } 188 189 @NonNull 190 public SokletServletContext build() { 191 return new SokletServletContext( 192 this.logWriter, 193 this.resourceRoot, 194 this.sessionTimeout, 195 this.requestCharset, 196 this.responseCharset); 197 } 198 199 } 200 201 @ThreadSafe 202 private interface ResourceRoot { 203 @Nullable 204 Set<@NonNull String> getResourcePaths(@NonNull String path); 205 206 @Nullable 207 URL getResource(@NonNull String path) throws MalformedURLException; 208 209 @Nullable 210 InputStream getResourceAsStream(@NonNull String path); 211 212 @NonNull 213 static ResourceRoot forFilesystem(@NonNull Path resourceRoot) { 214 return new FilesystemResourceRoot(resourceRoot); 215 } 216 217 @NonNull 218 static ResourceRoot forClasspath(@NonNull String resourceRoot) { 219 return new ClasspathResourceRoot(resourceRoot); 220 } 221 } 222 223 @ThreadSafe 224 private static final class FilesystemResourceRoot implements ResourceRoot { 225 @NonNull 226 private final Path root; 227 228 private FilesystemResourceRoot(@NonNull Path root) { 229 requireNonNull(root); 230 this.root = root.toAbsolutePath().normalize(); 231 } 232 233 @NonNull 234 private Optional<Path> resolvePath(@NonNull String path) { 235 String relative = path.substring(1); 236 Path resolved = root.resolve(relative).normalize(); 237 return resolved.startsWith(root) ? Optional.of(resolved) : Optional.empty(); 238 } 239 240 @Override 241 @Nullable 242 public Set<@NonNull String> getResourcePaths(@NonNull String path) { 243 requireNonNull(path); 244 String normalized = path; 245 246 if (!normalized.endsWith("/")) 247 normalized += "/"; 248 249 Path dir = resolvePath(normalized).orElse(null); 250 251 if (dir == null || !Files.isDirectory(dir)) 252 return null; 253 254 try (Stream<Path> stream = Files.list(dir)) { 255 Set<@NonNull String> out = new java.util.TreeSet<>(); 256 String prefix = normalized; 257 258 stream.forEach(child -> { 259 String name = child.getFileName().toString(); 260 boolean isDir = Files.isDirectory(child); 261 out.add(prefix + name + (isDir ? "/" : "")); 262 }); 263 264 return out.isEmpty() ? null : out; 265 } catch (IOException ignored) { 266 return null; 267 } 268 } 269 270 @Override 271 @Nullable 272 public URL getResource(@NonNull String path) throws MalformedURLException { 273 requireNonNull(path); 274 Path resolved = resolvePath(path).orElse(null); 275 276 if (resolved == null || !Files.exists(resolved)) 277 return null; 278 279 return resolved.toUri().toURL(); 280 } 281 282 @Override 283 @Nullable 284 public InputStream getResourceAsStream(@NonNull String path) { 285 try { 286 URL url = getResource(path); 287 return url == null ? null : url.openStream(); 288 } catch (IOException ignored) { 289 return null; 290 } 291 } 292 } 293 294 @ThreadSafe 295 private static final class ClasspathResourceRoot implements ResourceRoot { 296 @NonNull 297 private final String rootPrefix; 298 @NonNull 299 private final ClassLoader classLoader; 300 301 private ClasspathResourceRoot(@NonNull String rootPrefix) { 302 requireNonNull(rootPrefix); 303 this.rootPrefix = normalizePrefix(rootPrefix); 304 ClassLoader loader = Thread.currentThread().getContextClassLoader(); 305 this.classLoader = loader == null ? SokletServletContext.class.getClassLoader() : loader; 306 } 307 308 @NonNull 309 private static String normalizePrefix(@NonNull String prefix) { 310 String normalized = prefix.trim(); 311 312 if (normalized.startsWith("/")) 313 normalized = normalized.substring(1); 314 315 if (!normalized.isEmpty() && !normalized.endsWith("/")) 316 normalized += "/"; 317 318 if (containsDotDotSegment(normalized)) 319 throw new IllegalArgumentException("Classpath resource root must not contain '..'"); 320 321 return normalized; 322 } 323 324 private static boolean containsDotDotSegment(@NonNull String path) { 325 for (String segment : path.split("/")) { 326 if ("..".equals(segment)) 327 return true; 328 } 329 330 return false; 331 } 332 333 @NonNull 334 private Optional<String> toClasspathPath(@NonNull String path, 335 boolean forDirectoryListing) { 336 if (containsDotDotSegment(path)) 337 return Optional.empty(); 338 339 String relative = path.substring(1); 340 String lookup = rootPrefix + relative; 341 342 if (forDirectoryListing && !lookup.endsWith("/") && !lookup.isEmpty()) 343 lookup += "/"; 344 345 return Optional.of(lookup); 346 } 347 348 private void addFilesystemEntries(@NonNull Path dir, 349 @NonNull String prefix, 350 @NonNull Set<@NonNull String> out) throws IOException { 351 if (!Files.isDirectory(dir)) 352 return; 353 354 try (Stream<Path> stream = Files.list(dir)) { 355 stream.forEach(child -> { 356 String name = child.getFileName().toString(); 357 boolean isDir = Files.isDirectory(child); 358 out.add(prefix + name + (isDir ? "/" : "")); 359 }); 360 } 361 } 362 363 private void addJarEntries(@NonNull JarFile jar, 364 @NonNull String jarPrefix, 365 @NonNull String prefix, 366 @NonNull Set<@NonNull String> out) { 367 jar.stream() 368 .map(JarEntry::getName) 369 .filter(name -> name.startsWith(jarPrefix) && !name.equals(jarPrefix)) 370 .map(name -> { 371 String remainder = name.substring(jarPrefix.length()); 372 int slash = remainder.indexOf('/'); 373 if (slash == -1) 374 return prefix + remainder; 375 376 return prefix + remainder.substring(0, slash + 1); 377 }) 378 .forEach(out::add); 379 } 380 381 private void addClasspathRootEntries(@NonNull URL rootUrl, 382 @NonNull String classpathPath, 383 @NonNull String prefix, 384 @NonNull Set<@NonNull String> out) throws Exception { 385 String protocol = rootUrl.getProtocol(); 386 387 if ("file".equals(protocol)) { 388 Path rootPath = Paths.get(rootUrl.toURI()); 389 if (Files.isDirectory(rootPath)) { 390 Path dir = classpathPath.isEmpty() ? rootPath : rootPath.resolve(classpathPath); 391 addFilesystemEntries(dir, prefix, out); 392 } else if (Files.isRegularFile(rootPath)) { 393 try (JarFile jar = new JarFile(rootPath.toFile())) { 394 addJarEntries(jar, classpathPath, prefix, out); 395 } 396 } 397 } else if ("jar".equals(protocol)) { 398 String spec = rootUrl.getFile(); 399 int bang = spec.indexOf("!"); 400 String jarPath = bang >= 0 ? spec.substring(0, bang) : spec; 401 URL jarUrl = new URL(jarPath); 402 403 try (JarFile jar = new JarFile(new java.io.File(jarUrl.toURI()))) { 404 addJarEntries(jar, classpathPath, prefix, out); 405 } 406 } 407 } 408 409 @Override 410 @Nullable 411 public Set<@NonNull String> getResourcePaths(@NonNull String path) { 412 requireNonNull(path); 413 414 String classpathPath = toClasspathPath(path, true).orElse(null); 415 416 if (classpathPath == null) 417 return null; 418 419 try { 420 Enumeration<@NonNull URL> roots = classLoader.getResources(classpathPath); 421 Set<@NonNull String> out = new java.util.TreeSet<>(); 422 String prefix = path.endsWith("/") ? path : path + "/"; 423 boolean sawRoot = false; 424 425 while (roots.hasMoreElements()) { 426 sawRoot = true; 427 URL url = roots.nextElement(); 428 String protocol = url.getProtocol(); 429 430 if ("file".equals(protocol)) { 431 Path rootPath = Paths.get(url.toURI()); 432 if (Files.isDirectory(rootPath)) { 433 addFilesystemEntries(rootPath, prefix, out); 434 } else if (Files.isRegularFile(rootPath)) { 435 try (JarFile jar = new JarFile(rootPath.toFile())) { 436 addJarEntries(jar, classpathPath, prefix, out); 437 } 438 } 439 } else if ("jar".equals(protocol)) { 440 String spec = url.getFile(); 441 int bang = spec.indexOf("!"); 442 String jarPath = spec.substring(0, bang); 443 URL jarUrl = new URL(jarPath); 444 445 try (JarFile jar = new JarFile(new java.io.File(jarUrl.toURI()))) { 446 String jarPrefix = classpathPath; 447 addJarEntries(jar, jarPrefix, prefix, out); 448 } 449 } 450 } 451 452 if (!sawRoot) { 453 Enumeration<@NonNull URL> classpathRoots = classLoader.getResources(""); 454 455 while (classpathRoots.hasMoreElements()) { 456 URL rootUrl = classpathRoots.nextElement(); 457 addClasspathRootEntries(rootUrl, classpathPath, prefix, out); 458 } 459 } 460 461 if (out.isEmpty() && classLoader instanceof URLClassLoader) { 462 URL[] urls = ((URLClassLoader) classLoader).getURLs(); 463 464 for (URL rootUrl : urls) 465 addClasspathRootEntries(rootUrl, classpathPath, prefix, out); 466 } 467 468 return out.isEmpty() ? null : out; 469 } catch (Exception ignored) { 470 return null; 471 } 472 } 473 474 @Override 475 @Nullable 476 public URL getResource(@NonNull String path) throws MalformedURLException { 477 requireNonNull(path); 478 479 String classpathPath = toClasspathPath(path, false).orElse(null); 480 481 if (classpathPath == null) 482 return null; 483 484 URL url = classLoader.getResource(classpathPath); 485 return url; 486 } 487 488 @Override 489 @Nullable 490 public InputStream getResourceAsStream(@NonNull String path) { 491 String classpathPath = toClasspathPath(path, false).orElse(null); 492 493 if (classpathPath == null) 494 return null; 495 496 return classLoader.getResourceAsStream(classpathPath); 497 } 498 } 499 @ThreadSafe 500 private static class NoOpWriter extends Writer { 501 @Override 502 public void write(@NonNull char[] cbuf, 503 int off, 504 int len) throws IOException { 505 requireNonNull(cbuf); 506 // No-op 507 } 508 509 @Override 510 public void flush() throws IOException { 511 // No-op 512 } 513 514 @Override 515 public void close() throws IOException { 516 // No-op 517 } 518 } 519 520 // Implementation of ServletContext methods below: 521 522 @Override 523 @Nullable 524 public String getContextPath() { 525 return ""; 526 } 527 528 @Override 529 @Nullable 530 public ServletContext getContext(@Nullable String uripath) { 531 if (uripath == null) 532 return null; 533 534 String normalized = uripath.trim(); 535 536 if (normalized.isEmpty() || "/".equals(normalized)) 537 return this; 538 539 return null; 540 } 541 542 @Override 543 public int getMajorVersion() { 544 return 4; 545 } 546 547 @Override 548 public int getMinorVersion() { 549 return 0; 550 } 551 552 @Override 553 public int getEffectiveMajorVersion() { 554 return 4; 555 } 556 557 @Override 558 public int getEffectiveMinorVersion() { 559 return 0; 560 } 561 562 @Override 563 @Nullable 564 public String getMimeType(@Nullable String file) { 565 if (file == null) 566 return null; 567 568 return URLConnection.guessContentTypeFromName(file); 569 } 570 571 @Override 572 @Nullable 573 public Set<@NonNull String> getResourcePaths(@Nullable String path) { 574 if (path == null || !path.startsWith("/")) 575 return null; 576 577 ResourceRoot root = getResourceRoot().orElse(null); 578 return root == null ? null : root.getResourcePaths(path); 579 } 580 581 @Override 582 @Nullable 583 public URL getResource(@Nullable String path) throws MalformedURLException { 584 if (path == null) 585 return null; 586 587 if (!path.startsWith("/")) 588 throw new MalformedURLException("ServletContext resource paths must start with '/'"); 589 590 ResourceRoot root = getResourceRoot().orElse(null); 591 return root == null ? null : root.getResource(path); 592 } 593 594 @Override 595 @Nullable 596 public InputStream getResourceAsStream(@Nullable String path) { 597 if (path == null || !path.startsWith("/")) 598 return null; 599 600 ResourceRoot root = getResourceRoot().orElse(null); 601 return root == null ? null : root.getResourceAsStream(path); 602 } 603 604 @Override 605 @Nullable 606 public RequestDispatcher getRequestDispatcher(@Nullable String path) { 607 if (path == null || path.isBlank()) 608 return null; 609 610 return null; 611 } 612 613 @Override 614 @Nullable 615 public RequestDispatcher getNamedDispatcher(@Nullable String name) { 616 return null; 617 } 618 619 @Override 620 @Deprecated 621 @Nullable 622 public Servlet getServlet(@Nullable String name) throws ServletException { 623 // Deliberately null per spec b/c this method is deprecated 624 return null; 625 } 626 627 @Override 628 @Deprecated 629 @NonNull 630 public Enumeration<@NonNull Servlet> getServlets() { 631 // Deliberately empty per spec b/c this method is deprecated 632 return Collections.emptyEnumeration(); 633 } 634 635 @Override 636 @Deprecated 637 @NonNull 638 public Enumeration<@NonNull String> getServletNames() { 639 // Deliberately empty per spec b/c this method is deprecated 640 return Collections.emptyEnumeration(); 641 } 642 643 @Override 644 public void log(@Nullable String msg) { 645 if (msg == null) 646 return; 647 648 try { 649 synchronized (this.logLock) { 650 getLogWriter().write(msg); 651 } 652 } catch (IOException e) { 653 throw new UncheckedIOException(e); 654 } 655 } 656 657 @Override 658 @Deprecated 659 public void log(@Nullable Exception exception, 660 @Nullable String msg) { 661 if (exception == null && msg == null) 662 return; 663 664 log(msg, exception); 665 } 666 667 @Override 668 public void log(@Nullable String message, 669 @Nullable Throwable throwable) { 670 List<@NonNull String> components = new ArrayList<>(2); 671 672 if (message != null) 673 components.add(message); 674 675 if (throwable != null) { 676 StringWriter stringWriter = new StringWriter(); 677 PrintWriter printWriter = new PrintWriter(stringWriter); 678 throwable.printStackTrace(printWriter); 679 components.add(stringWriter.toString()); 680 } 681 682 String combinedMessage = components.stream().collect(Collectors.joining("\n")); 683 684 try { 685 synchronized (this.logLock) { 686 getLogWriter().write(combinedMessage); 687 } 688 } catch (IOException e) { 689 throw new UncheckedIOException(e); 690 } 691 } 692 693 @Override 694 @Nullable 695 public String getRealPath(@Nullable String path) { 696 // Soklet has no concept of a physical path on the filesystem for a URL path 697 return null; 698 } 699 700 @Override 701 @NonNull 702 public String getServerInfo() { 703 return "Soklet/Undefined"; 704 } 705 706 @Override 707 @Nullable 708 public String getInitParameter(String name) { 709 // Soklet has no concept of init parameters 710 return null; 711 } 712 713 @Override 714 @NonNull 715 public Enumeration<@NonNull String> getInitParameterNames() { 716 // Soklet has no concept of init parameters 717 return Collections.emptyEnumeration(); 718 } 719 720 @Override 721 public boolean setInitParameter(@Nullable String name, 722 @Nullable String value) { 723 throw new IllegalStateException(format("Soklet does not support %s init parameters.", 724 ServletContext.class.getSimpleName())); 725 } 726 727 @Override 728 @Nullable 729 public Object getAttribute(@Nullable String name) { 730 return name == null ? null : getAttributes().get(name); 731 } 732 733 @Override 734 @NonNull 735 public Enumeration<@NonNull String> getAttributeNames() { 736 return Collections.enumeration(getAttributes().keySet()); 737 } 738 739 @Override 740 public void setAttribute(@Nullable String name, 741 @Nullable Object object) { 742 if (name == null) 743 return; 744 745 if (object == null) 746 removeAttribute(name); 747 else 748 getAttributes().put(name, object); 749 } 750 751 @Override 752 public void removeAttribute(@Nullable String name) { 753 if (name == null) 754 return; 755 756 getAttributes().remove(name); 757 } 758 759 @Override 760 @Nullable 761 public String getServletContextName() { 762 // This is legal according to spec 763 return null; 764 } 765 766 @Override 767 public ServletRegistration.@Nullable Dynamic addServlet(@Nullable String servletName, 768 @Nullable String className) { 769 throw new IllegalStateException("Soklet does not support adding Servlets"); 770 } 771 772 @Override 773 public ServletRegistration.@Nullable Dynamic addServlet(@Nullable String servletName, 774 @Nullable Servlet servlet) { 775 throw new IllegalStateException("Soklet does not support adding Servlets"); 776 } 777 778 @Override 779 public ServletRegistration.@Nullable Dynamic addServlet(@Nullable String servletName, 780 @Nullable Class<? extends Servlet> servletClass) { 781 throw new IllegalStateException("Soklet does not support adding Servlets"); 782 } 783 784 @Override 785 public ServletRegistration.@Nullable Dynamic addJspFile(@Nullable String servletName, 786 @Nullable String jspFile) { 787 throw new IllegalStateException("Soklet does not support adding JSP files"); 788 } 789 790 @Override 791 @Nullable 792 public <T extends Servlet> T createServlet(@Nullable Class<T> clazz) throws ServletException { 793 throw new ServletException("Soklet does not support creating Servlets"); 794 } 795 796 @Override 797 @Nullable 798 public ServletRegistration getServletRegistration(@Nullable String servletName) { 799 // This is legal according to spec 800 return null; 801 } 802 803 @Override 804 @NonNull 805 public Map<@NonNull String, ? extends @NonNull ServletRegistration> getServletRegistrations() { 806 return Map.of(); 807 } 808 809 @Override 810 public FilterRegistration.@Nullable Dynamic addFilter(@Nullable String filterName, 811 @Nullable String className) { 812 throw new IllegalStateException("Soklet does not support adding Filters"); 813 } 814 815 @Override 816 public FilterRegistration.@Nullable Dynamic addFilter(@Nullable String filterName, 817 @Nullable Filter filter) { 818 throw new IllegalStateException("Soklet does not support adding Filters"); 819 } 820 821 @Override 822 public FilterRegistration.@Nullable Dynamic addFilter(@Nullable String filterName, 823 @Nullable Class<? extends Filter> filterClass) { 824 throw new IllegalStateException("Soklet does not support adding Filters"); 825 } 826 827 @Override 828 @Nullable 829 public <T extends Filter> T createFilter(@Nullable Class<T> clazz) throws ServletException { 830 throw new ServletException("Soklet does not support creating Filters"); 831 } 832 833 @Override 834 @Nullable 835 public FilterRegistration getFilterRegistration(@Nullable String filterName) { 836 // This is legal according to spec 837 return null; 838 } 839 840 @Override 841 @NonNull 842 public Map<@NonNull String, ? extends @NonNull FilterRegistration> getFilterRegistrations() { 843 return Map.of(); 844 } 845 846 @Override 847 @Nullable 848 public SessionCookieConfig getSessionCookieConfig() { 849 // Diverges from spec here; Soklet has no concept of "session cookie" 850 throw new IllegalStateException("Soklet does not support session cookies"); 851 } 852 853 @Override 854 public void setSessionTrackingModes(@Nullable Set<@NonNull SessionTrackingMode> sessionTrackingModes) { 855 throw new IllegalStateException("Soklet does not support session tracking"); 856 } 857 858 @Override 859 @NonNull 860 public Set<@NonNull SessionTrackingMode> getDefaultSessionTrackingModes() { 861 return Set.of(); 862 } 863 864 @Override 865 @NonNull 866 public Set<@NonNull SessionTrackingMode> getEffectiveSessionTrackingModes() { 867 return Set.of(); 868 } 869 870 @Override 871 public void addListener(@Nullable String className) { 872 throw new IllegalStateException("Soklet does not support listeners"); 873 } 874 875 @Override 876 public <T extends EventListener> void addListener(@Nullable T t) { 877 throw new IllegalStateException("Soklet does not support listeners"); 878 } 879 880 @Override 881 public void addListener(@Nullable Class<? extends EventListener> listenerClass) { 882 throw new IllegalStateException("Soklet does not support listeners"); 883 } 884 885 @Override 886 @Nullable 887 public <T extends EventListener> T createListener(@Nullable Class<T> clazz) throws ServletException { 888 throw new ServletException("Soklet does not support listeners"); 889 } 890 891 @Override 892 @Nullable 893 public JspConfigDescriptor getJspConfigDescriptor() { 894 // This is legal according to spec 895 return null; 896 } 897 898 @Override 899 @NonNull 900 public ClassLoader getClassLoader() { 901 return this.getClass().getClassLoader(); 902 } 903 904 @Override 905 public void declareRoles(@Nullable String @Nullable ... strings) { 906 throw new IllegalStateException("Soklet does not support Servlet roles"); 907 } 908 909 @Override 910 @NonNull 911 public String getVirtualServerName() { 912 return "soklet"; 913 } 914 915 @Override 916 public int getSessionTimeout() { 917 Integer timeout = this.sessionTimeout; 918 return timeout == null ? -1 : timeout; 919 } 920 921 @Override 922 public void setSessionTimeout(int sessionTimeout) { 923 this.sessionTimeout = sessionTimeout; 924 } 925 926 @Override 927 @Nullable 928 public String getRequestCharacterEncoding() { 929 return this.requestCharset == null ? null : this.requestCharset.name(); 930 } 931 932 @Override 933 public void setRequestCharacterEncoding(@Nullable String encoding) { 934 if (encoding == null) { 935 this.requestCharset = null; 936 return; 937 } 938 939 try { 940 this.requestCharset = Charset.forName(encoding); 941 } catch (Exception ignored) { 942 // Ignore invalid charset tokens. 943 } 944 } 945 946 @Override 947 @Nullable 948 public String getResponseCharacterEncoding() { 949 return this.responseCharset == null ? null : this.responseCharset.name(); 950 } 951 952 @Override 953 public void setResponseCharacterEncoding(@Nullable String encoding) { 954 if (encoding == null) { 955 this.responseCharset = null; 956 return; 957 } 958 959 try { 960 this.responseCharset = Charset.forName(encoding); 961 } catch (Exception ignored) { 962 // Ignore invalid charset tokens. 963 } 964 } 965}