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