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}