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}