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}