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}