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}