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 javax.annotation.Nonnull;
020import javax.annotation.Nullable;
021import java.io.PrintWriter;
022import java.io.StringWriter;
023import java.time.Duration;
024import java.util.List;
025import java.util.function.Consumer;
026import java.util.function.Function;
027
028import static java.util.Objects.requireNonNull;
029
030/**
031 * "Hook" methods for customizing behavior in response to system lifecycle events -
032 * server started, request received, response written, and so on.
033 * <p>
034 * The ability to modify request processing control flow is provided via {@link #wrapRequest(Request, ResourceMethod, Consumer)}
035 * and {@link #interceptRequest(Request, ResourceMethod, Function, Consumer)}.
036 * <p>
037 * Note: some of these methods are "fail-fast" - exceptions thrown will bubble out and stop execution - and for others, Soklet will
038 * catch exceptions and surface separately via {@link #didReceiveLogEvent(LogEvent)}.  Generally speaking, lifecycle events that are scoped
039 * at the server level (e.g. {@link #willStartServer(Server)}) will fail-fast and events that are scoped at the request level
040 * (e.g. {@link #didStartRequestHandling(Request, ResourceMethod)}) will not fail-fast.
041 * <p>
042 * A standard implementation can be acquired via the {@link #withDefaults()} factory method.
043 * <p>
044 * Full documentation is available at <a href="https://www.soklet.com/docs/request-lifecycle">https://www.soklet.com/docs/request-lifecycle</a>.
045 *
046 * @author <a href="https://www.revetkn.com">Mark Allen</a>
047 */
048public interface LifecycleInterceptor {
049        /**
050         * Called before the server starts.
051         * <p>
052         * This method <strong>is</strong> fail-fast. If an exception occurs when Soklet invokes this method, it will halt execution and bubble out for your application code to handle.
053         *
054         * @param server the server that will start
055         */
056        default void willStartServer(@Nonnull Server server) {
057                // No-op by default
058        }
059
060        /**
061         * Called after the server starts.
062         * <p>
063         * This method <strong>is</strong> fail-fast. If an exception occurs when Soklet invokes this method, it will halt execution and bubble out for your application code to handle.
064         *
065         * @param server the server that started
066         */
067        default void didStartServer(@Nonnull Server server) {
068                // No-op by default
069        }
070
071        /**
072         * Called before the server stops.
073         * <p>
074         * This method <strong>is</strong> fail-fast. If an exception occurs when Soklet invokes this method, it will halt execution and bubble out for your application code to handle.
075         *
076         * @param server the server that will stop
077         */
078        default void willStopServer(@Nonnull Server server) {
079                // No-op by default
080        }
081
082        /**
083         * Called after the server stops.
084         * <p>
085         * This method <strong>is</strong> fail-fast. If an exception occurs when Soklet invokes this method, it will halt execution and bubble out for your application code to handle.
086         *
087         * @param server the server that stopped
088         */
089        default void didStopServer(@Nonnull Server server) {
090                // No-op by default
091        }
092
093        /**
094         * Called as soon as a request is received and a <em>Resource Method</em> has been resolved to handle it.
095         * <p>
096         * This method <strong>is not</strong> fail-fast. If an exception occurs when Soklet invokes this method, Soklet will catch it and invoke {@link #didReceiveLogEvent(LogEvent)} with type {@link LogEventType#LIFECYCLE_INTERCEPTOR_DID_START_REQUEST_HANDLING_FAILED}.
097         *
098         * @param request        the request that was received
099         * @param resourceMethod the <em>Resource Method</em> that will handle the request
100         *                       May be {@code null} if no <em>Resource Method</em> was resolved, e.g. a 404
101         */
102        default void didStartRequestHandling(@Nonnull Request request,
103                                                                                                                                                         @Nullable ResourceMethod resourceMethod) {
104                // No-op by default
105        }
106
107        /**
108         * Called after a request has fully completed processing and a response has been sent to the client.
109         * <p>
110         * This method <strong>is not</strong> fail-fast. If an exception occurs when Soklet invokes this method, Soklet will catch it and invoke {@link #didReceiveLogEvent(LogEvent)} with type {@link LogEventType#LIFECYCLE_INTERCEPTOR_DID_FINISH_REQUEST_HANDLING_FAILED}.
111         *
112         * @param request            the request that was received
113         * @param resourceMethod     the <em>Resource Method</em> that will handle the request
114         *                           May be {@code null} if no <em>Resource Method</em> was resolved, e.g. a 404
115         * @param marshaledResponse  the response that was sent to the client
116         * @param processingDuration how long it took to process the whole request, including time to send the response to the client
117         * @param throwables         exceptions that occurred during request handling
118         */
119        default void didFinishRequestHandling(@Nonnull Request request,
120                                                                                                                                                                @Nullable ResourceMethod resourceMethod,
121                                                                                                                                                                @Nonnull MarshaledResponse marshaledResponse,
122                                                                                                                                                                @Nonnull Duration processingDuration,
123                                                                                                                                                                @Nonnull List<Throwable> throwables) {
124                // No-op by default
125        }
126
127        /**
128         * Called before the response is sent to the client.
129         * <p>
130         * This method <strong>is not</strong> fail-fast. If an exception occurs when Soklet invokes this method, Soklet will catch it and invoke {@link #didReceiveLogEvent(LogEvent)} with type {@link LogEventType#LIFECYCLE_INTERCEPTOR_WILL_START_RESPONSE_WRITING_FAILED}.
131         *
132         * @param request           the request that was received
133         * @param resourceMethod    the <em>Resource Method</em> that handled the request.
134         *                          May be {@code null} if no <em>Resource Method</em> was resolved, e.g. a 404
135         * @param marshaledResponse the response to send to the client
136         */
137        default void willStartResponseWriting(@Nonnull Request request,
138                                                                                                                                                                @Nullable ResourceMethod resourceMethod,
139                                                                                                                                                                @Nonnull MarshaledResponse marshaledResponse) {
140                // No-op by default
141        }
142
143        /**
144         * Called after the response is sent to the client.
145         * <p>
146         * This method <strong>is not</strong> fail-fast. If an exception occurs when Soklet invokes this method, Soklet will catch it and invoke {@link #didReceiveLogEvent(LogEvent)} with type {@link LogEventType#LIFECYCLE_INTERCEPTOR_DID_FINISH_RESPONSE_WRITING_FAILED}.
147         *
148         * @param request               the request that was received
149         * @param resourceMethod        the <em>Resource Method</em> that handled the request.
150         *                              May be {@code null} if no <em>Resource Method</em> was resolved, e.g. a 404
151         * @param marshaledResponse     the response that was sent to the client
152         * @param responseWriteDuration how long it took to send the response to the client
153         * @param throwable             the exception thrown during response writing (if any)
154         */
155        default void didFinishResponseWriting(@Nonnull Request request,
156                                                                                                                                                                @Nullable ResourceMethod resourceMethod,
157                                                                                                                                                                @Nonnull MarshaledResponse marshaledResponse,
158                                                                                                                                                                @Nonnull Duration responseWriteDuration,
159                                                                                                                                                                @Nullable Throwable throwable) {
160                // No-op by default
161        }
162
163        /**
164         * Called when an event suitable for logging occurs during processing (generally, an exception).
165         * <p>
166         * This method <strong>is not</strong> fail-fast. If an exception occurs when Soklet invokes this method, Soklet will catch the exception and print its stack trace to stderr.
167         *
168         * @param logEvent the event that occurred
169         */
170        default void didReceiveLogEvent(@Nonnull LogEvent logEvent) {
171                requireNonNull(logEvent);
172
173                Throwable throwable = logEvent.getThrowable().orElse(null);
174                String message = logEvent.getMessage();
175
176                if (throwable == null) {
177                        System.err.println(message);
178                } else {
179                        StringWriter stringWriter = new StringWriter();
180                        PrintWriter printWriter = new PrintWriter(stringWriter);
181                        throwable.printStackTrace(printWriter);
182
183                        String throwableWithStackTrace = stringWriter.toString().trim();
184                        System.err.printf("%s\n%s\n", message, throwableWithStackTrace);
185                }
186        }
187
188        /**
189         * Supports alteration of the request processing flow by enabling programmatic control over its two key phases: acquiring a response and writing the response to the client.
190         * <p>
191         * This is a more fine-grained approach than {@link #wrapRequest(Request, ResourceMethod, Consumer)}.
192         * <pre> // Default implementation: first, acquire a response for the given request.
193         * MarshaledResponse marshaledResponse = responseProducer.apply(request);
194         *
195         * // Second, send the response over the wire.
196         * responseWriter.accept(marshaledResponse);</pre>
197         * <p>
198         * This method <strong>is not</strong> fail-fast. If an exception occurs when Soklet invokes this method, Soklet will catch it and invoke {@link #didReceiveLogEvent(LogEvent)} with type {@link LogEventType#LIFECYCLE_INTERCEPTOR_INTERCEPT_REQUEST_FAILED}.
199         * <p>
200         * See <a href="https://www.soklet.com/docs/request-lifecycle#request-intercepting">https://www.soklet.com/docs/request-lifecycle#request-intercepting</a> for detailed documentation.
201         *
202         * @param request          the request that was received
203         * @param resourceMethod   the <em>Resource Method</em> that will handle the request
204         *                         May be {@code null} if no <em>Resource Method</em> was resolved, e.g. a 404
205         * @param responseProducer function that accepts the request as input and provides a response as output (usually by invoking the <em>Resource Method</em>)
206         * @param responseWriter   function that accepts a response as input and writes the response to the client
207         */
208        default void interceptRequest(@Nonnull Request request,
209                                                                                                                                @Nullable ResourceMethod resourceMethod,
210                                                                                                                                @Nonnull Function<Request, MarshaledResponse> responseProducer,
211                                                                                                                                @Nonnull Consumer<MarshaledResponse> responseWriter) {
212                requireNonNull(request);
213                requireNonNull(responseProducer);
214                requireNonNull(responseWriter);
215
216                MarshaledResponse marshaledResponse = responseProducer.apply(request);
217                responseWriter.accept(marshaledResponse);
218        }
219
220        /**
221         * Wraps around the whole "outside" of the entire request-handling flow.
222         * <p>
223         * The "inside" of the flow is everything from <em>Resource Method</em> execution to writing response bytes to the client.
224         * <p>
225         * This is a more coarse-grained approach than {@link #interceptRequest(Request, ResourceMethod, Function, Consumer)}.
226         * <pre> // Default implementation: let the request processing proceed as normal
227         * requestProcessor.accept(request);</pre>
228         * <p>
229         * This method <strong>is not</strong> fail-fast. If an exception occurs when Soklet invokes this method, Soklet will catch it and invoke {@link #didReceiveLogEvent(LogEvent)} with type {@link LogEventType#LIFECYCLE_INTERCEPTOR_WRAP_REQUEST_FAILED}.
230         * <p>
231         * See <a href="https://www.soklet.com/docs/request-lifecycle#request-wrapping">https://www.soklet.com/docs/request-lifecycle#request-wrapping</a> for detailed documentation.
232         *
233         * @param request          the request that was received
234         * @param resourceMethod   the <em>Resource Method</em> that will handle the request
235         *                         May be {@code null} if no <em>Resource Method</em> was resolved, e.g. a 404
236         * @param requestProcessor function that takes the request as input and performs all downstream processing
237         */
238        default void wrapRequest(@Nonnull Request request,
239                                                                                                         @Nullable ResourceMethod resourceMethod,
240                                                                                                         @Nonnull Consumer<Request> requestProcessor) {
241                requireNonNull(request);
242                requireNonNull(requestProcessor);
243
244                requestProcessor.accept(request);
245        }
246
247        /**
248         * Called before the Server-Sent Event server starts.
249         * <p>
250         * This method <strong>is</strong> fail-fast. If an exception occurs when Soklet invokes this method, it will halt execution and bubble out for your application code to handle.
251         *
252         * @param serverSentEventServer the Server-Sent Event server that will start
253         */
254        default void willStartServerSentEventServer(@Nonnull ServerSentEventServer serverSentEventServer) {
255                // No-op by default
256        }
257
258        /**
259         * Called after the Server-Sent Event server starts.
260         * <p>
261         * This method <strong>is</strong> fail-fast. If an exception occurs when Soklet invokes this method, it will halt execution and bubble out for your application code to handle.
262         *
263         * @param serverSentEventServer the Server-Sent Event server that started
264         */
265        default void didStartServerSentEventServer(@Nonnull ServerSentEventServer serverSentEventServer) {
266                // No-op by default
267        }
268
269        /**
270         * Called before the Server-Sent Event server stops.
271         * <p>
272         * This method <strong>is</strong> fail-fast. If an exception occurs when Soklet invokes this method, it will halt execution and bubble out for your application code to handle.
273         *
274         * @param serverSentEventServer the Server-Sent Event server that will stop
275         */
276        default void willStopServerSentEventServer(@Nonnull ServerSentEventServer serverSentEventServer) {
277                // No-op by default
278        }
279
280        /**
281         * Called after the Server-Sent Event server stops.
282         * <p>
283         * This method <strong>is</strong> fail-fast. If an exception occurs when Soklet invokes this method, it will halt execution and bubble out for your application code to handle.
284         *
285         * @param serverSentEventServer the Server-Sent Event server that stopped
286         */
287        default void didStopServerSentEventServer(@Nonnull ServerSentEventServer serverSentEventServer) {
288                // No-op by default
289        }
290
291        /**
292         * Called immediately before a Server-Sent Event connection of indefinite duration to the client is opened.
293         * <p>
294         * This occurs after the initial "handshake" Server-Sent Event request has successfully completed (that is, an HTTP 200 response).
295         * <p>
296         * This method <strong>is not</strong> fail-fast. If an exception occurs when Soklet invokes this method, Soklet will catch it and invoke {@link #didReceiveLogEvent(LogEvent)} with type {@link LogEventType#LIFECYCLE_INTERCEPTOR_WILL_ESTABLISH_SERVER_SENT_EVENT_CONNECTION_FAILED}.
297         *
298         * @param request        the initial "handshake" Server-Sent Event request that was received
299         * @param resourceMethod the <em>Resource Method</em> that handled the "handshake"
300         */
301        default void willEstablishServerSentEventConnection(@Nonnull Request request,
302                                                                                                                                                                                                                        @Nonnull ResourceMethod resourceMethod) {
303                // No-op by default
304        }
305
306        /**
307         * Called immediately after a Server-Sent Event connection of indefinite duration to the client is opened.
308         * <p>
309         * This occurs after the initial "handshake" Server-Sent Event request has successfully completed (that is, an HTTP 200 response).
310         * <p>
311         * This method <strong>is not</strong> fail-fast. If an exception occurs when Soklet invokes this method, Soklet will catch it and invoke {@link #didReceiveLogEvent(LogEvent)} with type {@link LogEventType#LIFECYCLE_INTERCEPTOR_DID_ESTABLISH_SERVER_SENT_EVENT_CONNECTION_FAILED}.
312         *
313         * @param request        the initial "handshake" Server-Sent Event request that was received
314         * @param resourceMethod the <em>Resource Method</em> that handled the "handshake"
315         */
316        default void didEstablishServerSentEventConnection(@Nonnull Request request,
317                                                                                                                                                                                                                 @Nonnull ResourceMethod resourceMethod) {
318                // No-op by default
319        }
320
321        /**
322         * Called immediately before a Server-Sent Event connection to the client is terminated.
323         * <p>
324         * This method <strong>is not</strong> fail-fast. If an exception occurs when Soklet invokes this method, Soklet will catch it and invoke {@link #didReceiveLogEvent(LogEvent)} with type {@link LogEventType#LIFECYCLE_INTERCEPTOR_WILL_TERMINATE_SERVER_SENT_EVENT_CONNECTION_FAILED}.
325         *
326         * @param request        the initial "handshake" Server-Sent Event request that was received
327         * @param resourceMethod the <em>Resource Method</em> that handled the "handshake"
328         * @param throwable      the exception thrown which caused the connection to terminate (if any)
329         */
330        default void willTerminateServerSentEventConnection(@Nonnull Request request,
331                                                                                                                                                                                                                        @Nonnull ResourceMethod resourceMethod,
332                                                                                                                                                                                                                        @Nullable Throwable throwable) {
333                // No-op by default
334        }
335
336        /**
337         * Called immediately after a Server-Sent Event connection to the client is terminated.
338         * <p>
339         * This method <strong>is not</strong> fail-fast. If an exception occurs when Soklet invokes this method, Soklet will catch it and invoke {@link #didReceiveLogEvent(LogEvent)} with type {@link LogEventType#LIFECYCLE_INTERCEPTOR_DID_TERMINATE_SERVER_SENT_EVENT_CONNECTION_FAILED}.
340         *
341         * @param request            the initial "handshake" Server-Sent Event request that was received
342         * @param resourceMethod     the <em>Resource Method</em> that handled the "handshake"
343         * @param connectionDuration how long the connection was open for
344         * @param throwable          the exception thrown which caused the connection to terminate (if any)
345         */
346        default void didTerminateServerSentEventConnection(@Nonnull Request request,
347                                                                                                                                                                                                                 @Nonnull ResourceMethod resourceMethod,
348                                                                                                                                                                                                                 @Nonnull Duration connectionDuration,
349                                                                                                                                                                                                                 @Nullable Throwable throwable) {
350                // No-op by default
351        }
352
353        /**
354         * Called before a Server-Sent Event is sent to the client.
355         * <p>
356         * This method <strong>is not</strong> fail-fast. If an exception occurs when Soklet invokes this method, Soklet will catch it and invoke {@link #didReceiveLogEvent(LogEvent)} with type {@link LogEventType#LIFECYCLE_INTERCEPTOR_WILL_START_SERVER_SENT_EVENT_WRITING_FAILED}.
357         *
358         * @param request         the initial "handshake" Server-Sent Event request that was received
359         * @param resourceMethod  the <em>Resource Method</em> that handled the "handshake"
360         * @param serverSentEvent the Server-Sent Event to send to the client
361         */
362        default void willStartServerSentEventWriting(@Nonnull Request request,
363                                                                                                                                                                                         @Nonnull ResourceMethod resourceMethod,
364                                                                                                                                                                                         @Nonnull ServerSentEvent serverSentEvent) {
365                // No-op by default
366        }
367
368        /**
369         * Called after a Server-Sent Event is sent to the client.
370         * <p>
371         * This method <strong>is not</strong> fail-fast. If an exception occurs when Soklet invokes this method, Soklet will catch it and invoke {@link #didReceiveLogEvent(LogEvent)} with type {@link LogEventType#LIFECYCLE_INTERCEPTOR_DID_FINISH_SERVER_SENT_EVENT_WRITING_FAILED}.
372         *
373         * @param request         the initial "handshake" Server-Sent Event request that was received
374         * @param resourceMethod  the <em>Resource Method</em> that handled the "handshake"
375         * @param serverSentEvent the Server-Sent Event that was sent to the client
376         * @param writeDuration   how long it took to send the Server-Sent Event to the client
377         * @param throwable       the exception thrown during Server-Sent Event writing (if any)
378         */
379        default void didFinishServerSentEventWriting(@Nonnull Request request,
380                                                                                                                                                                                         @Nonnull ResourceMethod resourceMethod,
381                                                                                                                                                                                         @Nonnull ServerSentEvent serverSentEvent,
382                                                                                                                                                                                         @Nonnull Duration writeDuration,
383                                                                                                                                                                                         @Nullable Throwable throwable) {
384                // No-op by default
385        }
386
387        /**
388         * Acquires a {@link LifecycleInterceptor} instance with sensible defaults.
389         * <p>
390         * Callers should not rely on reference identity; this method may return a new or cached instance.
391         *
392         * @return a {@code LifecycleInterceptor} with default settings
393         */
394        @Nonnull
395        static LifecycleInterceptor withDefaults() {
396                return DefaultLifecycleInterceptor.defaultInstance();
397        }
398}