001/*
002 * Copyright 2022-2025 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;
018
019import com.soklet.ServerSentEventRequestResult.HandshakeAccepted;
020import com.soklet.ServerSentEventRequestResult.HandshakeRejected;
021import com.soklet.internal.spring.LinkedCaseInsensitiveMap;
022
023import javax.annotation.Nonnull;
024import javax.annotation.Nullable;
025import javax.annotation.concurrent.ThreadSafe;
026import java.io.BufferedReader;
027import java.io.IOException;
028import java.io.InputStreamReader;
029import java.lang.reflect.InvocationTargetException;
030import java.nio.charset.Charset;
031import java.nio.charset.StandardCharsets;
032import java.time.Duration;
033import java.time.Instant;
034import java.time.ZoneId;
035import java.time.format.DateTimeFormatter;
036import java.util.ArrayList;
037import java.util.Collections;
038import java.util.EnumSet;
039import java.util.LinkedHashMap;
040import java.util.List;
041import java.util.Locale;
042import java.util.Map;
043import java.util.Map.Entry;
044import java.util.Objects;
045import java.util.Optional;
046import java.util.Set;
047import java.util.concurrent.ConcurrentHashMap;
048import java.util.concurrent.CopyOnWriteArraySet;
049import java.util.concurrent.CountDownLatch;
050import java.util.concurrent.atomic.AtomicBoolean;
051import java.util.concurrent.atomic.AtomicReference;
052import java.util.concurrent.locks.ReentrantLock;
053import java.util.function.Consumer;
054import java.util.stream.Collectors;
055
056import static com.soklet.Utilities.emptyByteArray;
057import static java.lang.String.format;
058import static java.util.Objects.requireNonNull;
059
060/**
061 * Soklet's main class - manages a {@link Server} (and optionally a {@link ServerSentEventServer}) using the provided system configuration.
062 * <p>
063 * <pre>{@code // Use out-of-the-box defaults
064 * SokletConfig config = SokletConfig.withServer(
065 *   Server.withPort(8080).build()
066 * ).build();
067 *
068 * try (Soklet soklet = Soklet.withConfig(config)) {
069 *   soklet.start();
070 *   System.out.println("Soklet started, press [enter] to exit");
071 *   soklet.awaitShutdown(ShutdownTrigger.ENTER_KEY);
072 * }}</pre>
073 * <p>
074 * Soklet also offers an off-network {@link Simulator} concept via {@link #runSimulator(SokletConfig, Consumer)}, useful for integration testing.
075 * <p>
076 * Given a <em>Resource Method</em>...
077 * <pre>{@code public class HelloResource {
078 *   @GET("/hello")
079 *   public String hello(@QueryParameter String name) {
080 *     return String.format("Hello, %s", name);
081 *   }
082 * }}</pre>
083 * ...we might test it like this:
084 * <pre>{@code @Test
085 * public void integrationTest() {
086 *   // Just use your app's existing configuration
087 *   SokletConfig config = obtainMySokletConfig();
088 *
089 *   // Instead of running on a real HTTP server that listens on a port,
090 *   // a non-network Simulator is provided against which you can
091 *   // issue requests and receive responses.
092 *   Soklet.runSimulator(config, (simulator -> {
093 *     // Construct a request
094 *     Request request = Request.withPath(HttpMethod.GET, "/hello")
095 *       .queryParameters(Map.of("name", Set.of("Mark")))
096 *       .build();
097 *
098 *     // Perform the request and get a handle to the response
099 *     RequestResult result = simulator.performRequest(request);
100 *     MarshaledResponse marshaledResponse = result.getMarshaledResponse();
101 *
102 *     // Verify status code
103 *     Integer expectedCode = 200;
104 *     Integer actualCode = marshaledResponse.getStatusCode();
105 *     assertEquals(expectedCode, actualCode, "Bad status code");
106 *
107 *     // Verify response body
108 *     marshaledResponse.getBody().ifPresentOrElse(body -> {
109 *       String expectedBody = "Hello, Mark";
110 *       String actualBody = new String(body, StandardCharsets.UTF_8);
111 *       assertEquals(expectedBody, actualBody, "Bad response body");
112 *     }, () -> {
113 *       Assertions.fail("No response body");
114 *     });
115 *   }));
116 * }}</pre>
117 * <p>
118 * The {@link Simulator} also supports Server-Sent Events.
119 * <p>
120 * Integration testing documentation is available at <a href="https://www.soklet.com/docs/testing">https://www.soklet.com/docs/testing</a>.
121 *
122 * @author <a href="https://www.revetkn.com">Mark Allen</a>
123 */
124@ThreadSafe
125public final class Soklet implements AutoCloseable {
126        @Nonnull
127        private static final Map<String, Set<String>> DEFAULT_ACCEPTED_HANDSHAKE_HEADERS;
128
129        static {
130                // Generally speaking, we always want these headers for SSE streaming responses.
131                // Users can override if they think necessary
132                LinkedCaseInsensitiveMap<Set<String>> defaultAcceptedHandshakeHeaders = new LinkedCaseInsensitiveMap<>(4);
133                defaultAcceptedHandshakeHeaders.put("Content-Type", Set.of("text/event-stream; charset=UTF-8"));
134                defaultAcceptedHandshakeHeaders.put("Cache-Control", Set.of("no-cache", "no-transform"));
135                defaultAcceptedHandshakeHeaders.put("Connection", Set.of("keep-alive"));
136                defaultAcceptedHandshakeHeaders.put("X-Accel-Buffering", Set.of("no"));
137
138                DEFAULT_ACCEPTED_HANDSHAKE_HEADERS = Collections.unmodifiableMap(defaultAcceptedHandshakeHeaders);
139        }
140
141        /**
142         * Acquires a Soklet instance with the given configuration.
143         *
144         * @param sokletConfig configuration that drives the Soklet system
145         * @return a Soklet instance
146         */
147        @Nonnull
148        public static Soklet withConfig(@Nonnull SokletConfig sokletConfig) {
149                requireNonNull(sokletConfig);
150                return new Soklet(sokletConfig);
151        }
152
153        @Nonnull
154        private final SokletConfig sokletConfig;
155        @Nonnull
156        private final ReentrantLock lock;
157        @Nonnull
158        private final AtomicReference<CountDownLatch> awaitShutdownLatchReference;
159
160        /**
161         * Creates a Soklet instance with the given configuration.
162         *
163         * @param sokletConfig configuration that drives the Soklet system
164         */
165        private Soklet(@Nonnull SokletConfig sokletConfig) {
166                requireNonNull(sokletConfig);
167
168                this.sokletConfig = sokletConfig;
169                this.lock = new ReentrantLock();
170                this.awaitShutdownLatchReference = new AtomicReference<>(new CountDownLatch(1));
171
172                // Fail fast in the event that Soklet appears misconfigured
173                if (sokletConfig.getResourceMethodResolver().getResourceMethods().size() == 0)
174                        throw new IllegalStateException(format("No Soklet Resource Methods were found. Please ensure your %s is configured correctly. "
175                                        + "See https://www.soklet.com/docs/request-handling#resource-method-resolution for details.", ResourceMethodResolver.class.getSimpleName()));
176
177                // Use a layer of indirection here so the Soklet type does not need to directly implement the `RequestHandler` interface.
178                // Reasoning: the `handleRequest` method for Soklet should not be public, which might lead to accidental invocation by users.
179                // That method should only be called by the managed `Server` instance.
180                Soklet soklet = this;
181
182                sokletConfig.getServer().initialize(getSokletConfig(), (request, marshaledResponseConsumer) -> {
183                        // Delegate to Soklet's internal request handling method
184                        soklet.handleRequest(request, ServerType.STANDARD_HTTP, marshaledResponseConsumer);
185                });
186
187                ServerSentEventServer serverSentEventServer = sokletConfig.getServerSentEventServer().orElse(null);
188
189                if (serverSentEventServer != null)
190                        serverSentEventServer.initialize(sokletConfig, (request, marshaledResponseConsumer) -> {
191                                // Delegate to Soklet's internal request handling method
192                                soklet.handleRequest(request, ServerType.SERVER_SENT_EVENT, marshaledResponseConsumer);
193                        });
194        }
195
196        /**
197         * Starts the managed server instance[s].
198         * <p>
199         * If the managed server[s] are already started, this is a no-op.
200         */
201        public void start() {
202                getLock().lock();
203
204                try {
205                        if (isStarted())
206                                return;
207
208                        getAwaitShutdownLatchReference().set(new CountDownLatch(1));
209
210                        SokletConfig sokletConfig = getSokletConfig();
211                        LifecycleInterceptor lifecycleInterceptor = sokletConfig.getLifecycleInterceptor();
212                        Server server = sokletConfig.getServer();
213
214                        lifecycleInterceptor.willStartServer(server);
215                        server.start();
216                        lifecycleInterceptor.didStartServer(server);
217
218                        ServerSentEventServer serverSentEventServer = sokletConfig.getServerSentEventServer().orElse(null);
219
220                        if (serverSentEventServer != null) {
221                                lifecycleInterceptor.willStartServerSentEventServer(serverSentEventServer);
222                                serverSentEventServer.start();
223                                lifecycleInterceptor.didStartServerSentEventServer(serverSentEventServer);
224                        }
225                } finally {
226                        getLock().unlock();
227                }
228        }
229
230        /**
231         * Stops the managed server instance[s].
232         * <p>
233         * If the managed server[s] are already stopped, this is a no-op.
234         */
235        public void stop() {
236                getLock().lock();
237
238                try {
239                        if (isStarted()) {
240                                SokletConfig sokletConfig = getSokletConfig();
241                                LifecycleInterceptor lifecycleInterceptor = sokletConfig.getLifecycleInterceptor();
242                                Server server = sokletConfig.getServer();
243
244                                if (server.isStarted()) {
245                                        lifecycleInterceptor.willStopServer(server);
246                                        server.stop();
247                                        lifecycleInterceptor.didStopServer(server);
248                                }
249
250                                ServerSentEventServer serverSentEventServer = sokletConfig.getServerSentEventServer().orElse(null);
251
252                                if (serverSentEventServer != null && serverSentEventServer.isStarted()) {
253                                        lifecycleInterceptor.willStopServerSentEventServer(serverSentEventServer);
254                                        serverSentEventServer.stop();
255                                        lifecycleInterceptor.didStopServerSentEventServer(serverSentEventServer);
256                                }
257                        }
258                } finally {
259                        try {
260                                getAwaitShutdownLatchReference().get().countDown();
261                        } finally {
262                                getLock().unlock();
263                        }
264                }
265        }
266
267        /**
268         * Blocks the current thread until JVM shutdown ({@code SIGTERM/SIGINT/System.exit(...)} and so forth), <strong>or</strong> if one of the provided {@code shutdownTriggers} occurs.
269         * <p>
270         * This method will automatically invoke this instance's {@link #stop()} method once it becomes unblocked.
271         * <p>
272         * <strong>Notes regarding {@link ShutdownTrigger#ENTER_KEY}:</strong>
273         * <ul>
274         *   <li>It will invoke {@link #stop()} on <i>all</i> Soklet instances, as stdin is process-wide</li>
275         *   <li>It is only supported for environments with an interactive TTY and will be ignored if none exists (e.g. running in a Docker container) - Soklet will detect this and fire {@link LifecycleInterceptor#didReceiveLogEvent(LogEvent)} with an event of type {@link LogEventType#CONFIGURATION_UNSUPPORTED}</li>
276         * </ul>
277         *
278         * @param shutdownTriggers additional trigger[s] which signal that shutdown should occur, e.g. {@link ShutdownTrigger#ENTER_KEY} for "enter key pressed"
279         * @throws InterruptedException if the current thread has its interrupted status set on entry to this method, or is interrupted while waiting
280         */
281        public void awaitShutdown(@Nullable ShutdownTrigger... shutdownTriggers) throws InterruptedException {
282                Thread shutdownHook = null;
283                boolean registeredEnterKeyShutdownTrigger = false;
284                Set<ShutdownTrigger> shutdownTriggersAsSet = shutdownTriggers == null || shutdownTriggers.length == 0 ? Set.of() : EnumSet.copyOf(Set.of(shutdownTriggers));
285
286                try {
287                        // Optionally listen for enter key
288                        if (shutdownTriggersAsSet.contains(ShutdownTrigger.ENTER_KEY)) {
289                                registeredEnterKeyShutdownTrigger = KeypressManager.register(this); // returns false if stdin unusable/disabled
290
291                                if (!registeredEnterKeyShutdownTrigger) {
292                                        LogEvent logEvent = LogEvent.with(
293                                                        LogEventType.CONFIGURATION_UNSUPPORTED,
294                                                        format("Ignoring request for %s.%s - it is unsupported in this environment (no interactive TTY detected)", ShutdownTrigger.class.getSimpleName(), ShutdownTrigger.ENTER_KEY.name())
295                                        ).build();
296
297                                        getSokletConfig().getLifecycleInterceptor().didReceiveLogEvent(logEvent);
298                                }
299                        }
300
301                        // Always register a shutdown hook
302                        shutdownHook = new Thread(() -> {
303                                try {
304                                        stop();
305                                } catch (Throwable ignored) {
306                                        // Nothing to do
307                                }
308                        }, "soklet-shutdown-hook");
309
310                        Runtime.getRuntime().addShutdownHook(shutdownHook);
311
312                        // Wait until "close" finishes
313                        getAwaitShutdownLatchReference().get().await();
314                } finally {
315                        if (registeredEnterKeyShutdownTrigger)
316                                KeypressManager.unregister(this);
317
318                        try {
319                                Runtime.getRuntime().removeShutdownHook(shutdownHook);
320                        } catch (IllegalStateException ignored) {
321                                // JVM shutting down
322                        }
323                }
324        }
325
326        /**
327         * Handles "awaitShutdown" for {@link ShutdownTrigger#ENTER_KEY} by listening to stdin - all Soklet instances are terminated on keypress.
328         */
329        @ThreadSafe
330        private static final class KeypressManager {
331                @Nonnull
332                private static final Set<Soklet> SOKLET_REGISTRY;
333                @Nonnull
334                private static final AtomicBoolean LISTENER_STARTED;
335
336                static {
337                        SOKLET_REGISTRY = new CopyOnWriteArraySet<>();
338                        LISTENER_STARTED = new AtomicBoolean(false);
339                }
340
341                /**
342                 * Register a Soklet for Enter-to-stop support. Returns true iff a listener is (or was already) active.
343                 * If System.in is not usable (or disabled), returns false and does nothing.
344                 */
345                @Nonnull
346                synchronized static Boolean register(@Nonnull Soklet soklet) {
347                        requireNonNull(soklet);
348
349                        // If stdin is not readable (e.g., container with no TTY), don't start a listener.
350                        if (!canReadFromStdin())
351                                return false;
352
353                        SOKLET_REGISTRY.add(soklet);
354
355                        // Start a single process-wide listener once.
356                        if (LISTENER_STARTED.compareAndSet(false, true)) {
357                                Thread thread = new Thread(KeypressManager::runLoop, "soklet-keypress-shutdown-listener");
358                                thread.setDaemon(true); // never block JVM exit
359                                thread.start();
360                        }
361
362                        return true;
363                }
364
365                synchronized static void unregister(@Nonnull Soklet soklet) {
366                        SOKLET_REGISTRY.remove(soklet);
367                        // We intentionally keep the listener alive; it's daemon and cheap.
368                        // If stdin hits EOF, the listener exits on its own.
369                }
370
371                /**
372                 * Heuristic: if System.in is present and calling available() doesn't throw,
373                 * treat it as readable. Works even in IDEs where System.console() is null.
374                 */
375                @Nonnull
376                private static Boolean canReadFromStdin() {
377                        if (System.in == null)
378                                return false;
379
380                        try {
381                                // available() >= 0 means stream is open; 0 means no buffered data (that’s fine).
382                                return System.in.available() >= 0;
383                        } catch (IOException e) {
384                                return false;
385                        }
386                }
387
388                /**
389                 * Single blocking read on stdin. On any line (or EOF), stop all registered servers.
390                 */
391                private static void runLoop() {
392                        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
393                                // Blocks until newline or EOF; EOF (null) happens with /dev/null or closed pipe.
394                                bufferedReader.readLine();
395                                stopAllSoklets();
396                        } catch (Throwable ignored) {
397                                // If stdin is closed mid-run, just exit quietly.
398                        }
399                }
400
401                synchronized private static void stopAllSoklets() {
402                        // Either a line or EOF → stop everything that’s currently registered.
403                        for (Soklet soklet : SOKLET_REGISTRY) {
404                                try {
405                                        soklet.stop();
406                                } catch (Throwable ignored) {
407                                        // Nothing to do
408                                }
409                        }
410                }
411
412                private KeypressManager() {}
413        }
414
415        /**
416         * Nonpublic "informal" implementation of {@link com.soklet.Server.RequestHandler} so Soklet does not need to expose {@code handleRequest} publicly.
417         * Reasoning: users of this library should never call {@code handleRequest} directly - it should only be invoked in response to events
418         * provided by a {@link Server} or {@link ServerSentEventServer} implementation.
419         */
420        protected void handleRequest(@Nonnull Request request,
421                                                                                                                         @Nonnull ServerType serverType,
422                                                                                                                         @Nonnull Consumer<RequestResult> requestResultConsumer) {
423                requireNonNull(request);
424                requireNonNull(serverType);
425                requireNonNull(requestResultConsumer);
426
427                Instant processingStarted = Instant.now();
428
429                SokletConfig sokletConfig = getSokletConfig();
430                ResourceMethodResolver resourceMethodResolver = sokletConfig.getResourceMethodResolver();
431                ResponseMarshaler responseMarshaler = sokletConfig.getResponseMarshaler();
432                LifecycleInterceptor lifecycleInterceptor = sokletConfig.getLifecycleInterceptor();
433
434                // Holders to permit mutable effectively-final variables
435                AtomicReference<MarshaledResponse> marshaledResponseHolder = new AtomicReference<>();
436                AtomicReference<Throwable> resourceMethodResolutionExceptionHolder = new AtomicReference<>();
437                AtomicReference<Request> requestHolder = new AtomicReference<>(request);
438                AtomicReference<ResourceMethod> resourceMethodHolder = new AtomicReference<>();
439                AtomicReference<RequestResult> requestResultHolder = new AtomicReference<>();
440
441                // Holders to permit mutable effectively-final state tracking
442                AtomicBoolean willStartResponseWritingCompleted = new AtomicBoolean(false);
443                AtomicBoolean didFinishResponseWritingCompleted = new AtomicBoolean(false);
444                AtomicBoolean didFinishRequestHandlingCompleted = new AtomicBoolean(false);
445
446                List<Throwable> throwables = new ArrayList<>(10);
447
448                Consumer<LogEvent> safelyLog = (logEvent -> {
449                        try {
450                                lifecycleInterceptor.didReceiveLogEvent(logEvent);
451                        } catch (Throwable throwable) {
452                                throwable.printStackTrace();
453                                throwables.add(throwable);
454                        }
455                });
456
457                requestHolder.set(request);
458
459                try {
460                        // Do we have an exact match for this resource method?
461                        resourceMethodHolder.set(resourceMethodResolver.resourceMethodForRequest(requestHolder.get(), serverType).orElse(null));
462                } catch (Throwable t) {
463                        safelyLog.accept(LogEvent.with(LogEventType.RESOURCE_METHOD_RESOLUTION_FAILED, "Unable to resolve Resource Method")
464                                        .throwable(t)
465                                        .request(requestHolder.get())
466                                        .build());
467
468                        // If an exception occurs here, keep track of it - we will surface them after letting LifecycleInterceptor
469                        // see that a request has come in.
470                        throwables.add(t);
471                        resourceMethodResolutionExceptionHolder.set(t);
472                }
473
474                try {
475                        lifecycleInterceptor.wrapRequest(request, resourceMethodHolder.get(), (wrappedRequest) -> {
476                                requestHolder.set(wrappedRequest);
477
478                                try {
479                                        lifecycleInterceptor.didStartRequestHandling(requestHolder.get(), resourceMethodHolder.get());
480                                } catch (Throwable t) {
481                                        safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_INTERCEPTOR_DID_START_REQUEST_HANDLING_FAILED,
482                                                                        format("An exception occurred while invoking %s::didStartRequestHandling",
483                                                                                        LifecycleInterceptor.class.getSimpleName()))
484                                                        .throwable(t)
485                                                        .request(requestHolder.get())
486                                                        .resourceMethod(resourceMethodHolder.get())
487                                                        .build());
488
489                                        throwables.add(t);
490                                }
491
492                                try {
493                                        lifecycleInterceptor.interceptRequest(request, resourceMethodHolder.get(), (interceptorRequest) -> {
494                                                requestHolder.set(interceptorRequest);
495
496                                                try {
497                                                        if (resourceMethodResolutionExceptionHolder.get() != null)
498                                                                throw resourceMethodResolutionExceptionHolder.get();
499
500                                                        RequestResult requestResult = toRequestResult(requestHolder.get(), resourceMethodHolder.get(), serverType);
501                                                        requestResultHolder.set(requestResult);
502
503                                                        MarshaledResponse originalMarshaledResponse = requestResult.getMarshaledResponse();
504                                                        MarshaledResponse updatedMarshaledResponse = requestResult.getMarshaledResponse();
505
506                                                        // A few special cases that are "global" in that they can affect all requests and
507                                                        // need to happen after marshaling the response...
508
509                                                        // 1. Customize response for HEAD (e.g. remove body, set Content-Length header)
510                                                        updatedMarshaledResponse = applyHeadResponseIfApplicable(request, updatedMarshaledResponse);
511
512                                                        // 2. Apply other standard response customizations (CORS, Content-Length)
513                                                        // Note that we don't want to write Content-Length for SSE "accepted" handshakes
514                                                        HandshakeResult handshakeResult = requestResult.getHandshakeResult().orElse(null);
515                                                        boolean suppressContentLength = handshakeResult != null && handshakeResult instanceof HandshakeResult.Accepted;
516
517                                                        updatedMarshaledResponse = applyCommonPropertiesToMarshaledResponse(request, updatedMarshaledResponse, suppressContentLength);
518
519                                                        // Update our result holder with the modified response if necessary
520                                                        if (originalMarshaledResponse != updatedMarshaledResponse) {
521                                                                marshaledResponseHolder.set(updatedMarshaledResponse);
522                                                                requestResultHolder.set(requestResult.copy()
523                                                                                .marshaledResponse(updatedMarshaledResponse)
524                                                                                .finish());
525                                                        }
526
527                                                        return updatedMarshaledResponse;
528                                                } catch (Throwable t) {
529                                                        if (!Objects.equals(t, resourceMethodResolutionExceptionHolder.get())) {
530                                                                throwables.add(t);
531
532                                                                safelyLog.accept(LogEvent.with(LogEventType.REQUEST_PROCESSING_FAILED,
533                                                                                                "An exception occurred while processing request")
534                                                                                .throwable(t)
535                                                                                .request(requestHolder.get())
536                                                                                .resourceMethod(resourceMethodHolder.get())
537                                                                                .build());
538                                                        }
539
540                                                        // Unhappy path.  Try to use configuration's exception response marshaler...
541                                                        try {
542                                                                MarshaledResponse marshaledResponse = responseMarshaler.forThrowable(requestHolder.get(), t, resourceMethodHolder.get());
543                                                                marshaledResponse = applyCommonPropertiesToMarshaledResponse(request, marshaledResponse);
544                                                                marshaledResponseHolder.set(marshaledResponse);
545
546                                                                return marshaledResponse;
547                                                        } catch (Throwable t2) {
548                                                                throwables.add(t2);
549
550                                                                safelyLog.accept(LogEvent.with(LogEventType.RESPONSE_MARSHALER_FOR_THROWABLE_FAILED,
551                                                                                                format("An exception occurred while trying to write an exception response for %s", t))
552                                                                                .throwable(t2)
553                                                                                .request(requestHolder.get())
554                                                                                .resourceMethod(resourceMethodHolder.get())
555                                                                                .build());
556
557                                                                // The configuration's exception response marshaler failed - provide a failsafe response to recover
558                                                                return provideFailsafeMarshaledResponse(requestHolder.get(), t2);
559                                                        }
560                                                }
561                                        }, (interceptorMarshaledResponse) -> {
562                                                marshaledResponseHolder.set(interceptorMarshaledResponse);
563                                        });
564                                } catch (Throwable t) {
565                                        throwables.add(t);
566
567                                        try {
568                                                // In the event that an error occurs during processing of a LifecycleInterceptor method, for example
569                                                safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_INTERCEPTOR_INTERCEPT_REQUEST_FAILED,
570                                                                                format("An exception occurred while invoking %s::interceptRequest", LifecycleInterceptor.class.getSimpleName()))
571                                                                .throwable(t)
572                                                                .request(requestHolder.get())
573                                                                .resourceMethod(resourceMethodHolder.get())
574                                                                .build());
575
576                                                MarshaledResponse marshaledResponse = responseMarshaler.forThrowable(requestHolder.get(), t, resourceMethodHolder.get());
577                                                marshaledResponse = applyCommonPropertiesToMarshaledResponse(request, marshaledResponse);
578                                                marshaledResponseHolder.set(marshaledResponse);
579                                        } catch (Throwable t2) {
580                                                throwables.add(t2);
581
582                                                safelyLog.accept(LogEvent.with(LogEventType.RESPONSE_MARSHALER_FOR_THROWABLE_FAILED,
583                                                                                format("An exception occurred while invoking %s::forThrowable when trying to write an exception response for %s", ResponseMarshaler.class.getSimpleName(), t))
584                                                                .throwable(t2)
585                                                                .request(requestHolder.get())
586                                                                .resourceMethod(resourceMethodHolder.get())
587                                                                .build());
588
589                                                marshaledResponseHolder.set(provideFailsafeMarshaledResponse(requestHolder.get(), t2));
590                                        }
591                                } finally {
592                                        try {
593                                                try {
594                                                        lifecycleInterceptor.willStartResponseWriting(requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get());
595                                                } finally {
596                                                        willStartResponseWritingCompleted.set(true);
597                                                }
598
599                                                Instant responseWriteStarted = Instant.now();
600
601                                                try {
602                                                        RequestResult requestResult = requestResultHolder.get();
603
604                                                        if (requestResult != null)
605                                                                requestResultConsumer.accept(requestResult);
606                                                        else
607                                                                requestResultConsumer.accept(RequestResult.withMarshaledResponse(marshaledResponseHolder.get())
608                                                                                .resourceMethod(resourceMethodHolder.get())
609                                                                                .build());
610
611                                                        Instant responseWriteFinished = Instant.now();
612                                                        Duration responseWriteDuration = Duration.between(responseWriteStarted, responseWriteFinished);
613
614                                                        try {
615                                                                lifecycleInterceptor.didFinishResponseWriting(requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get(), responseWriteDuration, null);
616                                                        } catch (Throwable t) {
617                                                                throwables.add(t);
618
619                                                                safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_INTERCEPTOR_DID_FINISH_RESPONSE_WRITING_FAILED,
620                                                                                                format("An exception occurred while invoking %s::didFinishResponseWriting",
621                                                                                                                LifecycleInterceptor.class.getSimpleName()))
622                                                                                .throwable(t)
623                                                                                .request(requestHolder.get())
624                                                                                .resourceMethod(resourceMethodHolder.get())
625                                                                                .marshaledResponse(marshaledResponseHolder.get())
626                                                                                .build());
627                                                        } finally {
628                                                                didFinishResponseWritingCompleted.set(true);
629                                                        }
630                                                } catch (Throwable t) {
631                                                        throwables.add(t);
632
633                                                        Instant responseWriteFinished = Instant.now();
634                                                        Duration responseWriteDuration = Duration.between(responseWriteStarted, responseWriteFinished);
635
636                                                        try {
637                                                                lifecycleInterceptor.didFinishResponseWriting(requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get(), responseWriteDuration, t);
638                                                        } catch (Throwable t2) {
639                                                                throwables.add(t2);
640
641                                                                safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_INTERCEPTOR_DID_FINISH_RESPONSE_WRITING_FAILED,
642                                                                                                format("An exception occurred while invoking %s::didFinishResponseWriting",
643                                                                                                                LifecycleInterceptor.class.getSimpleName()))
644                                                                                .throwable(t2)
645                                                                                .request(requestHolder.get())
646                                                                                .resourceMethod(resourceMethodHolder.get())
647                                                                                .marshaledResponse(marshaledResponseHolder.get())
648                                                                                .build());
649                                                        }
650                                                }
651                                        } finally {
652                                                try {
653                                                        Instant processingFinished = Instant.now();
654                                                        Duration processingDuration = Duration.between(processingStarted, processingFinished);
655
656                                                        lifecycleInterceptor.didFinishRequestHandling(requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get(), processingDuration, Collections.unmodifiableList(throwables));
657                                                } catch (Throwable t) {
658                                                        safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_INTERCEPTOR_DID_FINISH_REQUEST_HANDLING_FAILED,
659                                                                                        format("An exception occurred while invoking %s::didFinishRequestHandling",
660                                                                                                        LifecycleInterceptor.class.getSimpleName()))
661                                                                        .throwable(t)
662                                                                        .request(requestHolder.get())
663                                                                        .resourceMethod(resourceMethodHolder.get())
664                                                                        .marshaledResponse(marshaledResponseHolder.get())
665                                                                        .build());
666                                                } finally {
667                                                        didFinishRequestHandlingCompleted.set(true);
668                                                }
669                                        }
670                                }
671                        });
672                } catch (Throwable t) {
673                        // If an error occurred during request wrapping, it's possible a response was never written/communicated back to LifecycleInterceptor.
674                        // Detect that here and inform LifecycleInterceptor accordingly.
675                        safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_INTERCEPTOR_WRAP_REQUEST_FAILED,
676                                                        format("An exception occurred while invoking %s::wrapRequest",
677                                                                        LifecycleInterceptor.class.getSimpleName()))
678                                        .throwable(t)
679                                        .request(requestHolder.get())
680                                        .resourceMethod(resourceMethodHolder.get())
681                                        .marshaledResponse(marshaledResponseHolder.get())
682                                        .build());
683
684                        // If we don't have a response, let the marshaler try to make one for the exception.
685                        // If that fails, use the failsafe.
686                        if (marshaledResponseHolder.get() == null) {
687                                try {
688                                        MarshaledResponse marshaledResponse = responseMarshaler.forThrowable(requestHolder.get(), t, resourceMethodHolder.get());
689                                        marshaledResponse = applyCommonPropertiesToMarshaledResponse(request, marshaledResponse);
690                                        marshaledResponseHolder.set(marshaledResponse);
691                                } catch (Throwable t2) {
692                                        throwables.add(t2);
693
694                                        safelyLog.accept(LogEvent.with(LogEventType.RESPONSE_MARSHALER_FOR_THROWABLE_FAILED,
695                                                                        format("An exception occurred during request wrapping while invoking %s::forThrowable",
696                                                                                        ResponseMarshaler.class.getSimpleName()))
697                                                        .throwable(t2)
698                                                        .request(requestHolder.get())
699                                                        .resourceMethod(resourceMethodHolder.get())
700                                                        .marshaledResponse(marshaledResponseHolder.get())
701                                                        .build());
702
703                                        marshaledResponseHolder.set(provideFailsafeMarshaledResponse(requestHolder.get(), t));
704                                }
705                        }
706
707                        if (!willStartResponseWritingCompleted.get()) {
708                                try {
709                                        lifecycleInterceptor.willStartResponseWriting(requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get());
710                                } catch (Throwable t2) {
711                                        safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_INTERCEPTOR_WILL_START_RESPONSE_WRITING_FAILED,
712                                                                        format("An exception occurred while invoking %s::willStartResponseWriting",
713                                                                                        LifecycleInterceptor.class.getSimpleName()))
714                                                        .throwable(t2)
715                                                        .request(requestHolder.get())
716                                                        .resourceMethod(resourceMethodHolder.get())
717                                                        .marshaledResponse(marshaledResponseHolder.get())
718                                                        .build());
719                                }
720                        }
721
722                        try {
723                                Instant responseWriteStarted = Instant.now();
724
725                                if (!didFinishResponseWritingCompleted.get()) {
726                                        try {
727                                                RequestResult requestResult = requestResultHolder.get();
728
729                                                if (requestResult != null)
730                                                        requestResultConsumer.accept(requestResult);
731                                                else
732                                                        requestResultConsumer.accept(RequestResult.withMarshaledResponse(marshaledResponseHolder.get())
733                                                                        .resourceMethod(resourceMethodHolder.get())
734                                                                        .build());
735
736                                                Instant responseWriteFinished = Instant.now();
737                                                Duration responseWriteDuration = Duration.between(responseWriteStarted, responseWriteFinished);
738
739                                                try {
740                                                        lifecycleInterceptor.didFinishResponseWriting(requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get(), responseWriteDuration, null);
741                                                } catch (Throwable t2) {
742                                                        throwables.add(t2);
743
744                                                        safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_INTERCEPTOR_DID_FINISH_RESPONSE_WRITING_FAILED,
745                                                                                        format("An exception occurred while invoking %s::didFinishResponseWriting",
746                                                                                                        LifecycleInterceptor.class.getSimpleName()))
747                                                                        .throwable(t2)
748                                                                        .request(requestHolder.get())
749                                                                        .resourceMethod(resourceMethodHolder.get())
750                                                                        .marshaledResponse(marshaledResponseHolder.get())
751                                                                        .build());
752                                                }
753                                        } catch (Throwable t2) {
754                                                throwables.add(t2);
755
756                                                Instant responseWriteFinished = Instant.now();
757                                                Duration responseWriteDuration = Duration.between(responseWriteStarted, responseWriteFinished);
758
759                                                try {
760                                                        lifecycleInterceptor.didFinishResponseWriting(requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get(), responseWriteDuration, t);
761                                                } catch (Throwable t3) {
762                                                        throwables.add(t3);
763
764                                                        safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_INTERCEPTOR_DID_FINISH_RESPONSE_WRITING_FAILED,
765                                                                                        format("An exception occurred while invoking %s::didFinishResponseWriting",
766                                                                                                        LifecycleInterceptor.class.getSimpleName()))
767                                                                        .throwable(t3)
768                                                                        .request(requestHolder.get())
769                                                                        .resourceMethod(resourceMethodHolder.get())
770                                                                        .marshaledResponse(marshaledResponseHolder.get())
771                                                                        .build());
772                                                }
773                                        }
774                                }
775                        } finally {
776                                if (!didFinishRequestHandlingCompleted.get()) {
777                                        try {
778                                                Instant processingFinished = Instant.now();
779                                                Duration processingDuration = Duration.between(processingStarted, processingFinished);
780
781                                                lifecycleInterceptor.didFinishRequestHandling(requestHolder.get(), resourceMethodHolder.get(), marshaledResponseHolder.get(), processingDuration, Collections.unmodifiableList(throwables));
782                                        } catch (Throwable t2) {
783                                                safelyLog.accept(LogEvent.with(LogEventType.LIFECYCLE_INTERCEPTOR_DID_FINISH_REQUEST_HANDLING_FAILED,
784                                                                                format("An exception occurred while invoking %s::didFinishRequestHandling",
785                                                                                                LifecycleInterceptor.class.getSimpleName()))
786                                                                .throwable(t2)
787                                                                .request(requestHolder.get())
788                                                                .resourceMethod(resourceMethodHolder.get())
789                                                                .marshaledResponse(marshaledResponseHolder.get())
790                                                                .build());
791                                        }
792                                }
793                        }
794                }
795        }
796
797        @Nonnull
798        protected RequestResult toRequestResult(@Nonnull Request request,
799                                                                                                                                                                        @Nullable ResourceMethod resourceMethod,
800                                                                                                                                                                        @Nonnull ServerType serverType) throws Throwable {
801                requireNonNull(request);
802                requireNonNull(serverType);
803
804                ResourceMethodParameterProvider resourceMethodParameterProvider = getSokletConfig().getResourceMethodParameterProvider();
805                InstanceProvider instanceProvider = getSokletConfig().getInstanceProvider();
806                CorsAuthorizer corsAuthorizer = getSokletConfig().getCorsAuthorizer();
807                ResourceMethodResolver resourceMethodResolver = getSokletConfig().getResourceMethodResolver();
808                ResponseMarshaler responseMarshaler = getSokletConfig().getResponseMarshaler();
809                CorsPreflight corsPreflight = request.getCorsPreflight().orElse(null);
810
811                // Special short-circuit for big requests
812                if (request.isContentTooLarge())
813                        return RequestResult.withMarshaledResponse(responseMarshaler.forContentTooLarge(request, resourceMethodResolver.resourceMethodForRequest(request, serverType).orElse(null)))
814                                        .resourceMethod(resourceMethod)
815                                        .build();
816
817                // Special short-circuit for OPTIONS *
818                if (request.getResourcePath() == ResourcePath.OPTIONS_SPLAT_RESOURCE_PATH)
819                        return RequestResult.withMarshaledResponse(responseMarshaler.forOptionsSplat(request)).build();
820
821                // No resource method was found for this HTTP method and path.
822                if (resourceMethod == null) {
823                        // If this was an OPTIONS request, do special processing.
824                        // If not, figure out if we should return a 404 or 405.
825                        if (request.getHttpMethod() == HttpMethod.OPTIONS) {
826                                // See what methods are available to us for this request's path
827                                Map<HttpMethod, ResourceMethod> matchingResourceMethodsByHttpMethod = resolveMatchingResourceMethodsByHttpMethod(request, resourceMethodResolver, serverType);
828
829                                // Special handling for CORS preflight requests, if needed
830                                if (corsPreflight != null) {
831                                        // Let configuration function determine if we should authorize this request.
832                                        // Discard any OPTIONS references - see https://stackoverflow.com/a/68529748
833                                        Map<HttpMethod, ResourceMethod> nonOptionsMatchingResourceMethodsByHttpMethod = matchingResourceMethodsByHttpMethod.entrySet().stream()
834                                                        .filter(entry -> entry.getKey() != HttpMethod.OPTIONS)
835                                                        .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
836
837                                        CorsPreflightResponse corsPreflightResponse = corsAuthorizer.authorizePreflight(request, corsPreflight, nonOptionsMatchingResourceMethodsByHttpMethod).orElse(null);
838
839                                        // Allow or reject CORS depending on what the function said to do
840                                        if (corsPreflightResponse != null) {
841                                                // Allow
842                                                MarshaledResponse marshaledResponse = responseMarshaler.forCorsPreflightAllowed(request, corsPreflight, corsPreflightResponse);
843
844                                                return RequestResult.withMarshaledResponse(marshaledResponse)
845                                                                .corsPreflightResponse(corsPreflightResponse)
846                                                                .resourceMethod(resourceMethod)
847                                                                .build();
848                                        }
849
850                                        // Reject
851                                        return RequestResult.withMarshaledResponse(responseMarshaler.forCorsPreflightRejected(request, corsPreflight))
852                                                        .resourceMethod(resourceMethod)
853                                                        .build();
854                                } else {
855                                        // Just a normal OPTIONS response (non-CORS-preflight).
856                                        // If there's a matching OPTIONS resource method for this OPTIONS request, then invoke it.
857                                        ResourceMethod optionsResourceMethod = matchingResourceMethodsByHttpMethod.get(HttpMethod.OPTIONS);
858
859                                        if (optionsResourceMethod != null) {
860                                                resourceMethod = optionsResourceMethod;
861                                        } else {
862                                                // Ensure OPTIONS is always present in the map, even if there is no explicit matching resource method for it
863                                                if (!matchingResourceMethodsByHttpMethod.containsKey(HttpMethod.OPTIONS))
864                                                        matchingResourceMethodsByHttpMethod.put(HttpMethod.OPTIONS, null);
865
866                                                // Ensure HEAD is always present in the map, even if there is no explicit matching resource method for it
867                                                if (!matchingResourceMethodsByHttpMethod.containsKey(HttpMethod.HEAD))
868                                                        matchingResourceMethodsByHttpMethod.put(HttpMethod.HEAD, null);
869
870                                                return RequestResult.withMarshaledResponse(responseMarshaler.forOptions(request, matchingResourceMethodsByHttpMethod.keySet()))
871                                                                .resourceMethod(resourceMethod)
872                                                                .build();
873                                        }
874                                }
875                        } else if (request.getHttpMethod() == HttpMethod.HEAD) {
876                                // If there's a matching GET resource method for this HEAD request, then invoke it
877                                Request headGetRequest = request.copy().httpMethod(HttpMethod.GET).finish();
878                                ResourceMethod headGetResourceMethod = resourceMethodResolver.resourceMethodForRequest(headGetRequest, serverType).orElse(null);
879
880                                if (headGetResourceMethod != null)
881                                        resourceMethod = headGetResourceMethod;
882                                else
883                                        return RequestResult.withMarshaledResponse(responseMarshaler.forNotFound(request))
884                                                        .resourceMethod(resourceMethod)
885                                                        .build();
886                        } else {
887                                // Not an OPTIONS request, so it's possible we have a 405. See if other HTTP methods match...
888                                Map<HttpMethod, ResourceMethod> otherMatchingResourceMethodsByHttpMethod = resolveMatchingResourceMethodsByHttpMethod(request, resourceMethodResolver, serverType);
889
890                                Set<HttpMethod> matchingNonOptionsHttpMethods = otherMatchingResourceMethodsByHttpMethod.keySet().stream()
891                                                .filter(httpMethod -> httpMethod != HttpMethod.OPTIONS)
892                                                .collect(Collectors.toSet());
893
894                                // Ensure OPTIONS is always present in the map, even if there is no explicit matching resource method for it
895                                if (!otherMatchingResourceMethodsByHttpMethod.containsKey(HttpMethod.OPTIONS))
896                                        otherMatchingResourceMethodsByHttpMethod.put(HttpMethod.OPTIONS, null);
897
898                                // Ensure HEAD is always present in the map, even if there is no explicit matching resource method for it
899                                if (!otherMatchingResourceMethodsByHttpMethod.containsKey(HttpMethod.HEAD))
900                                        otherMatchingResourceMethodsByHttpMethod.put(HttpMethod.HEAD, null);
901
902                                if (matchingNonOptionsHttpMethods.size() > 0) {
903                                        // ...if some do, it's a 405
904                                        return RequestResult.withMarshaledResponse(responseMarshaler.forMethodNotAllowed(request, otherMatchingResourceMethodsByHttpMethod.keySet()))
905                                                        .resourceMethod(resourceMethod)
906                                                        .build();
907                                } else {
908                                        // no matching resource method found, it's a 404
909                                        return RequestResult.withMarshaledResponse(responseMarshaler.forNotFound(request))
910                                                        .resourceMethod(resourceMethod)
911                                                        .build();
912                                }
913                        }
914                }
915
916                // Found a resource method - happy path.
917                // 1. Get an instance of the resource class
918                // 2. Get values to pass to the resource method on the resource class
919                // 3. Invoke the resource method and use its return value to drive a response
920                Class<?> resourceClass = resourceMethod.getMethod().getDeclaringClass();
921                Object resourceClassInstance;
922
923                try {
924                        resourceClassInstance = instanceProvider.provide(resourceClass);
925                } catch (Exception e) {
926                        throw new IllegalArgumentException(format("Unable to acquire an instance of %s", resourceClass.getName()), e);
927                }
928
929                List<Object> parameterValues = resourceMethodParameterProvider.parameterValuesForResourceMethod(request, resourceMethod);
930
931                Object responseObject;
932
933                try {
934                        responseObject = resourceMethod.getMethod().invoke(resourceClassInstance, parameterValues.toArray());
935                } catch (InvocationTargetException e) {
936                        if (e.getTargetException() != null)
937                                throw e.getTargetException();
938
939                        throw e;
940                }
941
942                // Unwrap the Optional<T>, if one exists.  We do not recurse deeper than one level
943                if (responseObject instanceof Optional<?>)
944                        responseObject = ((Optional<?>) responseObject).orElse(null);
945
946                Response response;
947                HandshakeResult handshakeResult = null;
948
949                // If null/void return, it's a 204
950                // If it's a MarshaledResponse object, no marshaling + return it immediately - caller knows exactly what it wants to write.
951                // If it's a Response object, use as is.
952                // If it's a non-Response type of object, assume it's the response body and wrap in a Response.
953                if (responseObject == null) {
954                        response = Response.withStatusCode(204).build();
955                } else if (responseObject instanceof MarshaledResponse) {
956                        return RequestResult.withMarshaledResponse((MarshaledResponse) responseObject)
957                                        .resourceMethod(resourceMethod)
958                                        .build();
959                } else if (responseObject instanceof Response) {
960                        response = (Response) responseObject;
961                } else if (responseObject instanceof HandshakeResult.Accepted accepted) { // SSE "accepted" handshake
962                        return RequestResult.withMarshaledResponse(toMarshaledResponse(accepted))
963                                        .resourceMethod(resourceMethod)
964                                        .handshakeResult(accepted)
965                                        .build();
966                } else if (responseObject instanceof HandshakeResult.Rejected rejected) { // SSE "rejected" handshake
967                        response = rejected.getResponse();
968                        handshakeResult = rejected;
969                } else {
970                        response = Response.withStatusCode(200).body(responseObject).build();
971                }
972
973                MarshaledResponse marshaledResponse = responseMarshaler.forResourceMethod(request, response, resourceMethod);
974
975                return RequestResult.withMarshaledResponse(marshaledResponse)
976                                .response(response)
977                                .resourceMethod(resourceMethod)
978                                .handshakeResult(handshakeResult)
979                                .build();
980        }
981
982        @Nonnull
983        private MarshaledResponse toMarshaledResponse(@Nonnull HandshakeResult.Accepted accepted) {
984                requireNonNull(accepted);
985
986                Map<String, Set<String>> headers = accepted.getHeaders();
987                LinkedCaseInsensitiveMap<Set<String>> finalHeaders = new LinkedCaseInsensitiveMap<>(DEFAULT_ACCEPTED_HANDSHAKE_HEADERS.size() + headers.size());
988
989                // Start with defaults
990                for (Map.Entry<String, Set<String>> e : DEFAULT_ACCEPTED_HANDSHAKE_HEADERS.entrySet())
991                        finalHeaders.put(e.getKey(), e.getValue()); // values already unmodifiable
992
993                // Overlay user-supplied headers (prefer user values on key collision)
994                for (Map.Entry<String, Set<String>> e : headers.entrySet()) {
995                        // Defensively copy so callers can't mutate after construction
996                        Set<String> values = e.getValue() == null ? Set.of() : Set.copyOf(e.getValue());
997                        finalHeaders.put(e.getKey(), values);
998                }
999
1000                return MarshaledResponse.withStatusCode(200)
1001                                .headers(finalHeaders)
1002                                .cookies(accepted.getCookies())
1003                                .build();
1004        }
1005
1006        @Nonnull
1007        protected MarshaledResponse applyHeadResponseIfApplicable(@Nonnull Request request,
1008                                                                                                                                                                                                                                                @Nonnull MarshaledResponse marshaledResponse) {
1009                if (request.getHttpMethod() != HttpMethod.HEAD)
1010                        return marshaledResponse;
1011
1012                return getSokletConfig().getResponseMarshaler().forHead(request, marshaledResponse);
1013        }
1014
1015        // Hat tip to Aslan Parçası and GrayStar
1016        @Nonnull
1017        protected MarshaledResponse applyCommonPropertiesToMarshaledResponse(@Nonnull Request request,
1018                                                                                                                                                                                                                                                                                         @Nonnull MarshaledResponse marshaledResponse) {
1019                requireNonNull(request);
1020                requireNonNull(marshaledResponse);
1021
1022                return applyCommonPropertiesToMarshaledResponse(request, marshaledResponse, false);
1023        }
1024
1025        @Nonnull
1026        protected MarshaledResponse applyCommonPropertiesToMarshaledResponse(@Nonnull Request request,
1027                                                                                                                                                                                                                                                                                         @Nonnull MarshaledResponse marshaledResponse,
1028                                                                                                                                                                                                                                                                                         @Nonnull Boolean suppressContentLength) {
1029                requireNonNull(request);
1030                requireNonNull(marshaledResponse);
1031                requireNonNull(suppressContentLength);
1032
1033                // Don't write Content-Length for an accepted SSE Handshake, for example
1034                if (!suppressContentLength)
1035                        marshaledResponse = applyContentLengthIfApplicable(request, marshaledResponse);
1036
1037                // If the Date header is missing, add it using our cached provider
1038                if (!marshaledResponse.getHeaders().containsKey("Date"))
1039                        marshaledResponse = marshaledResponse.copy()
1040                                        .headers(headers -> headers.put("Date", Set.of(CachedHttpDate.getCurrentValue())))
1041                                        .finish();
1042
1043                marshaledResponse = applyCorsResponseIfApplicable(request, marshaledResponse);
1044
1045                return marshaledResponse;
1046        }
1047
1048        @Nonnull
1049        protected MarshaledResponse applyContentLengthIfApplicable(@Nonnull Request request,
1050                                                                                                                                                                                                                                                 @Nonnull MarshaledResponse marshaledResponse) {
1051                requireNonNull(request);
1052                requireNonNull(marshaledResponse);
1053
1054                Set<String> normalizedHeaderNames = marshaledResponse.getHeaders().keySet().stream()
1055                                .map(headerName -> headerName.toLowerCase(Locale.US))
1056                                .collect(Collectors.toSet());
1057
1058                // If Content-Length is already specified, don't do anything
1059                if (normalizedHeaderNames.contains("content-length"))
1060                        return marshaledResponse;
1061
1062                // If Content-Length is not specified, specify as the number of bytes in the body
1063                return marshaledResponse.copy()
1064                                .headers((mutableHeaders) -> {
1065                                        String contentLengthHeaderValue = String.valueOf(marshaledResponse.getBody().orElse(emptyByteArray()).length);
1066                                        mutableHeaders.put("Content-Length", Set.of(contentLengthHeaderValue));
1067                                }).finish();
1068        }
1069
1070        @Nonnull
1071        protected MarshaledResponse applyCorsResponseIfApplicable(@Nonnull Request request,
1072                                                                                                                                                                                                                                                @Nonnull MarshaledResponse marshaledResponse) {
1073                requireNonNull(request);
1074                requireNonNull(marshaledResponse);
1075
1076                Cors cors = request.getCors().orElse(null);
1077
1078                // If non-CORS request, nothing further to do (note that CORS preflight was handled earlier)
1079                if (cors == null)
1080                        return marshaledResponse;
1081
1082                CorsAuthorizer corsAuthorizer = getSokletConfig().getCorsAuthorizer();
1083
1084                // Does the authorizer say we are authorized?
1085                CorsResponse corsResponse = corsAuthorizer.authorize(request, cors).orElse(null);
1086
1087                // Not authorized - don't apply CORS headers to the response
1088                if (corsResponse == null)
1089                        return marshaledResponse;
1090
1091                // Authorized - OK, let's apply the headers to the response
1092                return getSokletConfig().getResponseMarshaler().forCorsAllowed(request, cors, corsResponse, marshaledResponse);
1093        }
1094
1095        @Nonnull
1096        protected Map<HttpMethod, ResourceMethod> resolveMatchingResourceMethodsByHttpMethod(@Nonnull Request request,
1097                                                                                                                                                                                                                                                                                                                                                         @Nonnull ResourceMethodResolver resourceMethodResolver,
1098                                                                                                                                                                                                                                                                                                                                                         @Nonnull ServerType serverType) {
1099                requireNonNull(request);
1100                requireNonNull(resourceMethodResolver);
1101                requireNonNull(serverType);
1102
1103                // Special handling for OPTIONS *
1104                if (request.getResourcePath() == ResourcePath.OPTIONS_SPLAT_RESOURCE_PATH)
1105                        return new LinkedHashMap<>();
1106
1107                Map<HttpMethod, ResourceMethod> matchingResourceMethodsByHttpMethod = new LinkedHashMap<>(HttpMethod.values().length);
1108
1109                for (HttpMethod httpMethod : HttpMethod.values()) {
1110                        // Make a quick copy of the request to see if other paths match
1111                        Request otherRequest = Request.withPath(httpMethod, request.getPath()).build();
1112                        ResourceMethod resourceMethod = resourceMethodResolver.resourceMethodForRequest(otherRequest, serverType).orElse(null);
1113
1114                        if (resourceMethod != null)
1115                                matchingResourceMethodsByHttpMethod.put(httpMethod, resourceMethod);
1116                }
1117
1118                return matchingResourceMethodsByHttpMethod;
1119        }
1120
1121        @Nonnull
1122        protected MarshaledResponse provideFailsafeMarshaledResponse(@Nonnull Request request,
1123                                                                                                                                                                                                                                                         @Nonnull Throwable throwable) {
1124                requireNonNull(request);
1125                requireNonNull(throwable);
1126
1127                Integer statusCode = 500;
1128                Charset charset = StandardCharsets.UTF_8;
1129
1130                return MarshaledResponse.withStatusCode(statusCode)
1131                                .headers(Map.of("Content-Type", Set.of(format("text/plain; charset=%s", charset.name()))))
1132                                .body(format("HTTP %d: %s", statusCode, StatusCode.fromStatusCode(statusCode).get().getReasonPhrase()).getBytes(charset))
1133                                .build();
1134        }
1135
1136        /**
1137         * Synonym for {@link #stop()}.
1138         */
1139        @Override
1140        public void close() {
1141                stop();
1142        }
1143
1144        /**
1145         * Is either the managed {@link Server} or {@link ServerSentEventServer} started?
1146         *
1147         * @return {@code true} if at least one is started, {@code false} otherwise
1148         */
1149        @Nonnull
1150        public Boolean isStarted() {
1151                getLock().lock();
1152
1153                try {
1154                        if (getSokletConfig().getServer().isStarted())
1155                                return true;
1156
1157                        ServerSentEventServer serverSentEventServer = getSokletConfig().getServerSentEventServer().orElse(null);
1158                        return serverSentEventServer != null && serverSentEventServer.isStarted();
1159                } finally {
1160                        getLock().unlock();
1161                }
1162        }
1163
1164        /**
1165         * Runs Soklet with special non-network "simulator" implementations of {@link Server} and {@link ServerSentEventServer} - useful for integration testing.
1166         * <p>
1167         * See <a href="https://www.soklet.com/docs/testing">https://www.soklet.com/docs/testing</a> for how to write these tests.
1168         *
1169         * @param sokletConfig      configuration that drives the Soklet system
1170         * @param simulatorConsumer code to execute within the context of the simulator
1171         */
1172        public static void runSimulator(@Nonnull SokletConfig sokletConfig,
1173                                                                                                                                        @Nonnull Consumer<Simulator> simulatorConsumer) {
1174                requireNonNull(sokletConfig);
1175                requireNonNull(simulatorConsumer);
1176
1177                // Create Soklet instance - this initializes the REAL implementations through proxies
1178                Soklet soklet = Soklet.withConfig(sokletConfig);
1179
1180                // Extract proxies (they're guaranteed to be proxies now)
1181                ServerProxy serverProxy = (ServerProxy) sokletConfig.getServer();
1182                ServerSentEventServerProxy serverSentEventServerProxy = sokletConfig.getServerSentEventServer()
1183                                .map(s -> (ServerSentEventServerProxy) s)
1184                                .orElse(null);
1185
1186                // Create mock implementations
1187                MockServer mockServer = new MockServer();
1188                MockServerSentEventServer mockServerSentEventServer = new MockServerSentEventServer();
1189
1190                // Switch proxies to simulator mode
1191                serverProxy.enableSimulatorMode(mockServer);
1192
1193                if (serverSentEventServerProxy != null)
1194                        serverSentEventServerProxy.enableSimulatorMode(mockServerSentEventServer);
1195
1196                try {
1197                        // Initialize mocks with request handlers that delegate to Soklet's processing
1198                        mockServer.initialize(sokletConfig, (request, marshaledResponseConsumer) -> {
1199                                // Delegate to Soklet's internal request handling
1200                                soklet.handleRequest(request, ServerType.STANDARD_HTTP, marshaledResponseConsumer);
1201                        });
1202
1203                        if (mockServerSentEventServer != null)
1204                                mockServerSentEventServer.initialize(sokletConfig, (request, marshaledResponseConsumer) -> {
1205                                        // Delegate to Soklet's internal request handling for SSE
1206                                        soklet.handleRequest(request, ServerType.SERVER_SENT_EVENT, marshaledResponseConsumer);
1207                                });
1208
1209                        // Create and provide simulator
1210                        Simulator simulator = new DefaultSimulator(mockServer, mockServerSentEventServer);
1211                        simulatorConsumer.accept(simulator);
1212                } finally {
1213                        // Always restore to real implementations
1214                        serverProxy.disableSimulatorMode();
1215
1216                        if (serverSentEventServerProxy != null)
1217                                serverSentEventServerProxy.disableSimulatorMode();
1218                }
1219        }
1220
1221        @Nonnull
1222        protected SokletConfig getSokletConfig() {
1223                return this.sokletConfig;
1224        }
1225
1226        @Nonnull
1227        protected ReentrantLock getLock() {
1228                return this.lock;
1229        }
1230
1231        @Nonnull
1232        protected AtomicReference<CountDownLatch> getAwaitShutdownLatchReference() {
1233                return this.awaitShutdownLatchReference;
1234        }
1235
1236        @ThreadSafe
1237        static class DefaultSimulator implements Simulator {
1238                @Nullable
1239                private MockServer server;
1240                @Nullable
1241                private MockServerSentEventServer serverSentEventServer;
1242
1243                public DefaultSimulator(@Nonnull MockServer server,
1244                                                                                                                @Nullable MockServerSentEventServer serverSentEventServer) {
1245                        requireNonNull(server);
1246
1247                        this.server = server;
1248                        this.serverSentEventServer = serverSentEventServer;
1249                }
1250
1251                @Nonnull
1252                @Override
1253                public RequestResult performRequest(@Nonnull Request request) {
1254                        AtomicReference<RequestResult> requestResultHolder = new AtomicReference<>();
1255                        Server.RequestHandler requestHandler = getServer().getRequestHandler().orElse(null);
1256
1257                        if (requestHandler == null)
1258                                throw new IllegalStateException("You must register a request handler prior to simulating requests");
1259
1260                        requestHandler.handleRequest(request, (requestResult -> {
1261                                requestResultHolder.set(requestResult);
1262                        }));
1263
1264                        return requestResultHolder.get();
1265                }
1266
1267                @Nonnull
1268                @Override
1269                public ServerSentEventRequestResult performServerSentEventRequest(@Nonnull Request request) {
1270                        MockServerSentEventServer serverSentEventServer = getServerSentEventServer().orElse(null);
1271
1272                        if (serverSentEventServer == null)
1273                                throw new IllegalStateException(format("You must specify a %s in your %s to simulate Server-Sent Event requests",
1274                                                ServerSentEventServer.class.getSimpleName(), SokletConfig.class.getSimpleName()));
1275
1276                        AtomicReference<RequestResult> requestResultHolder = new AtomicReference<>();
1277                        ServerSentEventServer.RequestHandler requestHandler = serverSentEventServer.getRequestHandler().orElse(null);
1278
1279                        if (requestHandler == null)
1280                                throw new IllegalStateException("You must register a request handler prior to simulating SSE Event Source requests");
1281
1282                        requestHandler.handleRequest(request, (requestResult -> {
1283                                requestResultHolder.set(requestResult);
1284                        }));
1285
1286                        RequestResult requestResult = requestResultHolder.get();
1287                        HandshakeResult handshakeResult = requestResult.getHandshakeResult().orElse(null);
1288
1289                        if (handshakeResult == null)
1290                                return new ServerSentEventRequestResult.RequestFailed(requestResult);
1291
1292                        if (handshakeResult instanceof HandshakeResult.Accepted acceptedHandshake) {
1293                                Consumer<ServerSentEventUnicaster> clientInitializer = acceptedHandshake.getClientInitializer().orElse(null);
1294
1295                                // Create a synthetic logical response using values from the accepted handshake
1296                                if (requestResult.getResponse().isEmpty())
1297                                        requestResult = requestResult.copy()
1298                                                        .response(Response.withStatusCode(200)
1299                                                                        .headers(acceptedHandshake.getHeaders())
1300                                                                        .cookies(acceptedHandshake.getCookies())
1301                                                                        .build())
1302                                                        .finish();
1303
1304                                HandshakeAccepted handshakeAccepted = new HandshakeAccepted(acceptedHandshake, request.getResourcePath(), requestResult, this, clientInitializer);
1305                                return handshakeAccepted;
1306                        }
1307
1308                        if (handshakeResult instanceof HandshakeResult.Rejected rejectedHandshake)
1309                                return new HandshakeRejected(rejectedHandshake, requestResult);
1310
1311                        throw new IllegalStateException(format("Encountered unexpected %s: %s", HandshakeResult.class.getSimpleName(), handshakeResult));
1312                }
1313
1314                @Nonnull
1315                MockServer getServer() {
1316                        return this.server;
1317                }
1318
1319                @Nonnull
1320                Optional<MockServerSentEventServer> getServerSentEventServer() {
1321                        return Optional.ofNullable(this.serverSentEventServer);
1322                }
1323        }
1324
1325        /**
1326         * Mock server that doesn't touch the network at all, useful for testing.
1327         *
1328         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1329         */
1330        @ThreadSafe
1331        static class MockServer implements Server {
1332                @Nullable
1333                private SokletConfig sokletConfig;
1334                @Nullable
1335                private Server.RequestHandler requestHandler;
1336
1337                @Override
1338                public void start() {
1339                        // No-op
1340                }
1341
1342                @Override
1343                public void stop() {
1344                        // No-op
1345                }
1346
1347                @Nonnull
1348                @Override
1349                public Boolean isStarted() {
1350                        return true;
1351                }
1352
1353                @Override
1354                public void initialize(@Nonnull SokletConfig sokletConfig,
1355                                                                                                         @Nonnull RequestHandler requestHandler) {
1356                        requireNonNull(sokletConfig);
1357                        requireNonNull(requestHandler);
1358
1359                        this.requestHandler = requestHandler;
1360                }
1361
1362                @Nonnull
1363                protected Optional<SokletConfig> getSokletConfig() {
1364                        return Optional.ofNullable(this.sokletConfig);
1365                }
1366
1367                @Nonnull
1368                protected Optional<RequestHandler> getRequestHandler() {
1369                        return Optional.ofNullable(this.requestHandler);
1370                }
1371        }
1372
1373        /**
1374         * Mock Server-Sent Event unicaster that doesn't touch the network at all, useful for testing.
1375         */
1376        @ThreadSafe
1377        static class MockServerSentEventUnicaster implements ServerSentEventUnicaster {
1378                @Nonnull
1379                private final ResourcePath resourcePath;
1380                @Nonnull
1381                private final Consumer<ServerSentEvent> eventConsumer;
1382                @Nonnull
1383                private final Consumer<String> commentConsumer;
1384
1385                public MockServerSentEventUnicaster(@Nonnull ResourcePath resourcePath,
1386                                                                                                                                                                @Nonnull Consumer<ServerSentEvent> eventConsumer,
1387                                                                                                                                                                @Nonnull Consumer<String> commentConsumer) {
1388                        requireNonNull(resourcePath);
1389                        requireNonNull(eventConsumer);
1390                        requireNonNull(commentConsumer);
1391
1392                        this.resourcePath = resourcePath;
1393                        this.eventConsumer = eventConsumer;
1394                        this.commentConsumer = commentConsumer;
1395                }
1396
1397                @Override
1398                public void unicastEvent(@Nonnull ServerSentEvent serverSentEvent) {
1399                        requireNonNull(serverSentEvent);
1400                        getEventConsumer().accept(serverSentEvent);
1401                }
1402
1403                @Override
1404                public void unicastComment(@Nonnull String comment) {
1405                        requireNonNull(comment);
1406                        getCommentConsumer().accept(comment);
1407                }
1408
1409                @Nonnull
1410                @Override
1411                public ResourcePath getResourcePath() {
1412                        return this.resourcePath;
1413                }
1414
1415                @Nonnull
1416                protected Consumer<ServerSentEvent> getEventConsumer() {
1417                        return this.eventConsumer;
1418                }
1419
1420                @Nonnull
1421                protected Consumer<String> getCommentConsumer() {
1422                        return this.commentConsumer;
1423                }
1424        }
1425
1426        /**
1427         * Mock Server-Sent Event broadcaster that doesn't touch the network at all, useful for testing.
1428         */
1429        @ThreadSafe
1430        static class MockServerSentEventBroadcaster implements ServerSentEventBroadcaster {
1431                @Nonnull
1432                private final ResourcePath resourcePath;
1433                @Nonnull
1434                private final Set<Consumer<ServerSentEvent>> eventConsumers;
1435                @Nonnull
1436                private final Set<Consumer<String>> commentConsumers;
1437
1438                public MockServerSentEventBroadcaster(@Nonnull ResourcePath resourcePath) {
1439                        requireNonNull(resourcePath);
1440
1441                        this.resourcePath = resourcePath;
1442                        this.eventConsumers = ConcurrentHashMap.newKeySet();
1443                        this.commentConsumers = ConcurrentHashMap.newKeySet();
1444                }
1445
1446                @Nonnull
1447                @Override
1448                public ResourcePath getResourcePath() {
1449                        return this.resourcePath;
1450                }
1451
1452                @Nonnull
1453                @Override
1454                public Long getClientCount() {
1455                        return Long.valueOf(getEventConsumers().size() + getCommentConsumers().size());
1456                }
1457
1458                @Override
1459                public void broadcastEvent(@Nonnull ServerSentEvent serverSentEvent) {
1460                        requireNonNull(serverSentEvent);
1461
1462                        for (Consumer<ServerSentEvent> eventConsumer : getEventConsumers()) {
1463                                try {
1464                                        eventConsumer.accept(serverSentEvent);
1465                                } catch (Throwable throwable) {
1466                                        // TODO: revisit this - should we communicate back exceptions, and should we fire these on separate threads for "realism" (probably not)?
1467                                        // TODO: should probably tie this and other mock SSE types into LifecycleInterceptor for parity once we fully implement metric tracking
1468                                        throwable.printStackTrace();
1469                                }
1470                        }
1471                }
1472
1473                @Override
1474                public void broadcastComment(@Nonnull String comment) {
1475                        requireNonNull(comment);
1476
1477                        for (Consumer<String> commentConsumer : getCommentConsumers()) {
1478                                try {
1479                                        commentConsumer.accept(comment);
1480                                } catch (Throwable throwable) {
1481                                        // TODO: revisit this - should we communicate back exceptions, and should we fire these on separate threads for "realism" (probably not)?
1482                                        throwable.printStackTrace();
1483                                }
1484                        }
1485                }
1486
1487                @Nonnull
1488                public Boolean registerEventConsumer(@Nonnull Consumer<ServerSentEvent> eventConsumer) {
1489                        requireNonNull(eventConsumer);
1490                        return getEventConsumers().add(eventConsumer);
1491                }
1492
1493                @Nonnull
1494                public Boolean unregisterEventConsumer(@Nonnull Consumer<ServerSentEvent> eventConsumer) {
1495                        requireNonNull(eventConsumer);
1496                        return getEventConsumers().remove(eventConsumer);
1497                }
1498
1499                @Nonnull
1500                public Boolean registerCommentConsumer(@Nonnull Consumer<String> commentConsumer) {
1501                        requireNonNull(commentConsumer);
1502                        return getCommentConsumers().add(commentConsumer);
1503                }
1504
1505                @Nonnull
1506                public Boolean unregisterCommentConsumer(@Nonnull Consumer<String> commentConsumer) {
1507                        requireNonNull(commentConsumer);
1508                        return getCommentConsumers().remove(commentConsumer);
1509                }
1510
1511                @Nonnull
1512                protected Set<Consumer<ServerSentEvent>> getEventConsumers() {
1513                        return this.eventConsumers;
1514                }
1515
1516                @Nonnull
1517                protected Set<Consumer<String>> getCommentConsumers() {
1518                        return this.commentConsumers;
1519                }
1520        }
1521
1522        /**
1523         * Mock Server-Sent Event server that doesn't touch the network at all, useful for testing.
1524         *
1525         * @author <a href="https://www.revetkn.com">Mark Allen</a>
1526         */
1527        @ThreadSafe
1528        static class MockServerSentEventServer implements ServerSentEventServer {
1529                @Nullable
1530                private SokletConfig sokletConfig;
1531                @Nullable
1532                private ServerSentEventServer.RequestHandler requestHandler;
1533                @Nonnull
1534                private final ConcurrentHashMap<ResourcePath, MockServerSentEventBroadcaster> broadcastersByResourcePath;
1535
1536                public MockServerSentEventServer() {
1537                        this.broadcastersByResourcePath = new ConcurrentHashMap<>();
1538                }
1539
1540                @Override
1541                public void start() {
1542                        // No-op
1543                }
1544
1545                @Override
1546                public void stop() {
1547                        // No-op
1548                }
1549
1550                @Nonnull
1551                @Override
1552                public Boolean isStarted() {
1553                        return true;
1554                }
1555
1556                @Nonnull
1557                @Override
1558                public Optional<? extends ServerSentEventBroadcaster> acquireBroadcaster(@Nullable ResourcePath resourcePath) {
1559                        if (resourcePath == null)
1560                                return Optional.empty();
1561
1562                        MockServerSentEventBroadcaster broadcaster = getBroadcastersByResourcePath()
1563                                        .computeIfAbsent(resourcePath, rp -> new MockServerSentEventBroadcaster(rp));
1564
1565                        return Optional.of(broadcaster);
1566                }
1567
1568                public void registerEventConsumer(@Nonnull ResourcePath resourcePath,
1569                                                                                                                                                        @Nonnull Consumer<ServerSentEvent> eventConsumer) {
1570                        requireNonNull(resourcePath);
1571                        requireNonNull(eventConsumer);
1572
1573                        MockServerSentEventBroadcaster broadcaster = getBroadcastersByResourcePath()
1574                                        .computeIfAbsent(resourcePath, rp -> new MockServerSentEventBroadcaster(rp));
1575
1576                        broadcaster.registerEventConsumer(eventConsumer);
1577                }
1578
1579                @Nonnull
1580                public Boolean unregisterEventConsumer(@Nonnull ResourcePath resourcePath,
1581                                                                                                                                                                         @Nonnull Consumer<ServerSentEvent> eventConsumer) {
1582                        requireNonNull(resourcePath);
1583                        requireNonNull(eventConsumer);
1584
1585                        MockServerSentEventBroadcaster broadcaster = getBroadcastersByResourcePath().get(resourcePath);
1586
1587                        if (broadcaster == null)
1588                                return false;
1589
1590                        return broadcaster.unregisterEventConsumer(eventConsumer);
1591                }
1592
1593                public void registerCommentConsumer(@Nonnull ResourcePath resourcePath,
1594                                                                                                                                                                @Nonnull Consumer<String> commentConsumer) {
1595                        requireNonNull(resourcePath);
1596                        requireNonNull(commentConsumer);
1597
1598                        MockServerSentEventBroadcaster broadcaster = getBroadcastersByResourcePath()
1599                                        .computeIfAbsent(resourcePath, rp -> new MockServerSentEventBroadcaster(rp));
1600
1601                        broadcaster.registerCommentConsumer(commentConsumer);
1602                }
1603
1604                @Nonnull
1605                public Boolean unregisterCommentConsumer(@Nonnull ResourcePath resourcePath,
1606                                                                                                                                                                                 @Nonnull Consumer<String> commentConsumer) {
1607                        requireNonNull(resourcePath);
1608                        requireNonNull(commentConsumer);
1609
1610                        MockServerSentEventBroadcaster broadcaster = getBroadcastersByResourcePath().get(resourcePath);
1611
1612                        if (broadcaster == null)
1613                                return false;
1614
1615                        return broadcaster.unregisterCommentConsumer(commentConsumer);
1616                }
1617
1618                @Override
1619                public void initialize(@Nonnull SokletConfig sokletConfig,
1620                                                                                                         @Nonnull ServerSentEventServer.RequestHandler requestHandler) {
1621                        requireNonNull(sokletConfig);
1622                        requireNonNull(requestHandler);
1623
1624                        this.sokletConfig = sokletConfig;
1625                        this.requestHandler = requestHandler;
1626                }
1627
1628                @Nullable
1629                protected Optional<SokletConfig> getSokletConfig() {
1630                        return Optional.ofNullable(this.sokletConfig);
1631                }
1632
1633                @Nullable
1634                protected Optional<ServerSentEventServer.RequestHandler> getRequestHandler() {
1635                        return Optional.ofNullable(this.requestHandler);
1636                }
1637
1638                @Nonnull
1639                protected ConcurrentHashMap<ResourcePath, MockServerSentEventBroadcaster> getBroadcastersByResourcePath() {
1640                        return this.broadcastersByResourcePath;
1641                }
1642        }
1643
1644        /**
1645         * Efficiently provides the current time in RFC 1123 HTTP-date format.
1646         * Updates once per second to avoid the overhead of formatting dates on every request.
1647         */
1648        @ThreadSafe
1649        private static final class CachedHttpDate {
1650                @Nonnull
1651                private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z", Locale.ENGLISH)
1652                                .withZone(ZoneId.of("GMT"));
1653                @Nonnull
1654                private static volatile String CURRENT_VALUE = FORMATTER.format(Instant.now());
1655
1656                static {
1657                        Thread t = new Thread(() -> {
1658                                while (true) {
1659                                        try {
1660                                                Thread.sleep(1000);
1661                                                CURRENT_VALUE = FORMATTER.format(Instant.now());
1662                                        } catch (InterruptedException e) {
1663                                                break; // Allow thread to die on JVM shutdown
1664                                        }
1665                                }
1666                        }, "soklet-date-header-value-updater");
1667                        t.setDaemon(true);
1668                        t.start();
1669                }
1670
1671                @Nonnull
1672                static String getCurrentValue() {
1673                        return CURRENT_VALUE;
1674                }
1675        }
1676}