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}