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.jakarta; 018 019import jakarta.servlet.Filter; 020import jakarta.servlet.FilterRegistration; 021import jakarta.servlet.RequestDispatcher; 022import jakarta.servlet.Servlet; 023import jakarta.servlet.ServletContext; 024import jakarta.servlet.ServletException; 025import jakarta.servlet.ServletRegistration; 026import jakarta.servlet.SessionCookieConfig; 027import jakarta.servlet.SessionTrackingMode; 028import jakarta.servlet.descriptor.JspConfigDescriptor; 029import org.jspecify.annotations.NonNull; 030import org.jspecify.annotations.Nullable; 031 032import javax.annotation.concurrent.NotThreadSafe; 033import javax.annotation.concurrent.ThreadSafe; 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.URLClassLoader; 043import java.net.URLConnection; 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 500 @ThreadSafe 501 private static class NoOpWriter extends Writer { 502 @Override 503 public void write(@NonNull char[] cbuf, 504 int off, 505 int len) throws IOException { 506 requireNonNull(cbuf); 507 // No-op 508 } 509 510 @Override 511 public void flush() throws IOException { 512 // No-op 513 } 514 515 @Override 516 public void close() throws IOException { 517 // No-op 518 } 519 } 520 521 // Implementation of ServletContext methods below: 522 523 @Override 524 @Nullable 525 public String getContextPath() { 526 return ""; 527 } 528 529 @Override 530 @Nullable 531 public ServletContext getContext(@Nullable String uripath) { 532 if (uripath == null) 533 return null; 534 535 String normalized = uripath.trim(); 536 537 if (normalized.isEmpty() || "/".equals(normalized)) 538 return this; 539 540 return null; 541 } 542 543 @Override 544 public int getMajorVersion() { 545 return 4; 546 } 547 548 @Override 549 public int getMinorVersion() { 550 return 0; 551 } 552 553 @Override 554 public int getEffectiveMajorVersion() { 555 return 4; 556 } 557 558 @Override 559 public int getEffectiveMinorVersion() { 560 return 0; 561 } 562 563 @Override 564 @Nullable 565 public String getMimeType(@Nullable String file) { 566 if (file == null) 567 return null; 568 569 return URLConnection.guessContentTypeFromName(file); 570 } 571 572 @Override 573 @Nullable 574 public Set<@NonNull String> getResourcePaths(@Nullable String path) { 575 if (path == null || !path.startsWith("/")) 576 return null; 577 578 ResourceRoot root = getResourceRoot().orElse(null); 579 return root == null ? null : root.getResourcePaths(path); 580 } 581 582 @Override 583 @Nullable 584 public URL getResource(@Nullable String path) throws MalformedURLException { 585 if (path == null) 586 return null; 587 588 if (!path.startsWith("/")) 589 throw new MalformedURLException("ServletContext resource paths must start with '/'"); 590 591 ResourceRoot root = getResourceRoot().orElse(null); 592 return root == null ? null : root.getResource(path); 593 } 594 595 @Override 596 @Nullable 597 public InputStream getResourceAsStream(@Nullable String path) { 598 if (path == null || !path.startsWith("/")) 599 return null; 600 601 ResourceRoot root = getResourceRoot().orElse(null); 602 return root == null ? null : root.getResourceAsStream(path); 603 } 604 605 @Override 606 @Nullable 607 public RequestDispatcher getRequestDispatcher(@Nullable String path) { 608 if (path == null || path.isBlank()) 609 return null; 610 611 return null; 612 } 613 614 @Override 615 @Nullable 616 public RequestDispatcher getNamedDispatcher(@Nullable String name) { 617 return null; 618 } 619 620 @Override 621 public void log(@Nullable String msg) { 622 if (msg == null) 623 return; 624 625 try { 626 synchronized (this.logLock) { 627 getLogWriter().write(msg); 628 } 629 } catch (IOException e) { 630 throw new UncheckedIOException(e); 631 } 632 } 633 634 @Override 635 public void log(@Nullable String message, 636 @Nullable Throwable throwable) { 637 List<@NonNull String> components = new ArrayList<>(2); 638 639 if (message != null) 640 components.add(message); 641 642 if (throwable != null) { 643 StringWriter stringWriter = new StringWriter(); 644 PrintWriter printWriter = new PrintWriter(stringWriter); 645 throwable.printStackTrace(printWriter); 646 components.add(stringWriter.toString()); 647 } 648 649 String combinedMessage = components.stream().collect(Collectors.joining("\n")); 650 651 try { 652 synchronized (this.logLock) { 653 getLogWriter().write(combinedMessage); 654 } 655 } catch (IOException e) { 656 throw new UncheckedIOException(e); 657 } 658 } 659 660 @Override 661 @Nullable 662 public String getRealPath(@Nullable String path) { 663 // Soklet has no concept of a physical path on the filesystem for a URL path 664 return null; 665 } 666 667 @Override 668 @NonNull 669 public String getServerInfo() { 670 return "Soklet/Undefined"; 671 } 672 673 @Override 674 @Nullable 675 public String getInitParameter(String name) { 676 // Soklet has no concept of init parameters 677 return null; 678 } 679 680 @Override 681 @NonNull 682 public Enumeration<@NonNull String> getInitParameterNames() { 683 // Soklet has no concept of init parameters 684 return Collections.emptyEnumeration(); 685 } 686 687 @Override 688 public boolean setInitParameter(@Nullable String name, 689 @Nullable String value) { 690 throw new IllegalStateException(format("Soklet does not support %s init parameters.", 691 ServletContext.class.getSimpleName())); 692 } 693 694 @Override 695 @Nullable 696 public Object getAttribute(@Nullable String name) { 697 return name == null ? null : getAttributes().get(name); 698 } 699 700 @Override 701 @NonNull 702 public Enumeration<@NonNull String> getAttributeNames() { 703 return Collections.enumeration(getAttributes().keySet()); 704 } 705 706 @Override 707 public void setAttribute(@Nullable String name, 708 @Nullable Object object) { 709 if (name == null) 710 return; 711 712 if (object == null) 713 removeAttribute(name); 714 else 715 getAttributes().put(name, object); 716 } 717 718 @Override 719 public void removeAttribute(@Nullable String name) { 720 if (name == null) 721 return; 722 723 getAttributes().remove(name); 724 } 725 726 @Override 727 @Nullable 728 public String getServletContextName() { 729 // This is legal according to spec 730 return null; 731 } 732 733 @Override 734 public ServletRegistration.@Nullable Dynamic addServlet(@Nullable String servletName, 735 @Nullable String className) { 736 throw new IllegalStateException("Soklet does not support adding Servlets"); 737 } 738 739 @Override 740 public ServletRegistration.@Nullable Dynamic addServlet(@Nullable String servletName, 741 @Nullable Servlet servlet) { 742 throw new IllegalStateException("Soklet does not support adding Servlets"); 743 } 744 745 @Override 746 public ServletRegistration.@Nullable Dynamic addServlet(@Nullable String servletName, 747 @Nullable Class<? extends Servlet> servletClass) { 748 throw new IllegalStateException("Soklet does not support adding Servlets"); 749 } 750 751 @Override 752 public ServletRegistration.@Nullable Dynamic addJspFile(@Nullable String servletName, 753 @Nullable String jspFile) { 754 throw new IllegalStateException("Soklet does not support adding JSP files"); 755 } 756 757 @Override 758 @Nullable 759 public <T extends Servlet> T createServlet(@Nullable Class<T> clazz) throws ServletException { 760 throw new ServletException("Soklet does not support creating Servlets"); 761 } 762 763 @Override 764 @Nullable 765 public ServletRegistration getServletRegistration(@Nullable String servletName) { 766 // This is legal according to spec 767 return null; 768 } 769 770 @Override 771 @NonNull 772 public Map<@NonNull String, ? extends @NonNull ServletRegistration> getServletRegistrations() { 773 return Map.of(); 774 } 775 776 @Override 777 public FilterRegistration.@Nullable Dynamic addFilter(@Nullable String filterName, 778 @Nullable String className) { 779 throw new IllegalStateException("Soklet does not support adding Filters"); 780 } 781 782 @Override 783 public FilterRegistration.@Nullable Dynamic addFilter(@Nullable String filterName, 784 @Nullable Filter filter) { 785 throw new IllegalStateException("Soklet does not support adding Filters"); 786 } 787 788 @Override 789 public FilterRegistration.@Nullable Dynamic addFilter(@Nullable String filterName, 790 @Nullable Class<? extends Filter> filterClass) { 791 throw new IllegalStateException("Soklet does not support adding Filters"); 792 } 793 794 @Override 795 @Nullable 796 public <T extends Filter> T createFilter(@Nullable Class<T> clazz) throws ServletException { 797 throw new ServletException("Soklet does not support creating Filters"); 798 } 799 800 @Override 801 @Nullable 802 public FilterRegistration getFilterRegistration(@Nullable String filterName) { 803 // This is legal according to spec 804 return null; 805 } 806 807 @Override 808 @NonNull 809 public Map<@NonNull String, ? extends @NonNull FilterRegistration> getFilterRegistrations() { 810 return Map.of(); 811 } 812 813 @Override 814 @Nullable 815 public SessionCookieConfig getSessionCookieConfig() { 816 // Diverges from spec here; Soklet has no concept of "session cookie" 817 throw new IllegalStateException("Soklet does not support session cookies"); 818 } 819 820 @Override 821 public void setSessionTrackingModes(@Nullable Set<@NonNull SessionTrackingMode> sessionTrackingModes) { 822 throw new IllegalStateException("Soklet does not support session tracking"); 823 } 824 825 @Override 826 @NonNull 827 public Set<@NonNull SessionTrackingMode> getDefaultSessionTrackingModes() { 828 return Set.of(); 829 } 830 831 @Override 832 @NonNull 833 public Set<@NonNull SessionTrackingMode> getEffectiveSessionTrackingModes() { 834 return Set.of(); 835 } 836 837 @Override 838 public void addListener(@Nullable String className) { 839 throw new IllegalStateException("Soklet does not support listeners"); 840 } 841 842 @Override 843 public <T extends EventListener> void addListener(@Nullable T t) { 844 throw new IllegalStateException("Soklet does not support listeners"); 845 } 846 847 @Override 848 public void addListener(@Nullable Class<? extends EventListener> listenerClass) { 849 throw new IllegalStateException("Soklet does not support listeners"); 850 } 851 852 @Override 853 @Nullable 854 public <T extends EventListener> T createListener(@Nullable Class<T> clazz) throws ServletException { 855 throw new ServletException("Soklet does not support listeners"); 856 } 857 858 @Override 859 @Nullable 860 public JspConfigDescriptor getJspConfigDescriptor() { 861 // This is legal according to spec 862 return null; 863 } 864 865 @Override 866 @NonNull 867 public ClassLoader getClassLoader() { 868 return this.getClass().getClassLoader(); 869 } 870 871 @Override 872 public void declareRoles(@Nullable String @Nullable ... strings) { 873 throw new IllegalStateException("Soklet does not support Servlet roles"); 874 } 875 876 @Override 877 @NonNull 878 public String getVirtualServerName() { 879 return "soklet"; 880 } 881 882 @Override 883 public int getSessionTimeout() { 884 Integer timeout = this.sessionTimeout; 885 return timeout == null ? -1 : timeout; 886 } 887 888 @Override 889 public void setSessionTimeout(int sessionTimeout) { 890 this.sessionTimeout = sessionTimeout; 891 } 892 893 @Override 894 @Nullable 895 public String getRequestCharacterEncoding() { 896 return this.requestCharset == null ? null : this.requestCharset.name(); 897 } 898 899 @Override 900 public void setRequestCharacterEncoding(@Nullable String encoding) { 901 if (encoding == null) { 902 this.requestCharset = null; 903 return; 904 } 905 906 try { 907 this.requestCharset = Charset.forName(encoding); 908 } catch (Exception ignored) { 909 // Ignore invalid charset tokens. 910 } 911 } 912 913 @Override 914 @Nullable 915 public String getResponseCharacterEncoding() { 916 return this.responseCharset == null ? null : this.responseCharset.name(); 917 } 918 919 @Override 920 public void setResponseCharacterEncoding(@Nullable String encoding) { 921 if (encoding == null) { 922 this.responseCharset = null; 923 return; 924 } 925 926 try { 927 this.responseCharset = Charset.forName(encoding); 928 } catch (Exception ignored) { 929 // Ignore invalid charset tokens. 930 } 931 } 932}