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 javax.annotation.concurrent.NotThreadSafe; 022import java.nio.charset.Charset; 023import java.util.Set; 024 025import static java.util.Objects.requireNonNull; 026 027/** 028 * Prepares responses for each request scenario Soklet supports (happy path, exception, CORS preflight, etc.) 029 * <p> 030 * The {@link MarshaledResponse} value returned from these methods is what is ultimately sent back to 031 * clients as bytes over the wire. 032 * <p> 033 * Standard implementations can be acquired via these factory methods: 034 * <ul> 035 * <li>{@link #withDefaults()}</li> 036 * <li>{@link #withCharset(Charset)}</li> 037 * </ul> 038 * <p> 039 * Full documentation is available at <a href="https://www.soklet.com/docs/response-writing">https://www.soklet.com/docs/response-writing</a>. 040 * 041 * @author <a href="https://www.revetkn.com">Mark Allen</a> 042 */ 043public interface ResponseMarshaler { 044 /** 045 * Prepares a "happy path" response - the request was matched to a <em>Resource Method</em> and executed non-exceptionally. 046 * <p> 047 * Detailed documentation is available at <a href="https://www.soklet.com/docs/response-writing#happy-path">https://www.soklet.com/docs/response-writing#happy-path</a>. 048 * 049 * @param request the HTTP request 050 * @param response the response provided by the <em>Resource Method</em> that handled the request 051 * @param resourceMethod the <em>Resource Method</em> that handled the request 052 * @return the response to be sent over the wire 053 */ 054 @Nonnull 055 MarshaledResponse forHappyPath(@Nonnull Request request, 056 @Nonnull Response response, 057 @Nonnull ResourceMethod resourceMethod); 058 059 /** 060 * Prepares a response for a request that triggers an 061 * <a href="https://httpwg.org/specs/rfc9110.html#status.404">HTTP 404 Not Found</a>. 062 * <p> 063 * Detailed documentation is available at <a href="https://www.soklet.com/docs/response-writing#404-not-found">https://www.soklet.com/docs/response-writing#404-not-found</a>. 064 * 065 * @param request the HTTP request 066 * @return the response to be sent over the wire 067 */ 068 @Nonnull 069 MarshaledResponse forNotFound(@Nonnull Request request); 070 071 /** 072 * Prepares a response for a request that triggers an 073 * <a href="https://httpwg.org/specs/rfc9110.html#status.405">HTTP 405 Method Not Allowed</a>. 074 * <p> 075 * Detailed documentation is available at <a href="https://www.soklet.com/docs/response-writing#405-method-not-allowed">https://www.soklet.com/docs/response-writing#405-method-not-allowed</a>. 076 * 077 * @param request the HTTP request 078 * @param allowedHttpMethods appropriate HTTP methods to write to the {@code Allow} response header 079 * @return the response to be sent over the wire 080 */ 081 @Nonnull 082 MarshaledResponse forMethodNotAllowed(@Nonnull Request request, 083 @Nonnull Set<HttpMethod> allowedHttpMethods); 084 085 /** 086 * Prepares a response for a request that triggers an <a href="https://httpwg.org/specs/rfc9110.html#status.413">HTTP 413 Content Too Large</a>. 087 * <p> 088 * Detailed documentation is available at <a href="https://www.soklet.com/docs/response-writing#413-content-too-large">https://www.soklet.com/docs/response-writing#413-content-too-large</a>. 089 * 090 * @param request the HTTP request 091 * @param resourceMethod the <em>Resource Method</em> that would have handled the request, if available 092 * @return the response to be sent over the wire 093 */ 094 @Nonnull 095 MarshaledResponse forContentTooLarge(@Nonnull Request request, 096 @Nullable ResourceMethod resourceMethod); 097 098 /** 099 * Prepares a response for an HTTP {@code OPTIONS} request. 100 * <p> 101 * Note that CORS preflight responses are handled specially by {@link #forCorsPreflightAllowed(Request, CorsPreflight, CorsPreflightResponse)} 102 * and {@link #forCorsPreflightRejected(Request, CorsPreflight)} - not this method. 103 * <p> 104 * Detailed documentation is available at <a href="https://www.soklet.com/docs/response-writing#http-options">https://www.soklet.com/docs/response-writing#http-options</a>. 105 * 106 * @param request the HTTP request 107 * @param allowedHttpMethods appropriate HTTP methods to write to the {@code Allow} response header 108 * @return the response to be sent over the wire 109 */ 110 @Nonnull 111 MarshaledResponse forOptions(@Nonnull Request request, 112 @Nonnull Set<HttpMethod> allowedHttpMethods); 113 114 /** 115 * Prepares a response for scenarios in which an uncaught exception is encountered. 116 * <p> 117 * Detailed documentation is available at <a href="https://www.soklet.com/docs/response-writing#uncaught-exceptions">https://www.soklet.com/docs/response-writing#uncaught-exceptions</a>. 118 * 119 * @param request the HTTP request 120 * @param throwable the exception that was thrown 121 * @param resourceMethod the <em>Resource Method</em> that would have handled the request, if available 122 * @return the response to be sent over the wire 123 */ 124 @Nonnull 125 MarshaledResponse forThrowable(@Nonnull Request request, 126 @Nonnull Throwable throwable, 127 @Nullable ResourceMethod resourceMethod); 128 129 /** 130 * Prepares a response for an HTTP {@code HEAD} request. 131 * <p> 132 * Detailed documentation is available at <a href="https://www.soklet.com/docs/response-writing#http-head">https://www.soklet.com/docs/response-writing#http-head</a>. 133 * 134 * @param request the HTTP request 135 * @param getMethodMarshaledResponse the binary data that would have been sent over the wire for an equivalent {@code GET} request (necessary in order to write the {@code Content-Length} header for a {@code HEAD} response) 136 * @return the response to be sent over the wire 137 */ 138 @Nonnull 139 MarshaledResponse forHead(@Nonnull Request request, 140 @Nonnull MarshaledResponse getMethodMarshaledResponse); 141 142 /** 143 * Prepares a response for "CORS preflight allowed" scenario when your {@link CorsAuthorizer} approves a preflight request. 144 * <p> 145 * Detailed documentation is available at <a href="https://www.soklet.com/docs/cors#writing-cors-responses">https://www.soklet.com/docs/cors#writing-cors-responses</a>. 146 * 147 * @param request the HTTP request 148 * @param corsPreflight the CORS preflight request data 149 * @param corsPreflightResponse the data that should be included in this CORS preflight response 150 * @return the response to be sent over the wire 151 */ 152 @Nonnull 153 MarshaledResponse forCorsPreflightAllowed(@Nonnull Request request, 154 @Nonnull CorsPreflight corsPreflight, 155 @Nonnull CorsPreflightResponse corsPreflightResponse); 156 157 /** 158 * Prepares a response for "CORS preflight rejected" scenario when your {@link CorsAuthorizer} denies a preflight request. 159 * <p> 160 * Detailed documentation is available at <a href="https://www.soklet.com/docs/cors#writing-cors-responses">https://www.soklet.com/docs/cors#writing-cors-responses</a>. 161 * 162 * @param request the HTTP request 163 * @param corsPreflight the CORS preflight request data 164 * @return the response to be sent over the wire 165 */ 166 @Nonnull 167 MarshaledResponse forCorsPreflightRejected(@Nonnull Request request, 168 @Nonnull CorsPreflight corsPreflight); 169 170 /** 171 * Applies "CORS is permitted for this request" data to a response. 172 * <p> 173 * Invoked for any non-preflight CORS request that your {@link CorsAuthorizer} approves. 174 * <p> 175 * This method will normally return a copy of the {@code marshaledResponse} with these headers applied 176 * based on the values of {@code corsResponse}: 177 * <ul> 178 * <li>{@code Access-Control-Allow-Origin} (required)</li> 179 * <li>{@code Access-Control-Allow-Credentials} (optional)</li> 180 * <li>{@code Access-Control-Expose-Headers} (optional)</li> 181 * </ul> 182 * 183 * @param request the HTTP request 184 * @param cors the CORS request data 185 * @param corsResponse CORS response data to write as specified by {@link CorsAuthorizer} 186 * @param marshaledResponse the existing response to which we should apply relevant CORS headers 187 * @return the response to be sent over the wire 188 */ 189 @Nonnull 190 MarshaledResponse forCorsAllowed(@Nonnull Request request, 191 @Nonnull Cors cors, 192 @Nonnull CorsResponse corsResponse, 193 @Nonnull MarshaledResponse marshaledResponse); 194 195 /** 196 * Acquires a {@link ResponseMarshaler} with a reasonable "out of the box" configuration. 197 * <p> 198 * Callers should not rely on reference identity; this method may return a new or cached instance. 199 * 200 * @return a {@code ResponseMarshaler} with default settings 201 */ 202 @Nonnull 203 static ResponseMarshaler withDefaults() { 204 return DefaultResponseMarshaler.defaultInstance(); 205 } 206 207 /** 208 * Acquires a builder for a default {@link ResponseMarshaler} implementation. 209 * 210 * @param charset the default charset to use when writing response data 211 * @return a {@code ResponseMarshaler} builder 212 */ 213 @Nonnull 214 static Builder withCharset(@Nonnull Charset charset) { 215 requireNonNull(charset); 216 return new Builder(charset); 217 } 218 219 /** 220 * Builder used to construct a standard implementation of {@link ResponseMarshaler}. 221 * <p> 222 * This class is intended for use by a single thread. 223 * 224 * @author <a href="https://www.revetkn.com">Mark Allen</a> 225 */ 226 @NotThreadSafe 227 class Builder { 228 @FunctionalInterface 229 public interface HappyPathHandler { 230 @Nonnull 231 MarshaledResponse handle(@Nonnull Request request, 232 @Nonnull Response response, 233 @Nonnull ResourceMethod method); 234 } 235 236 @FunctionalInterface 237 public interface NotFoundHandler { 238 @Nonnull 239 MarshaledResponse handle(@Nonnull Request request); 240 } 241 242 @FunctionalInterface 243 public interface MethodNotAllowedHandler { 244 @Nonnull 245 MarshaledResponse handle(@Nonnull Request request, 246 @Nonnull Set<HttpMethod> allowedHttpMethods); 247 } 248 249 @FunctionalInterface 250 public interface ContentTooLargeHandler { 251 @Nonnull 252 MarshaledResponse handle(@Nonnull Request request, 253 @Nullable ResourceMethod resourceMethod); 254 } 255 256 @FunctionalInterface 257 public interface OptionsHandler { 258 @Nonnull 259 MarshaledResponse handle(@Nonnull Request request, 260 @Nonnull Set<HttpMethod> allowedHttpMethods); 261 } 262 263 @FunctionalInterface 264 public interface ThrowableHandler { 265 @Nonnull 266 MarshaledResponse handle(@Nonnull Request request, 267 @Nonnull Throwable throwable, 268 @Nullable ResourceMethod resourceMethod); 269 } 270 271 @FunctionalInterface 272 public interface HeadHandler { 273 @Nonnull 274 MarshaledResponse handle(@Nonnull Request request, 275 @Nonnull MarshaledResponse getMethodMarshaledResponse); 276 } 277 278 @FunctionalInterface 279 public interface CorsPreflightAllowedHandler { 280 @Nonnull 281 MarshaledResponse handle(@Nonnull Request request, 282 @Nonnull CorsPreflight corsPreflight, 283 @Nonnull CorsPreflightResponse corsPreflightResponse); 284 } 285 286 @FunctionalInterface 287 public interface CorsPreflightRejectedHandler { 288 @Nonnull 289 MarshaledResponse handle(@Nonnull Request request, 290 @Nonnull CorsPreflight corsPreflight); 291 } 292 293 @FunctionalInterface 294 public interface CorsAllowedHandler { 295 @Nonnull 296 MarshaledResponse handle(@Nonnull Request request, 297 @Nonnull Cors cors, 298 @Nonnull CorsResponse corsResponse, 299 @Nonnull MarshaledResponse marshaledResponse); 300 } 301 302 @FunctionalInterface 303 public interface PostProcessor { 304 @Nonnull 305 MarshaledResponse postProcess(@Nonnull MarshaledResponse marshaledResponse); 306 } 307 308 @Nonnull 309 Charset charset; 310 @Nullable 311 HappyPathHandler happyPathHandler; 312 @Nullable 313 NotFoundHandler notFoundHandler; 314 @Nullable 315 MethodNotAllowedHandler methodNotAllowedHandler; 316 @Nullable 317 ContentTooLargeHandler contentTooLargeHandler; 318 @Nullable 319 OptionsHandler optionsHandler; 320 @Nullable 321 ThrowableHandler throwableHandler; 322 @Nullable 323 HeadHandler headHandler; 324 @Nullable 325 CorsPreflightAllowedHandler corsPreflightAllowedHandler; 326 @Nullable 327 CorsPreflightRejectedHandler corsPreflightRejectedHandler; 328 @Nullable 329 CorsAllowedHandler corsAllowedHandler; 330 @Nullable 331 PostProcessor postProcessor; 332 333 private Builder(@Nonnull Charset charset) { 334 requireNonNull(charset); 335 this.charset = charset; 336 } 337 338 /** 339 * Specifies the default charset to use for encoding character data. 340 * 341 * @param charset the charset to use for encoding character data 342 * @return this {@code Builder}, for chaining 343 */ 344 @Nonnull 345 public Builder charset(@Nonnull Charset charset) { 346 requireNonNull(charset); 347 this.charset = charset; 348 return this; 349 } 350 351 /** 352 * Specifies a custom "happy path" handler for requests. 353 * 354 * @param happyPathHandler an optional "happy path" handler 355 * @return this {@code Builder}, for chaining 356 */ 357 @Nonnull 358 public Builder happyPath(@Nullable HappyPathHandler happyPathHandler) { 359 this.happyPathHandler = happyPathHandler; 360 return this; 361 } 362 363 @Nonnull 364 public Builder notFound(@Nullable NotFoundHandler notFoundHandler) { 365 this.notFoundHandler = notFoundHandler; 366 return this; 367 } 368 369 @Nonnull 370 public Builder methodNotAllowed(@Nullable MethodNotAllowedHandler methodNotAllowedHandler) { 371 this.methodNotAllowedHandler = methodNotAllowedHandler; 372 return this; 373 } 374 375 @Nonnull 376 public Builder contentTooLarge(@Nullable ContentTooLargeHandler contentTooLargeHandler) { 377 this.contentTooLargeHandler = contentTooLargeHandler; 378 return this; 379 } 380 381 @Nonnull 382 public Builder options(@Nullable OptionsHandler optionsHandler) { 383 this.optionsHandler = optionsHandler; 384 return this; 385 } 386 387 @Nonnull 388 public Builder throwable(@Nullable ThrowableHandler throwableHandler) { 389 this.throwableHandler = throwableHandler; 390 return this; 391 } 392 393 @Nonnull 394 public Builder head(@Nullable HeadHandler headHandler) { 395 this.headHandler = headHandler; 396 return this; 397 } 398 399 @Nonnull 400 public Builder corsPreflightAllowed(@Nullable CorsPreflightAllowedHandler corsPreflightAllowedHandler) { 401 this.corsPreflightAllowedHandler = corsPreflightAllowedHandler; 402 return this; 403 } 404 405 @Nonnull 406 public Builder corsPreflightRejected(@Nullable CorsPreflightRejectedHandler corsPreflightRejectedHandler) { 407 this.corsPreflightRejectedHandler = corsPreflightRejectedHandler; 408 return this; 409 } 410 411 @Nonnull 412 public Builder corsAllowed(@Nullable CorsAllowedHandler corsAllowedHandler) { 413 this.corsAllowedHandler = corsAllowedHandler; 414 return this; 415 } 416 417 /** 418 * Specifies an optional "post-process" hook for any final customization or processing before data goes over the wire. 419 * 420 * @param postProcessor an optional "post-process" hook 421 * @return this {@code Builder}, for chaining 422 */ 423 @Nonnull 424 public Builder postProcessor(@Nullable PostProcessor postProcessor) { 425 this.postProcessor = postProcessor; 426 return this; 427 } 428 429 /** 430 * Constructs a default {@code ResponseMarshaler} instance. 431 * <p> 432 * The constructed instance is thread-safe. 433 * 434 * @return a {@code ResponseMarshaler} instance 435 */ 436 @Nonnull 437 public ResponseMarshaler build() { 438 return new DefaultResponseMarshaler(this); 439 } 440 } 441}