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}