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