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}