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 com.soklet.core.impl.DefaultIdGenerator;
020import com.soklet.core.impl.DefaultMultipartParser;
021import com.soklet.internal.spring.LinkedCaseInsensitiveMap;
022
023import javax.annotation.Nonnull;
024import javax.annotation.Nullable;
025import javax.annotation.concurrent.NotThreadSafe;
026import javax.annotation.concurrent.ThreadSafe;
027import java.net.URLEncoder;
028import java.nio.charset.Charset;
029import java.nio.charset.StandardCharsets;
030import java.util.ArrayList;
031import java.util.Collection;
032import java.util.Collections;
033import java.util.LinkedHashMap;
034import java.util.List;
035import java.util.Locale;
036import java.util.Map;
037import java.util.Objects;
038import java.util.Optional;
039import java.util.Set;
040import java.util.concurrent.locks.ReentrantLock;
041import java.util.function.Consumer;
042import java.util.stream.Collectors;
043
044import static com.soklet.core.Utilities.trimAggressivelyToNull;
045import static java.lang.String.format;
046import static java.util.Collections.unmodifiableList;
047import static java.util.Objects.requireNonNull;
048
049/**
050 * Encapsulates information specified in an HTTP request.
051 * <p>
052 * Detailed documentation available at <a href="https://www.soklet.com/docs/request-handling">https://www.soklet.com/docs/request-handling</a>.
053 *
054 * @author <a href="https://www.revetkn.com">Mark Allen</a>
055 */
056@ThreadSafe
057public class Request {
058        @Nonnull
059        private static final Charset DEFAULT_CHARSET;
060        @Nonnull
061        private static final IdGenerator DEFAULT_ID_GENERATOR;
062
063        static {
064                DEFAULT_CHARSET = StandardCharsets.UTF_8;
065                DEFAULT_ID_GENERATOR = new DefaultIdGenerator();
066        }
067
068        @Nonnull
069        private final Object id;
070        @Nonnull
071        private final HttpMethod httpMethod;
072        @Nonnull
073        private final String uri;
074        @Nonnull
075        private final ResourcePath resourcePath;
076        @Nonnull
077        private final Map<String, Set<String>> cookies;
078        @Nonnull
079        private final Map<String, Set<String>> queryParameters;
080        @Nonnull
081        private final Map<String, Set<String>> formParameters;
082        @Nullable
083        private final String contentType;
084        @Nullable
085        private final Charset charset;
086        @Nonnull
087        private final Map<String, Set<String>> headers;
088        @Nullable
089        private final Cors cors;
090        @Nullable
091        private final CorsPreflight corsPreflight;
092        @Nullable
093        private final byte[] body;
094        @Nonnull
095        private final Boolean multipart;
096        @Nonnull
097        private final Map<String, Set<MultipartField>> multipartFields;
098        @Nonnull
099        private final Boolean contentTooLarge;
100        @Nonnull
101        private final ReentrantLock lock;
102        @Nullable
103        private volatile String bodyAsString = null;
104        @Nullable
105        private volatile List<Locale> locales = null;
106
107        /**
108         * Acquires a builder for {@link Request} instances.
109         *
110         * @param httpMethod the HTTP method for this request ({@code GET, POST, etc.})
111         * @param uri        the URI for this request, which must start with a {@code /} character and might include query parameters, e.g. {@code /example/123} or {@code /one?two=three}
112         * @return the builder
113         */
114        @Nonnull
115        public static Builder with(@Nonnull HttpMethod httpMethod,
116                                                                                                                 @Nonnull String uri) {
117                requireNonNull(httpMethod);
118                requireNonNull(uri);
119
120                return new Builder(httpMethod, uri);
121        }
122
123        /**
124         * Vends a mutable copier seeded with this instance's data, suitable for building new instances.
125         *
126         * @return a copier for this instance
127         */
128        @Nonnull
129        public Copier copy() {
130                return new Copier(this);
131        }
132
133        protected Request(@Nonnull Builder builder) {
134                requireNonNull(builder);
135
136                // TODO: should we use InstanceProvider to vend IdGenerator type instead of explicitly specifying?
137                IdGenerator idGenerator = builder.idGenerator == null ? getDefaultIdGenerator() : builder.idGenerator;
138
139                this.lock = new ReentrantLock();
140                this.id = builder.id == null ? idGenerator.generateId() : builder.id;
141                this.httpMethod = builder.httpMethod;
142
143                // Header names are case-insensitive.  Enforce that here with a special map
144                this.headers = Collections.unmodifiableMap(new LinkedCaseInsensitiveMap<>(builder.headers));
145                this.cookies = Collections.unmodifiableMap(Utilities.extractCookiesFromHeaders(this.headers));
146                this.corsPreflight = this.httpMethod == HttpMethod.OPTIONS ? CorsPreflight.fromHeaders(this.headers).orElse(null) : null;
147                this.cors = this.corsPreflight == null ? Cors.fromHeaders(this.httpMethod, this.headers).orElse(null) : null;
148                this.body = builder.body;
149                this.contentType = Utilities.extractContentTypeFromHeaders(this.headers).orElse(null);
150                this.charset = Utilities.extractCharsetFromHeaders(this.headers).orElse(null);
151
152                String uri = trimAggressivelyToNull(builder.uri);
153
154                if (uri == null)
155                        throw new IllegalArgumentException("URI cannot be blank.");
156
157                if (!uri.startsWith("/"))
158                        throw new IllegalArgumentException(format("URI must start with a '/' character. Illegal URI was '%s'", uri));
159
160                // If the URI contains a query string, parse query parameters (if present) from it
161                if (uri.contains("?")) {
162                        this.uri = uri;
163                        this.queryParameters = Collections.unmodifiableMap(Utilities.extractQueryParametersFromUrl(uri));
164
165                        // Cannot have 2 different ways of specifying query parameters
166                        if (builder.queryParameters != null && builder.queryParameters.size() > 0)
167                                throw new IllegalArgumentException("You cannot specify both query parameters and a URI with a query string.");
168                } else {
169                        // If the URI does not contain a query string, then use query parameters provided by the builder, if present
170                        this.queryParameters = builder.queryParameters == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(builder.queryParameters));
171
172                        if (this.queryParameters.size() == 0) {
173                                this.uri = uri;
174                        } else {
175                                Charset queryParameterCharset = getCharset().orElse(DEFAULT_CHARSET);
176                                String queryString = this.queryParameters.entrySet().stream()
177                                                .map((entry) -> {
178                                                        String name = entry.getKey();
179                                                        Set<String> values = entry.getValue();
180
181                                                        if (name == null)
182                                                                return List.<String>of();
183
184                                                        if (values == null || values.size() == 0)
185                                                                return List.of(format("%s=", URLEncoder.encode(name, queryParameterCharset)));
186
187                                                        List<String> nameValuePairs = new ArrayList<>();
188
189                                                        for (String value : values)
190                                                                nameValuePairs.add(format("%s=%s", URLEncoder.encode(name, queryParameterCharset),
191                                                                                value == null ? "" : URLEncoder.encode(value, queryParameterCharset)));
192
193                                                        return nameValuePairs;
194                                                })
195                                                .filter(nameValuePairs -> nameValuePairs.size() > 0)
196                                                .flatMap(Collection::stream)
197                                                .collect(Collectors.joining("&"));
198
199                                this.uri = format("%s?%s", uri, queryString);
200                        }
201                }
202
203                this.resourcePath = ResourcePath.of(Utilities.normalizedPathForUrl(uri));
204
205                // Form parameters
206                // TODO: optimize copy/modify scenarios - we don't want to be re-processing body data
207                Map<String, Set<String>> formParameters = Map.of();
208
209                if (this.body != null && this.contentType != null && this.contentType.equalsIgnoreCase("application/x-www-form-urlencoded")) {
210                        String bodyAsString = getBodyAsString().orElse(null);
211                        formParameters = Collections.unmodifiableMap(Utilities.extractQueryParametersFromQuery(bodyAsString));
212                }
213
214                this.formParameters = formParameters;
215
216                // Multipart handling
217                // TODO: optimize copy/modify scenarios - we don't want to be copying big already-parsed multipart byte arrays
218                boolean multipart = false;
219                Map<String, Set<MultipartField>> multipartFields = Map.of();
220
221                if (this.contentType != null && this.contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/")) {
222                        multipart = true;
223
224                        // TODO: should we use InstanceProvider to vend MultipartParser type instead of explicitly specifying?
225                        MultipartParser multipartParser = builder.multipartParser == null ? DefaultMultipartParser.sharedInstance() : builder.multipartParser;
226                        multipartFields = Collections.unmodifiableMap(multipartParser.extractMultipartFields(this));
227                }
228
229                this.multipart = multipart;
230                this.multipartFields = multipartFields;
231
232                this.contentTooLarge = builder.contentTooLarge == null ? false : builder.contentTooLarge;
233        }
234
235        @Override
236        public String toString() {
237                return format("%s{id=%s, httpMethod=%s, uri=%s, path=%s, cookies=%s, queryParameters=%s, headers=%s, body=%s}",
238                                getClass().getSimpleName(), getId(), getHttpMethod(), getUri(), getPath(), getCookies(), getQueryParameters(),
239                                getHeaders(), format("%d bytes", getBody().isPresent() ? getBody().get().length : 0));
240        }
241
242        @Override
243        public boolean equals(@Nullable Object object) {
244                if (this == object)
245                        return true;
246
247                if (!(object instanceof Request request))
248                        return false;
249
250                return Objects.equals(getId(), request.getId())
251                                && Objects.equals(getHttpMethod(), request.getHttpMethod())
252                                && Objects.equals(getUri(), request.getUri())
253                                && Objects.equals(getQueryParameters(), request.getQueryParameters())
254                                && Objects.equals(getHeaders(), request.getHeaders())
255                                && Objects.equals(getBody(), request.getBody())
256                                && Objects.equals(isContentTooLarge(), request.isContentTooLarge());
257        }
258
259        @Override
260        public int hashCode() {
261                return Objects.hash(getId(), getHttpMethod(), getUri(), getQueryParameters(), getHeaders(), getBody(), isContentTooLarge());
262        }
263
264        /**
265         * An application-specific identifier for this request.
266         * <p>
267         * The identifier is not necessarily unique (for example, numbers that "wrap around" if they get too large).
268         *
269         * @return the request's identifier
270         */
271        @Nonnull
272        public Object getId() {
273                return this.id;
274        }
275
276        /**
277         * The <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods">HTTP method</a> for this request.
278         *
279         * @return the request's HTTP method
280         */
281        @Nonnull
282        public HttpMethod getHttpMethod() {
283                return this.httpMethod;
284        }
285
286        /**
287         * The URI for this request, which must start with a {@code /} character and might include query parameters, such as {@code /example/123} or {@code /one?two=three}.
288         *
289         * @return the request's URI
290         */
291        @Nonnull
292        public String getUri() {
293                return this.uri;
294        }
295
296        /**
297         * The path component of this request, which is a representation of the value returned by {@link #getUri()} with the query string (if any) removed.
298         *
299         * @return the path for this request
300         */
301        @Nonnull
302        public String getPath() {
303                return getResourcePath().getPath();
304        }
305
306        /**
307         * Convenience method to acquire a {@link ResourcePath} representation of {@link #getPath()}.
308         *
309         * @return the resource path for this request
310         */
311        @Nonnull
312        public ResourcePath getResourcePath() {
313                return this.resourcePath;
314        }
315
316        /**
317         * The cookies provided by the client for this request.
318         * <p>
319         * The keys are the {@code Cookie} header names and the values are {@code Cookie} header values
320         * (it is possible for a client to send multiple {@code Cookie} headers with the same name).
321         * <p>
322         * <em>Note that {@code Cookie} headers, like all request headers, have case-insensitive names per the HTTP spec.</em>
323         * <p>
324         * Use {@link #getCookie(String)} for a convenience method to access cookie values when only one is expected.
325         *
326         * @return the request's cookies
327         */
328        @Nonnull
329        public Map<String, Set<String>> getCookies() {
330                return this.cookies;
331        }
332
333        /**
334         * The query parameters provided by the client for this request.
335         * <p>
336         * The keys are the query parameter names and the values are query parameter values
337         * (it is possible for a client to send multiple query parameters with the same name, e.g. {@code ?test=1&test=2}).
338         * <p>
339         * <em>Note that query parameters have case-sensitive names per the HTTP spec.</em>
340         * <p>
341         * Use {@link #getQueryParameter(String)} for a convenience method to access query parameter values when only one is expected.
342         *
343         * @return the request's query parameters
344         */
345        @Nonnull
346        public Map<String, Set<String>> getQueryParameters() {
347                return this.queryParameters;
348        }
349
350        /**
351         * The HTML {@code form} parameters provided by the client for this request.
352         * <p>
353         * The keys are the form parameter names and the values are form parameter values
354         * (it is possible for a client to send multiple form parameters with the same name, e.g. {@code ?test=1&test=2}).
355         * <p>
356         * <em>Note that form parameters have case-sensitive names per the HTTP spec.</em>
357         * <p>
358         * Use {@link #getFormParameter(String)} for a convenience method to access form parameter values when only one is expected.
359         *
360         * @return the request's form parameters
361         */
362        @Nonnull
363        public Map<String, Set<String>> getFormParameters() {
364                return this.formParameters;
365        }
366
367        /**
368         * The headers provided by the client for this request.
369         * <p>
370         * The keys are the header names and the values are header values
371         * (it is possible for a client to send multiple headers with the same name).
372         * <p>
373         * <em>Note that request headers have case-insensitive names per the HTTP spec.</em>
374         * <p>
375         * Use {@link #getHeader(String)} for a convenience method to access header values when only one is expected.
376         *
377         * @return the request's headers
378         */
379        @Nonnull
380        public Map<String, Set<String>> getHeaders() {
381                return this.headers;
382        }
383
384        /**
385         * The {@code Content-Type} header value, as specified by the client.
386         *
387         * @return the request's {@code Content-Type} header value, or {@link Optional#empty()} if not specified
388         */
389        @Nonnull
390        public Optional<String> getContentType() {
391                return Optional.ofNullable(this.contentType);
392        }
393
394        /**
395         * The request's character encoding, as specified by the client in the {@code Content-Type} header value.
396         *
397         * @return the request's character encoding, or {@link Optional#empty()} if not specified
398         */
399        @Nonnull
400        public Optional<Charset> getCharset() {
401                return Optional.ofNullable(this.charset);
402        }
403
404        /**
405         * Is this a request with {@code Content-Type} of {@code multipart/form-data}?
406         *
407         * @return {@code true} if this is a {@code multipart/form-data} request, {@code false} otherwise
408         */
409        @Nonnull
410        public Boolean isMultipart() {
411                return this.multipart;
412        }
413
414        /**
415         * The HTML {@code multipart/form-data} fields provided by the client for this request.
416         * <p>
417         * The keys are the multipart field names and the values are multipart field values
418         * (it is possible for a client to send multiple multipart fields with the same name).
419         * <p>
420         * <em>Note that multipart fields have case-sensitive names per the HTTP spec.</em>
421         * <p>
422         * Use {@link #getMultipartField(String)} for a convenience method to access a multipart parameter field value when only one is expected.
423         * <p>
424         * When using {@link com.soklet.core.impl.DefaultServer}, multipart fields are parsed using the {@link MultipartParser} as configured by {@link com.soklet.core.impl.DefaultServer.Builder#multipartParser(MultipartParser)}.
425         *
426         * @return the request's multipart fields, or the empty map if none are present
427         */
428        @Nonnull
429        public Map<String, Set<MultipartField>> getMultipartFields() {
430                return this.multipartFields;
431        }
432
433        /**
434         * The raw bytes of the request body.
435         * <p>
436         * For convenience, {@link #getBodyAsString()} is available if you expect your request body to be of type {@link String}.
437         *
438         * @return the request body bytes, or {@link Optional#empty()} if none was supplied
439         */
440        @Nonnull
441        public Optional<byte[]> getBody() {
442                return Optional.ofNullable(this.body);
443        }
444
445        /**
446         * Was this request too large for the server to handle?
447         * <p>
448         * <em>If so, this request might have incomplete sets of headers/cookies. It will always have a zero-length body.</em>
449         * <p>
450         * Soklet is designed to power systems that exchange small "transactional" payloads that live entirely in memory. It is not appropriate for handling multipart files at scale, buffering uploads to disk, streaming, etc.
451         * <p>
452         * When using {@link com.soklet.core.impl.DefaultServer}, maximum request size is configured by {@link com.soklet.core.impl.DefaultServer.Builder#maximumRequestSizeInBytes(Integer)}.
453         *
454         * @return {@code true} if this request is larger than the server is able to handle, {@code false} otherwise
455         */
456        @Nonnull
457        public Boolean isContentTooLarge() {
458                return this.contentTooLarge;
459        }
460
461        /**
462         * Convenience method that provides the {@link #getBody()} bytes as a {@link String} encoded using the client-specified character set per {@link #getCharset()}.
463         * <p>
464         * If no character set is specified, {@link StandardCharsets#UTF_8} is used to perform the encoding.
465         * <p>
466         * This method will lazily convert the raw bytes as specified by {@link #getBody()} to an instance of {@link String} when first invoked.  The {@link String} representation is then cached and re-used for subsequent invocations.
467         * <p>
468         * This method is threadsafe.
469         *
470         * @return a {@link String} representation of this request's body, or {@link Optional#empty()} if no request body was specified by the client
471         */
472        @Nonnull
473        public Optional<String> getBodyAsString() {
474                // Lazily instantiate a string instance using double-checked locking
475                if (this.body != null && this.bodyAsString == null) {
476                        getLock().lock();
477                        try {
478                                if (this.body != null && this.bodyAsString == null)
479                                        this.bodyAsString = new String(this.body, getCharset().orElse(DEFAULT_CHARSET));
480                        } finally {
481                                getLock().unlock();
482                        }
483                }
484
485                return Optional.ofNullable(this.bodyAsString);
486        }
487
488        /**
489         * <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">Non-preflight CORS</a> request data.
490         * <p>
491         * See <a href="https://www.soklet.com/docs/cors">https://www.soklet.com/docs/cors</a> for details.
492         *
493         * @return non-preflight CORS request data, or {@link Optional#empty()} if none was specified
494         */
495        @Nonnull
496        public Optional<Cors> getCors() {
497                return Optional.ofNullable(this.cors);
498        }
499
500        /**
501         * <a href="https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request">CORS preflight</a>-related request data.
502         * <p>
503         * See <a href="https://www.soklet.com/docs/cors">https://www.soklet.com/docs/cors</a> for details.
504         *
505         * @return preflight CORS request data, or {@link Optional#empty()} if none was specified
506         */
507        @Nonnull
508        public Optional<CorsPreflight> getCorsPreflight() {
509                return Optional.ofNullable(this.corsPreflight);
510        }
511
512        /**
513         * Locale information for this request as specified by {@code Accept-Language} header value[s] and ordered by weight as defined by <a href="https://www.rfc-editor.org/rfc/rfc7231#section-5.3.5">RFC 7231, Section 5.3.5</a>.
514         * <p>
515         * This method will lazily parse {@code Accept-Language} header values into to an ordered {@link List} of {@link Locale} when first invoked.  This representation is then cached and re-used for subsequent invocations.
516         * <p>
517         * This method is threadsafe.
518         *
519         * @return locale information for this request, or the empty list if none was specified
520         */
521        @Nonnull
522        public List<Locale> getLocales() {
523                // Lazily instantiate our parsed locales using double-checked locking
524                if (this.locales == null) {
525                        getLock().lock();
526                        try {
527                                if (this.locales == null) {
528                                        Set<String> acceptLanguageHeaderValue = getHeaders().get("Accept-Language");
529
530                                        if (acceptLanguageHeaderValue != null && acceptLanguageHeaderValue.size() > 0)
531                                                this.locales = unmodifiableList(Utilities.localesFromAcceptLanguageHeaderValue(acceptLanguageHeaderValue.stream().findFirst().get()));
532                                        else
533                                                this.locales = List.of();
534                                } else {
535                                        this.locales = List.of();
536                                }
537                        } finally {
538                                getLock().unlock();
539                        }
540                }
541
542                return this.locales;
543        }
544
545        /**
546         * Convenience method to access a query parameter's value when at most one is expected for the given {@code name}.
547         * <p>
548         * If a query parameter {@code name} can support multiple values, {@link #getQueryParameters()} should be used instead of this method.
549         * <p>
550         * If this method is invoked for a query parameter {@code name} with multiple values, Soklet does not guarantee which value will be returned.
551         * <p>
552         * <em>Note that query parameters have case-sensitive names per the HTTP spec.</em>
553         *
554         * @param name the name of the query parameter
555         * @return the value for the query parameter, or {@link Optional#empty()} if none is present
556         */
557        @Nonnull
558        public Optional<String> getQueryParameter(@Nonnull String name) {
559                requireNonNull(name);
560                return singleValueForName(name, getQueryParameters());
561        }
562
563        /**
564         * Convenience method to access a form parameter's value when at most one is expected for the given {@code name}.
565         * <p>
566         * If a form parameter {@code name} can support multiple values, {@link #getFormParameters()} should be used instead of this method.
567         * <p>
568         * If this method is invoked for a form parameter {@code name} with multiple values, Soklet does not guarantee which value will be returned.
569         * <p>
570         * <em>Note that form parameters have case-sensitive names per the HTTP spec.</em>
571         *
572         * @param name the name of the form parameter
573         * @return the value for the form parameter, or {@link Optional#empty()} if none is present
574         */
575        @Nonnull
576        public Optional<String> getFormParameter(@Nonnull String name) {
577                requireNonNull(name);
578                return singleValueForName(name, getFormParameters());
579        }
580
581        /**
582         * Convenience method to access a header's value when at most one is expected for the given {@code name}.
583         * <p>
584         * If a header {@code name} can support multiple values, {@link #getHeaders()} should be used instead of this method.
585         * <p>
586         * If this method is invoked for a header {@code name} with multiple values, Soklet does not guarantee which value will be returned.
587         * <p>
588         * <em>Note that request headers have case-insensitive names per the HTTP spec.</em>
589         *
590         * @param name the name of the header
591         * @return the value for the header, or {@link Optional#empty()} if none is present
592         */
593        @Nonnull
594        public Optional<String> getHeader(@Nonnull String name) {
595                requireNonNull(name);
596                return singleValueForName(name, getHeaders());
597        }
598
599        /**
600         * Convenience method to access a cookie's value when at most one is expected for the given {@code name}.
601         * <p>
602         * If a cookie {@code name} can support multiple values, {@link #getCookies()} should be used instead of this method.
603         * <p>
604         * If this method is invoked for a cookie {@code name} with multiple values, Soklet does not guarantee which value will be returned.
605         * <p>
606         * <em>Note that {@code Cookie} headers, like all request headers, have case-insensitive names per the HTTP spec.</em>
607         *
608         * @param name the name of the cookie
609         * @return the value for the cookie, or {@link Optional#empty()} if none is present
610         */
611        @Nonnull
612        public Optional<String> getCookie(@Nonnull String name) {
613                requireNonNull(name);
614                return singleValueForName(name, getCookies());
615        }
616
617        /**
618         * Convenience method to access a multipart field when at most one is expected for the given {@code name}.
619         * <p>
620         * If a {@code name} can support multiple multipart fields, {@link #getMultipartFields()} should be used instead of this method.
621         * <p>
622         * If this method is invoked for a {@code name} with multiple multipart field values, Soklet does not guarantee which value will be returned.
623         * <p>
624         * <em>Note that multipart fields have case-sensitive names per the HTTP spec.</em>
625         *
626         * @param name the name of the multipart field
627         * @return the multipart field value, or {@link Optional#empty()} if none is present
628         */
629        @Nonnull
630        public Optional<MultipartField> getMultipartField(@Nonnull String name) {
631                requireNonNull(name);
632                return singleValueForName(name, getMultipartFields());
633        }
634
635        @Nonnull
636        protected IdGenerator getDefaultIdGenerator() {
637                return DEFAULT_ID_GENERATOR;
638        }
639
640        @Nonnull
641        protected ReentrantLock getLock() {
642                return this.lock;
643        }
644
645        @Nonnull
646        protected <T> Optional<T> singleValueForName(@Nonnull String name,
647                                                                                                                                                                                         @Nullable Map<String, Set<T>> valuesByName) {
648                if (valuesByName == null)
649                        return Optional.empty();
650
651                Set<T> values = valuesByName.get(name);
652
653                if (values == null)
654                        return Optional.empty();
655
656                if (values.size() > 1)
657                        throw new IllegalArgumentException(format("Expected single value but found multiple values for %s: %s", name, values));
658
659                return values.stream().findFirst();
660        }
661
662        /**
663         * Builder used to construct instances of {@link Request} via {@link Request#with(HttpMethod, String)}.
664         * <p>
665         * This class is intended for use by a single thread.
666         *
667         * @author <a href="https://www.revetkn.com">Mark Allen</a>
668         */
669        @NotThreadSafe
670        public static class Builder {
671                @Nonnull
672                private HttpMethod httpMethod;
673                @Nonnull
674                private String uri;
675                @Nullable
676                private Object id;
677                @Nullable
678                private IdGenerator idGenerator;
679                @Nullable
680                private MultipartParser multipartParser;
681                @Nullable
682                private Map<String, Set<String>> queryParameters;
683                @Nullable
684                private Map<String, Set<String>> headers;
685                @Nullable
686                private byte[] body;
687                @Nullable
688                private Boolean contentTooLarge;
689
690                protected Builder(@Nonnull HttpMethod httpMethod,
691                                                                                        @Nonnull String uri) {
692                        requireNonNull(httpMethod);
693                        requireNonNull(uri);
694
695                        this.httpMethod = httpMethod;
696                        this.uri = uri;
697                }
698
699                @Nonnull
700                public Builder httpMethod(@Nonnull HttpMethod httpMethod) {
701                        requireNonNull(httpMethod);
702                        this.httpMethod = httpMethod;
703                        return this;
704                }
705
706                @Nonnull
707                public Builder uri(@Nonnull String uri) {
708                        requireNonNull(uri);
709                        this.uri = uri;
710                        return this;
711                }
712
713                @Nonnull
714                public Builder id(@Nullable Object id) {
715                        this.id = id;
716                        return this;
717                }
718
719                @Nonnull
720                public Builder idGenerator(@Nullable IdGenerator idGenerator) {
721                        this.idGenerator = idGenerator;
722                        return this;
723                }
724
725                @Nonnull
726                public Builder multipartParser(@Nullable MultipartParser multipartParser) {
727                        this.multipartParser = multipartParser;
728                        return this;
729                }
730
731                @Nonnull
732                public Builder queryParameters(@Nullable Map<String, Set<String>> queryParameters) {
733                        this.queryParameters = queryParameters;
734                        return this;
735                }
736
737                @Nonnull
738                public Builder headers(@Nullable Map<String, Set<String>> headers) {
739                        this.headers = headers;
740                        return this;
741                }
742
743                @Nonnull
744                public Builder body(@Nullable byte[] body) {
745                        this.body = body;
746                        return this;
747                }
748
749                @Nonnull
750                public Builder contentTooLarge(@Nullable Boolean contentTooLarge) {
751                        this.contentTooLarge = contentTooLarge;
752                        return this;
753                }
754
755                @Nonnull
756                public Request build() {
757                        return new Request(this);
758                }
759        }
760
761        /**
762         * Builder used to copy instances of {@link Request} via {@link Request#copy()}.
763         * <p>
764         * This class is intended for use by a single thread.
765         *
766         * @author <a href="https://www.revetkn.com">Mark Allen</a>
767         */
768        @NotThreadSafe
769        public static class Copier {
770                @Nonnull
771                private final Builder builder;
772
773                Copier(@Nonnull Request request) {
774                        requireNonNull(request);
775
776                        this.builder = new Builder(request.getHttpMethod(), request.getUri())
777                                        .id(request.getId())
778                                        .queryParameters(new LinkedHashMap<>(request.getQueryParameters()))
779                                        .headers(new LinkedCaseInsensitiveMap<>(request.getHeaders()))
780                                        .body(request.getBody().orElse(null))
781                                        .contentTooLarge(request.isContentTooLarge());
782                }
783
784                @Nonnull
785                public Copier httpMethod(@Nonnull HttpMethod httpMethod) {
786                        requireNonNull(httpMethod);
787                        this.builder.httpMethod(httpMethod);
788                        return this;
789                }
790
791                @Nonnull
792                public Copier uri(@Nonnull String uri) {
793                        requireNonNull(uri);
794                        this.builder.uri(uri);
795                        return this;
796                }
797
798                @Nonnull
799                public Copier id(@Nullable Object id) {
800                        this.builder.id(id);
801                        return this;
802                }
803
804                @Nonnull
805                public Copier queryParameters(@Nullable Map<String, Set<String>> queryParameters) {
806                        this.builder.queryParameters(queryParameters);
807                        return this;
808                }
809
810                // Convenience method for mutation
811                @Nonnull
812                public Copier queryParameters(@Nonnull Consumer<Map<String, Set<String>>> queryParametersConsumer) {
813                        requireNonNull(queryParametersConsumer);
814
815                        if (this.builder.queryParameters == null)
816                                this.builder.queryParameters(new LinkedHashMap<>());
817
818                        queryParametersConsumer.accept(this.builder.queryParameters);
819                        return this;
820                }
821
822                @Nonnull
823                public Copier headers(@Nullable Map<String, Set<String>> headers) {
824                        this.builder.headers(headers);
825                        return this;
826                }
827
828                // Convenience method for mutation
829                @Nonnull
830                public Copier headers(@Nonnull Consumer<Map<String, Set<String>>> headersConsumer) {
831                        requireNonNull(headersConsumer);
832
833                        if (this.builder.headers == null)
834                                this.builder.headers(new LinkedCaseInsensitiveMap<>());
835
836                        headersConsumer.accept(this.builder.headers);
837                        return this;
838                }
839
840                @Nonnull
841                public Copier body(@Nullable byte[] body) {
842                        this.builder.body(body);
843                        return this;
844                }
845
846                @Nonnull
847                public Copier contentTooLarge(@Nullable Boolean contentTooLarge) {
848                        this.builder.contentTooLarge(contentTooLarge);
849                        return this;
850                }
851
852                @Nonnull
853                public Request finish() {
854                        return this.builder.build();
855                }
856        }
857}