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