001/* 002 * Copyright 2022-2026 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.exception.IllegalFormParameterException; 020import com.soklet.exception.IllegalMultipartFieldException; 021import com.soklet.exception.IllegalQueryParameterException; 022import com.soklet.exception.IllegalRequestCookieException; 023import com.soklet.exception.IllegalRequestException; 024import com.soklet.exception.IllegalRequestHeaderException; 025import com.soklet.internal.spring.LinkedCaseInsensitiveMap; 026import org.jspecify.annotations.NonNull; 027import org.jspecify.annotations.Nullable; 028 029import javax.annotation.concurrent.NotThreadSafe; 030import javax.annotation.concurrent.ThreadSafe; 031import java.net.InetSocketAddress; 032import java.nio.charset.Charset; 033import java.nio.charset.StandardCharsets; 034import java.util.Arrays; 035import java.util.Collections; 036import java.util.LinkedHashMap; 037import java.util.LinkedHashSet; 038import java.util.List; 039import java.util.Locale; 040import java.util.Locale.LanguageRange; 041import java.util.Map; 042import java.util.Objects; 043import java.util.Optional; 044import java.util.Set; 045import java.util.concurrent.locks.ReentrantLock; 046import java.util.function.Consumer; 047import java.util.stream.Collectors; 048 049import static com.soklet.Utilities.trimAggressivelyToEmpty; 050import static java.lang.String.format; 051import static java.util.Collections.unmodifiableList; 052import static java.util.Objects.requireNonNull; 053 054/** 055 * Encapsulates information specified in an HTTP request. 056 * <p> 057 * Instances can be acquired via the {@link #withRawUrl(HttpMethod, String)} (e.g. provided by clients on a "raw" HTTP/1.1 request line, un-decoded) and {@link #withPath(HttpMethod, String)} (e.g. manually-constructed during integration testing, understood to be already-decoded) builder factory methods. 058 * Convenience instance factories are also available via {@link #fromRawUrl(HttpMethod, String)} and {@link #fromPath(HttpMethod, String)}. 059 * <p> 060 * Any necessary decoding (path, URL parameter, {@code Content-Type: application/x-www-form-urlencoded}, etc.) will be automatically performed. Unless otherwise indicated, all accessor methods will return decoded data. 061 * <p> 062 * For performance, collection values (headers, query parameters, form parameters, cookies, multipart fields) are shallow-copied and not defensively deep-copied. Treat returned collections as immutable. 063 * <p> 064 * Detailed documentation available at <a href="https://www.soklet.com/docs/request-handling">https://www.soklet.com/docs/request-handling</a>. 065 * 066 * @author <a href="https://www.revetkn.com">Mark Allen</a> 067 */ 068@ThreadSafe 069public final class Request { 070 @NonNull 071 private static final Charset DEFAULT_CHARSET; 072 @NonNull 073 private static final IdGenerator DEFAULT_ID_GENERATOR; 074 075 static { 076 DEFAULT_CHARSET = StandardCharsets.UTF_8; 077 DEFAULT_ID_GENERATOR = DefaultIdGenerator.defaultInstance(); 078 } 079 080 @NonNull 081 private final Object id; 082 @NonNull 083 private final HttpMethod httpMethod; 084 @NonNull 085 private final String rawPath; 086 @Nullable 087 private final String rawQuery; 088 @NonNull 089 private final String path; 090 @NonNull 091 private final ResourcePath resourcePath; 092 @NonNull 093 private final Map<@NonNull String, @NonNull Set<@NonNull String>> queryParameters; 094 @Nullable 095 private final String contentType; 096 @Nullable 097 private final Charset charset; 098 @NonNull 099 private final Map<@NonNull String, @NonNull Set<@NonNull String>> headers; 100 @Nullable 101 private final InetSocketAddress remoteAddress; 102 @Nullable 103 private final Cors cors; 104 @Nullable 105 private final CorsPreflight corsPreflight; 106 @Nullable 107 private final byte[] body; 108 @NonNull 109 private final Boolean multipart; 110 @NonNull 111 private final Boolean contentTooLarge; 112 @NonNull 113 private final MultipartParser multipartParser; 114 @NonNull 115 private final IdGenerator<?> idGenerator; 116 @NonNull 117 private final ReentrantLock lock; 118 @Nullable 119 private volatile String bodyAsString = null; 120 @Nullable 121 private volatile List<@NonNull Locale> locales = null; 122 @Nullable 123 private volatile List<@NonNull LanguageRange> languageRanges = null; 124 @Nullable 125 private volatile Map<@NonNull String, @NonNull Set<@NonNull String>> cookies = null; 126 @Nullable 127 private volatile Map<@NonNull String, @NonNull Set<@NonNull MultipartField>> multipartFields = null; 128 @Nullable 129 private volatile Map<@NonNull String, @NonNull Set<@NonNull String>> formParameters = null; 130 131 /** 132 * Acquires a builder for {@link Request} instances from the URL provided by clients on a "raw" HTTP/1.1 request line. 133 * <p> 134 * The provided {@code rawUrl} must be un-decoded and in either "path-and-query" form (i.e. starts with a {@code /} character) or an absolute URL (i.e. starts with {@code http://} or {@code https://}). 135 * It might include un-decoded query parameters, e.g. {@code https://www.example.com/one?two=thr%20ee} or {@code /one?two=thr%20ee}. An exception to this rule is {@code OPTIONS *} requests, where the URL is the {@code *} "splat" symbol. 136 * <p> 137 * Note: request targets are normalized to origin-form. For example, if a client sends an absolute-form URL like {@code http://example.com/path?query}, only the path and query components are retained. 138 * <p> 139 * Paths will be percent-decoded. Percent-encoded slashes (e.g. {@code %2F}) are rejected. 140 * Malformed percent-encoding is rejected. 141 * <p> 142 * Query parameters are parsed and decoded using RFC 3986 semantics - see {@link QueryFormat#RFC_3986_STRICT}. 143 * Query decoding always uses UTF-8, regardless of any {@code Content-Type} charset. 144 * <p> 145 * Request body form parameters with {@code Content-Type: application/x-www-form-urlencoded} are parsed and decoded by using {@link QueryFormat#X_WWW_FORM_URLENCODED}. 146 * 147 * @param httpMethod the HTTP method for this request ({@code GET, POST, etc.}) 148 * @param rawUrl the raw (un-decoded) URL for this request 149 * @return the builder 150 */ 151 @NonNull 152 public static RawBuilder withRawUrl(@NonNull HttpMethod httpMethod, 153 @NonNull String rawUrl) { 154 requireNonNull(httpMethod); 155 requireNonNull(rawUrl); 156 157 return new RawBuilder(httpMethod, rawUrl); 158 } 159 160 /** 161 * Creates a {@link Request} from a raw request target without additional customization. 162 * 163 * @param httpMethod the HTTP method 164 * @param rawUrl a raw HTTP/1.1 request target (not URL-decoded) 165 * @return a {@link Request} instance 166 */ 167 @NonNull 168 public static Request fromRawUrl(@NonNull HttpMethod httpMethod, 169 @NonNull String rawUrl) { 170 return withRawUrl(httpMethod, rawUrl).build(); 171 } 172 173 /** 174 * Acquires a builder for {@link Request} instances from already-decoded path and query components - useful for manual construction, e.g. integration tests. 175 * <p> 176 * The provided {@code path} must start with the {@code /} character and already be decoded (e.g. {@code "/my path"}, not {@code "/my%20path"}). It must not include query parameters. For {@code OPTIONS *} requests, the {@code path} must be {@code *} - the "splat" symbol. 177 * <p> 178 * Query parameters must be specified via {@link PathBuilder#queryParameters(Map)} and are assumed to be already-decoded. 179 * <p> 180 * Request body form parameters with {@code Content-Type: application/x-www-form-urlencoded} are parsed and decoded by using {@link QueryFormat#X_WWW_FORM_URLENCODED}. 181 * 182 * @param httpMethod the HTTP method for this request ({@code GET, POST, etc.}) 183 * @param path the decoded URL path for this request 184 * @return the builder 185 */ 186 @NonNull 187 public static PathBuilder withPath(@NonNull HttpMethod httpMethod, 188 @NonNull String path) { 189 requireNonNull(httpMethod); 190 requireNonNull(path); 191 192 return new PathBuilder(httpMethod, path); 193 } 194 195 /** 196 * Creates a {@link Request} from a path without additional customization. 197 * 198 * @param httpMethod the HTTP method 199 * @param path a decoded request path (e.g. {@code /widgets/123}) 200 * @return a {@link Request} instance 201 */ 202 @NonNull 203 public static Request fromPath(@NonNull HttpMethod httpMethod, 204 @NonNull String path) { 205 return withPath(httpMethod, path).build(); 206 } 207 208 /** 209 * Vends a mutable copier seeded with this instance's data, suitable for building new instances. 210 * 211 * @return a copier for this instance 212 */ 213 @NonNull 214 public Copier copy() { 215 return new Copier(this); 216 } 217 218 private Request(@Nullable RawBuilder rawBuilder, 219 @Nullable PathBuilder pathBuilder) { 220 // Should never occur 221 if (rawBuilder == null && pathBuilder == null) 222 throw new IllegalStateException(format("Neither %s nor %s were specified", RawBuilder.class.getSimpleName(), PathBuilder.class.getSimpleName())); 223 224 IdGenerator builderIdGenerator = rawBuilder == null ? pathBuilder.idGenerator : rawBuilder.idGenerator; 225 Object builderId = rawBuilder == null ? pathBuilder.id : rawBuilder.id; 226 HttpMethod builderHttpMethod = rawBuilder == null ? pathBuilder.httpMethod : rawBuilder.httpMethod; 227 byte[] builderBody = rawBuilder == null ? pathBuilder.body : rawBuilder.body; 228 MultipartParser builderMultipartParser = rawBuilder == null ? pathBuilder.multipartParser : rawBuilder.multipartParser; 229 Boolean builderContentTooLarge = rawBuilder == null ? pathBuilder.contentTooLarge : rawBuilder.contentTooLarge; 230 Map<String, Set<String>> builderHeaders = rawBuilder == null ? pathBuilder.headers : rawBuilder.headers; 231 InetSocketAddress builderRemoteAddress = rawBuilder == null ? pathBuilder.remoteAddress : rawBuilder.remoteAddress; 232 233 if (builderHeaders == null) 234 builderHeaders = Map.of(); 235 236 this.idGenerator = builderIdGenerator == null ? DEFAULT_ID_GENERATOR : builderIdGenerator; 237 this.multipartParser = builderMultipartParser == null ? DefaultMultipartParser.defaultInstance() : builderMultipartParser; 238 239 // Header names are case-insensitive. Enforce that here with a special map 240 Map<String, Set<String>> caseInsensitiveHeaders = new LinkedCaseInsensitiveMap<>(builderHeaders); 241 this.headers = Collections.unmodifiableMap(caseInsensitiveHeaders); 242 this.contentType = Utilities.extractContentTypeFromHeaders(this.headers).orElse(null); 243 this.charset = Utilities.extractCharsetFromHeaders(this.headers).orElse(null); 244 this.remoteAddress = builderRemoteAddress; 245 246 String path; 247 248 // If we use PathBuilder, use its path directly. 249 // If we use RawBuilder, parse and decode its path. 250 if (pathBuilder != null) { 251 path = trimAggressivelyToEmpty(pathBuilder.path); 252 253 // Validate path 254 if (!path.startsWith("/") && !path.equals("*")) 255 throw new IllegalRequestException("Path must start with '/' or be '*'"); 256 257 if (path.contains("?")) 258 throw new IllegalRequestException(format("Path should not contain a query string. Use %s.withPath(...).queryParameters(...) to specify query parameters as a %s.", 259 Request.class.getSimpleName(), Map.class.getSimpleName())); 260 261 // Use already-decoded query parameters as provided by the path builder 262 this.queryParameters = pathBuilder.queryParameters == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(pathBuilder.queryParameters)); 263 } else { 264 // RawBuilder scenario 265 String rawUrl = trimAggressivelyToEmpty(rawBuilder.rawUrl); 266 267 // Special handling for OPTIONS * 268 if ("*".equals(rawUrl)) { 269 path = "*"; 270 this.queryParameters = Map.of(); 271 } else { 272 // First, parse and decode the path... 273 path = Utilities.extractPathFromUrl(rawUrl, true); 274 275 // ...then, parse out any query parameters. 276 if (rawUrl.contains("?")) { 277 // We always assume RFC_3986_STRICT for query parameters because Soklet is for modern systems - HTML Form "GET" submissions are rare/legacy. 278 // This means we leave "+" as "+" (not decode to " ") and then apply any percent-decoding rules. 279 // Query parameters are decoded as UTF-8 regardless of Content-Type. 280 // In the future, we might expose a way to let applications prefer QueryFormat.X_WWW_FORM_URLENCODED instead, which treats "+" as a space 281 this.queryParameters = Collections.unmodifiableMap(Utilities.extractQueryParametersFromUrl(rawUrl, QueryFormat.RFC_3986_STRICT, DEFAULT_CHARSET)); 282 } else { 283 this.queryParameters = Map.of(); 284 } 285 } 286 } 287 288 if (path.equals("*") && builderHttpMethod != HttpMethod.OPTIONS) 289 throw new IllegalRequestException(format("Path '*' is only legal for HTTP %s", HttpMethod.OPTIONS.name())); 290 291 if (path.contains("\u0000") || path.contains("%00")) 292 throw new IllegalRequestException(format("Illegal null byte in path '%s'", path)); 293 294 this.path = path; 295 296 String rawPath; 297 String rawQuery; 298 299 if (pathBuilder != null) { 300 // PathBuilder scenario: check if explicit raw values were provided 301 if (pathBuilder.rawPath != null) { 302 // Explicit raw values provided (e.g. from Copier preserving originals) 303 rawPath = pathBuilder.rawPath; 304 rawQuery = pathBuilder.rawQuery; 305 } else { 306 // No explicit raw values; encode from decoded values 307 if (path.equals("*")) { 308 rawPath = "*"; 309 } else { 310 rawPath = Utilities.encodePath(path); 311 } 312 313 if (this.queryParameters.isEmpty()) { 314 rawQuery = null; 315 } else { 316 rawQuery = Utilities.encodeQueryParameters(this.queryParameters, QueryFormat.RFC_3986_STRICT); 317 } 318 } 319 } else { 320 // RawBuilder scenario: extract raw components from rawUrl 321 String rawUrl = trimAggressivelyToEmpty(rawBuilder.rawUrl); 322 323 if ("*".equals(rawUrl)) { 324 rawPath = "*"; 325 rawQuery = null; 326 } else { 327 rawPath = Utilities.extractPathFromUrl(rawUrl, false); 328 if (containsEncodedSlash(rawPath)) 329 throw new IllegalRequestException("Encoded slashes are not allowed in request paths"); 330 rawQuery = Utilities.extractRawQueryFromUrl(rawUrl).orElse(null); 331 } 332 } 333 334 this.rawPath = rawPath; 335 this.rawQuery = rawQuery; 336 337 this.lock = new ReentrantLock(); 338 this.httpMethod = builderHttpMethod; 339 this.corsPreflight = this.httpMethod == HttpMethod.OPTIONS ? CorsPreflight.fromHeaders(this.headers).orElse(null) : null; 340 this.cors = this.corsPreflight == null ? Cors.fromHeaders(this.httpMethod, this.headers).orElse(null) : null; 341 this.resourcePath = this.path.equals("*") ? ResourcePath.OPTIONS_SPLAT_RESOURCE_PATH : ResourcePath.fromPath(this.path); 342 this.multipart = this.contentType != null && this.contentType.toLowerCase(Locale.ENGLISH).startsWith("multipart/"); 343 this.contentTooLarge = builderContentTooLarge == null ? false : builderContentTooLarge; 344 345 // It's illegal to specify a body if the request is marked "content too large" 346 this.body = this.contentTooLarge ? null : builderBody; 347 348 // Last step of ctor: generate an ID (if necessary) using this fully-constructed Request 349 this.id = builderId == null ? this.idGenerator.generateId(this) : builderId; 350 351 // Note that cookies, form parameters, and multipart data are lazily parsed/instantiated when callers try to access them 352 } 353 354 @Override 355 public String toString() { 356 return format("%s{id=%s, httpMethod=%s, path=%s, cookies=%s, queryParameters=%s, headers=%s, body=%s}", 357 getClass().getSimpleName(), getId(), getHttpMethod(), getPath(), getCookies(), getQueryParameters(), 358 getHeaders(), format("%d bytes", getBody().isPresent() ? getBody().get().length : 0)); 359 } 360 361 @Override 362 public boolean equals(@Nullable Object object) { 363 if (this == object) 364 return true; 365 366 if (!(object instanceof Request request)) 367 return false; 368 369 return Objects.equals(getId(), request.getId()) 370 && Objects.equals(getHttpMethod(), request.getHttpMethod()) 371 && Objects.equals(getPath(), request.getPath()) 372 && Objects.equals(getQueryParameters(), request.getQueryParameters()) 373 && Objects.equals(getHeaders(), request.getHeaders()) 374 && Arrays.equals(this.body, request.body) 375 && Objects.equals(isContentTooLarge(), request.isContentTooLarge()); 376 } 377 378 @Override 379 public int hashCode() { 380 return Objects.hash(getId(), getHttpMethod(), getPath(), getQueryParameters(), getHeaders(), Arrays.hashCode(this.body), isContentTooLarge()); 381 } 382 383 private static boolean containsEncodedSlash(@NonNull String rawPath) { 384 requireNonNull(rawPath); 385 return rawPath.toLowerCase(Locale.ROOT).contains("%2f"); 386 } 387 388 /** 389 * An application-specific identifier for this request. 390 * <p> 391 * The identifier is not necessarily unique (for example, numbers that "wrap around" if they get too large). 392 * 393 * @return the request's identifier 394 */ 395 @NonNull 396 public Object getId() { 397 return this.id; 398 } 399 400 /** 401 * The <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods">HTTP method</a> for this request. 402 * 403 * @return the request's HTTP method 404 */ 405 @NonNull 406 public HttpMethod getHttpMethod() { 407 return this.httpMethod; 408 } 409 410 /** 411 * The percent-decoded path component of this request (no query string). 412 * 413 * @return the path for this request 414 */ 415 @NonNull 416 public String getPath() { 417 return this.path; 418 } 419 420 /** 421 * Convenience method to acquire a {@link ResourcePath} representation of {@link #getPath()}. 422 * 423 * @return the resource path for this request 424 */ 425 @NonNull 426 public ResourcePath getResourcePath() { 427 return this.resourcePath; 428 } 429 430 /** 431 * The cookies provided by the client for this request. 432 * <p> 433 * The keys are the {@code Cookie} header names and the values are {@code Cookie} header values 434 * (it is possible for a client to send multiple {@code Cookie} headers with the same name). 435 * <p> 436 * <em>Note that {@code Cookie} headers, like all request headers, have case-insensitive names per the HTTP spec.</em> 437 * <p> 438 * Use {@link #getCookie(String)} for a convenience method to access cookie values when only one is expected. 439 * 440 * @return the request's cookies 441 */ 442 @NonNull 443 public Map<@NonNull String, @NonNull Set<@NonNull String>> getCookies() { 444 Map<String, Set<String>> result = this.cookies; 445 446 if (result == null) { 447 getLock().lock(); 448 449 try { 450 result = this.cookies; 451 452 if (result == null) { 453 result = Collections.unmodifiableMap(Utilities.extractCookiesFromHeaders(getHeaders())); 454 this.cookies = result; 455 } 456 } finally { 457 getLock().unlock(); 458 } 459 } 460 461 return result; 462 } 463 464 /** 465 * The decoded query parameters provided by the client for this request. 466 * <p> 467 * The keys are the query parameter names and the values are query parameter values 468 * (it is possible for a client to send multiple query parameters with the same name, e.g. {@code ?test=1&test=2}). 469 * <p> 470 * <em>Note that query parameters have case-sensitive names per the HTTP spec.</em> 471 * <p> 472 * Use {@link #getQueryParameter(String)} for a convenience method to access query parameter values when only one is expected. 473 * 474 * @return the request's query parameters 475 */ 476 @NonNull 477 public Map<@NonNull String, @NonNull Set<@NonNull String>> getQueryParameters() { 478 return this.queryParameters; 479 } 480 481 /** 482 * The decoded HTML {@code application/x-www-form-urlencoded} form parameters provided by the client for this request. 483 * <p> 484 * The keys are the form parameter names and the values are form parameter values 485 * (it is possible for a client to send multiple form parameters with the same name, e.g. {@code ?test=1&test=2}). 486 * <p> 487 * <em>Note that form parameters have case-sensitive names per the HTTP spec.</em> 488 * <p> 489 * Use {@link #getFormParameter(String)} for a convenience method to access form parameter values when only one is expected. 490 * 491 * @return the request's form parameters 492 */ 493 @NonNull 494 public Map<@NonNull String, @NonNull Set<@NonNull String>> getFormParameters() { 495 Map<String, Set<String>> result = this.formParameters; 496 497 if (result == null) { 498 getLock().lock(); 499 try { 500 result = this.formParameters; 501 502 if (result == null) { 503 if (this.body != null && this.contentType != null && this.contentType.equalsIgnoreCase("application/x-www-form-urlencoded")) { 504 String bodyAsString = getBodyAsString().orElse(null); 505 result = Collections.unmodifiableMap(Utilities.extractQueryParametersFromQuery(bodyAsString, QueryFormat.X_WWW_FORM_URLENCODED, getCharset().orElse(DEFAULT_CHARSET))); 506 } else { 507 result = Map.of(); 508 } 509 510 this.formParameters = result; 511 } 512 } finally { 513 getLock().unlock(); 514 } 515 } 516 517 return result; 518 } 519 520 /** 521 * The raw (un-decoded) path component of this request exactly as the client specified. 522 * <p> 523 * For example, {@code "/a%20b"} (never decoded). 524 * <p> 525 * <em>Note: For requests constructed via {@link #withPath(HttpMethod, String)}, this value is 526 * generated by encoding the decoded path, which may not exactly match the original wire format.</em> 527 * 528 * @return the raw path for this request 529 */ 530 @NonNull 531 public String getRawPath() { 532 return this.rawPath; 533 } 534 535 /** 536 * The raw (un-decoded) query component of this request exactly as the client specified. 537 * <p> 538 * For example, {@code "a=b&c=d+e"} (never decoded). 539 * <p> 540 * This is useful for special cases like HMAC signature verification, which relies on the exact client format. 541 * <p> 542 * <em>Note: For requests constructed via {@link #withPath(HttpMethod, String)}, this value is 543 * generated by encoding the decoded query parameters, which may not exactly match the original wire format.</em> 544 * 545 * @return the raw query for this request, or {@link Optional#empty()} if none was specified 546 */ 547 @NonNull 548 public Optional<String> getRawQuery() { 549 return Optional.ofNullable(this.rawQuery); 550 } 551 552 /** 553 * The raw (un-decoded) path and query components of this request exactly as the client specified. 554 * <p> 555 * For example, {@code "/my%20path?a=b&c=d%20e"} (never decoded). 556 * <p> 557 * <em>Note: For requests constructed via {@link #withPath(HttpMethod, String)}, this value is 558 * generated by encoding the decoded path and query parameters, which may not exactly match the original wire format.</em> 559 * 560 * @return the raw path and query for this request 561 */ 562 @NonNull 563 public String getRawPathAndQuery() { 564 if (this.rawQuery == null) 565 return this.rawPath; 566 567 return this.rawPath + "?" + this.rawQuery; 568 } 569 570 /** 571 * The remote network address for the client connection, if available. 572 * 573 * @return the remote address for this request, or {@link Optional#empty()} if unavailable 574 */ 575 @NonNull 576 public Optional<InetSocketAddress> getRemoteAddress() { 577 return Optional.ofNullable(this.remoteAddress); 578 } 579 580 /** 581 * The headers provided by the client for this request. 582 * <p> 583 * The keys are the header names and the values are header values 584 * (it is possible for a client to send multiple headers with the same name). 585 * <p> 586 * <em>Note that request headers have case-insensitive names per the HTTP spec.</em> 587 * <p> 588 * Use {@link #getHeader(String)} for a convenience method to access header values when only one is expected. 589 * 590 * @return the request's headers 591 */ 592 @NonNull 593 public Map<@NonNull String, @NonNull Set<@NonNull String>> getHeaders() { 594 return this.headers; 595 } 596 597 /** 598 * The {@code Content-Type} header value, as specified by the client. 599 * 600 * @return the request's {@code Content-Type} header value, or {@link Optional#empty()} if not specified 601 */ 602 @NonNull 603 public Optional<String> getContentType() { 604 return Optional.ofNullable(this.contentType); 605 } 606 607 /** 608 * The request's character encoding, as specified by the client in the {@code Content-Type} header value. 609 * 610 * @return the request's character encoding, or {@link Optional#empty()} if not specified 611 */ 612 @NonNull 613 public Optional<Charset> getCharset() { 614 return Optional.ofNullable(this.charset); 615 } 616 617 /** 618 * Is this a request with {@code Content-Type} of {@code multipart/form-data}? 619 * 620 * @return {@code true} if this is a {@code multipart/form-data} request, {@code false} otherwise 621 */ 622 @NonNull 623 public Boolean isMultipart() { 624 return this.multipart; 625 } 626 627 /** 628 * The decoded HTML {@code multipart/form-data} fields provided by the client for this request. 629 * <p> 630 * The keys are the multipart field names and the values are multipart field values 631 * (it is possible for a client to send multiple multipart fields with the same name). 632 * <p> 633 * <em>Note that multipart fields have case-sensitive names per the HTTP spec.</em> 634 * <p> 635 * Use {@link #getMultipartField(String)} for a convenience method to access a multipart parameter field value when only one is expected. 636 * <p> 637 * When using Soklet's default {@link Server}, multipart fields are parsed using the {@link MultipartParser} as configured by {@link Server.Builder#multipartParser(MultipartParser)}. 638 * 639 * @return the request's multipart fields, or the empty map if none are present 640 */ 641 @NonNull 642 public Map<@NonNull String, @NonNull Set<@NonNull MultipartField>> getMultipartFields() { 643 if (!isMultipart()) 644 return Map.of(); 645 646 Map<String, Set<MultipartField>> result = this.multipartFields; 647 648 if (result == null) { 649 getLock().lock(); 650 try { 651 result = this.multipartFields; 652 653 if (result == null) { 654 result = Collections.unmodifiableMap(getMultipartParser().extractMultipartFields(this)); 655 this.multipartFields = result; 656 } 657 } finally { 658 getLock().unlock(); 659 } 660 } 661 662 return result; 663 } 664 665 /** 666 * The raw bytes of the request body - <strong>callers should not modify this array; it is not defensively copied for performance reasons</strong>. 667 * <p> 668 * For convenience, {@link #getBodyAsString()} is available if you expect your request body to be of type {@link String}. 669 * 670 * @return the request body bytes, or {@link Optional#empty()} if none was supplied 671 */ 672 @NonNull 673 public Optional<byte[]> getBody() { 674 return Optional.ofNullable(this.body); 675 676 // Note: it would be nice to defensively copy, but it's inefficient 677 // return Optional.ofNullable(this.body == null ? null : Arrays.copyOf(this.body, this.body.length)); 678 } 679 680 /** 681 * Was this request too large for the server to handle? 682 * <p> 683 * <em>If so, this request might have incomplete sets of headers/cookies. It will always have a zero-length body.</em> 684 * <p> 685 * 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. 686 * <p> 687 * When using Soklet's default {@link Server}, maximum request size is configured by {@link Server.Builder#maximumRequestSizeInBytes(Integer)}. 688 * 689 * @return {@code true} if this request is larger than the server is able to handle, {@code false} otherwise 690 */ 691 @NonNull 692 public Boolean isContentTooLarge() { 693 return this.contentTooLarge; 694 } 695 696 /** 697 * Convenience method that provides the {@link #getBody()} bytes as a {@link String} encoded using the client-specified character set per {@link #getCharset()}. 698 * <p> 699 * If no character set is specified, {@link StandardCharsets#UTF_8} is used to perform the encoding. 700 * <p> 701 * 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. 702 * <p> 703 * This method is threadsafe. 704 * 705 * @return a {@link String} representation of this request's body, or {@link Optional#empty()} if no request body was specified by the client 706 */ 707 @NonNull 708 public Optional<String> getBodyAsString() { 709 // Lazily instantiate a string instance using double-checked locking 710 String result = this.bodyAsString; 711 712 if (this.body != null && result == null) { 713 getLock().lock(); 714 715 try { 716 result = this.bodyAsString; 717 718 if (this.body != null && result == null) { 719 result = new String(this.body, getCharset().orElse(DEFAULT_CHARSET)); 720 this.bodyAsString = result; 721 } 722 } finally { 723 getLock().unlock(); 724 } 725 } 726 727 return Optional.ofNullable(result); 728 } 729 730 /** 731 * <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">Non-preflight CORS</a> request data. 732 * <p> 733 * See <a href="https://www.soklet.com/docs/cors">https://www.soklet.com/docs/cors</a> for details. 734 * 735 * @return non-preflight CORS request data, or {@link Optional#empty()} if none was specified 736 */ 737 @NonNull 738 public Optional<Cors> getCors() { 739 return Optional.ofNullable(this.cors); 740 } 741 742 /** 743 * <a href="https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request">CORS preflight</a>-related request data. 744 * <p> 745 * See <a href="https://www.soklet.com/docs/cors">https://www.soklet.com/docs/cors</a> for details. 746 * 747 * @return preflight CORS request data, or {@link Optional#empty()} if none was specified 748 */ 749 @NonNull 750 public Optional<CorsPreflight> getCorsPreflight() { 751 return Optional.ofNullable(this.corsPreflight); 752 } 753 754 /** 755 * 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>. 756 * <p> 757 * 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. 758 * <p> 759 * This method is threadsafe. 760 * <p> 761 * See {@link #getLanguageRanges()} for a variant that pulls {@link LanguageRange} values. 762 * 763 * @return locale information for this request, or the empty list if none was specified 764 */ 765 @NonNull 766 public List<@NonNull Locale> getLocales() { 767 // Lazily instantiate our parsed locales using double-checked locking 768 List<Locale> result = this.locales; 769 770 if (result == null) { 771 getLock().lock(); 772 773 try { 774 result = this.locales; 775 776 if (result == null) { 777 Set<String> acceptLanguageHeaderValues = getHeaders().get("Accept-Language"); 778 779 if (acceptLanguageHeaderValues != null && !acceptLanguageHeaderValues.isEmpty()) { 780 // Support data spread across multiple header lines, which spec allows 781 String acceptLanguageHeaderValue = acceptLanguageHeaderValues.stream() 782 .filter(value -> trimAggressivelyToEmpty(value).length() > 0) 783 .collect(Collectors.joining(",")); 784 785 try { 786 result = unmodifiableList(Utilities.extractLocalesFromAcceptLanguageHeaderValue(acceptLanguageHeaderValue)); 787 } catch (Exception ignored) { 788 // Malformed Accept-Language header; ignore it 789 result = List.of(); 790 } 791 } else { 792 result = List.of(); 793 } 794 795 this.locales = result; 796 } 797 } finally { 798 getLock().unlock(); 799 } 800 } 801 802 return result; 803 } 804 805 /** 806 * {@link LanguageRange} information for this request as specified by {@code Accept-Language} header value[s]. 807 * <p> 808 * 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. 809 * <p> 810 * This method is threadsafe. 811 * <p> 812 * See {@link #getLocales()} for a variant that pulls {@link Locale} values. 813 * 814 * @return language range information for this request, or the empty list if none was specified 815 */ 816 @NonNull 817 public List<@NonNull LanguageRange> getLanguageRanges() { 818 // Lazily instantiate our parsed language ranges using double-checked locking 819 List<LanguageRange> result = this.languageRanges; 820 821 if (result == null) { 822 getLock().lock(); 823 try { 824 result = this.languageRanges; 825 826 if (result == null) { 827 Set<String> acceptLanguageHeaderValues = getHeaders().get("Accept-Language"); 828 829 if (acceptLanguageHeaderValues != null && !acceptLanguageHeaderValues.isEmpty()) { 830 // Support data spread across multiple header lines, which spec allows 831 String acceptLanguageHeaderValue = acceptLanguageHeaderValues.stream() 832 .filter(value -> trimAggressivelyToEmpty(value).length() > 0) 833 .collect(Collectors.joining(",")); 834 835 try { 836 result = Collections.unmodifiableList(LanguageRange.parse(acceptLanguageHeaderValue)); 837 } catch (Exception ignored) { 838 // Malformed Accept-Language header; ignore it 839 result = List.of(); 840 } 841 } else { 842 result = List.of(); 843 } 844 845 this.languageRanges = result; 846 } 847 } finally { 848 getLock().unlock(); 849 } 850 } 851 852 return result; 853 } 854 855 /** 856 * Convenience method to access a decoded query parameter's value when at most one is expected for the given {@code name}. 857 * <p> 858 * If a query parameter {@code name} can support multiple values, {@link #getQueryParameters()} should be used instead of this method. 859 * <p> 860 * If this method is invoked for a query parameter {@code name} with multiple values, Soklet will throw {@link IllegalQueryParameterException}. 861 * <p> 862 * <em>Note that query parameters have case-sensitive names per the HTTP spec.</em> 863 * 864 * @param name the name of the query parameter 865 * @return the value for the query parameter, or {@link Optional#empty()} if none is present 866 * @throws IllegalQueryParameterException if the query parameter with the given {@code name} has multiple values 867 */ 868 @NonNull 869 public Optional<String> getQueryParameter(@NonNull String name) { 870 requireNonNull(name); 871 872 try { 873 return singleValueForName(name, getQueryParameters()); 874 } catch (MultipleValuesException e) { 875 @SuppressWarnings("unchecked") 876 String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", "))); 877 throw new IllegalQueryParameterException(format("Multiple values specified for query parameter '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString); 878 } 879 } 880 881 /** 882 * Convenience method to access a decoded form parameter's value when at most one is expected for the given {@code name}. 883 * <p> 884 * If a form parameter {@code name} can support multiple values, {@link #getFormParameters()} should be used instead of this method. 885 * <p> 886 * If this method is invoked for a form parameter {@code name} with multiple values, Soklet will throw {@link IllegalFormParameterException}. 887 * <p> 888 * <em>Note that form parameters have case-sensitive names per the HTTP spec.</em> 889 * 890 * @param name the name of the form parameter 891 * @return the value for the form parameter, or {@link Optional#empty()} if none is present 892 * @throws IllegalFormParameterException if the form parameter with the given {@code name} has multiple values 893 */ 894 @NonNull 895 public Optional<String> getFormParameter(@NonNull String name) { 896 requireNonNull(name); 897 898 try { 899 return singleValueForName(name, getFormParameters()); 900 } catch (MultipleValuesException e) { 901 @SuppressWarnings("unchecked") 902 String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", "))); 903 throw new IllegalFormParameterException(format("Multiple values specified for form parameter '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString); 904 } 905 } 906 907 /** 908 * Convenience method to access a header's value when at most one is expected for the given {@code name}. 909 * <p> 910 * If a header {@code name} can support multiple values, {@link #getHeaders()} should be used instead of this method. 911 * <p> 912 * If this method is invoked for a header {@code name} with multiple values, Soklet will throw {@link IllegalRequestHeaderException}. 913 * <p> 914 * <em>Note that request headers have case-insensitive names per the HTTP spec.</em> 915 * 916 * @param name the name of the header 917 * @return the value for the header, or {@link Optional#empty()} if none is present 918 * @throws IllegalRequestHeaderException if the header with the given {@code name} has multiple values 919 */ 920 @NonNull 921 public Optional<String> getHeader(@NonNull String name) { 922 requireNonNull(name); 923 924 try { 925 return singleValueForName(name, getHeaders()); 926 } catch (MultipleValuesException e) { 927 @SuppressWarnings("unchecked") 928 String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", "))); 929 throw new IllegalRequestHeaderException(format("Multiple values specified for request header '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString); 930 } 931 } 932 933 /** 934 * Convenience method to access a cookie's value when at most one is expected for the given {@code name}. 935 * <p> 936 * If a cookie {@code name} can support multiple values, {@link #getCookies()} should be used instead of this method. 937 * <p> 938 * If this method is invoked for a cookie {@code name} with multiple values, Soklet will throw {@link IllegalRequestCookieException}. 939 * <p> 940 * <em>Note that {@code Cookie} headers, like all request headers, have case-insensitive names per the HTTP spec.</em> 941 * 942 * @param name the name of the cookie 943 * @return the value for the cookie, or {@link Optional#empty()} if none is present 944 * @throws IllegalRequestCookieException if the cookie with the given {@code name} has multiple values 945 */ 946 @NonNull 947 public Optional<String> getCookie(@NonNull String name) { 948 requireNonNull(name); 949 950 try { 951 return singleValueForName(name, getCookies()); 952 } catch (MultipleValuesException e) { 953 @SuppressWarnings("unchecked") 954 String valuesAsString = format("[%s]", ((Set<String>) e.getValues()).stream().collect(Collectors.joining(", "))); 955 throw new IllegalRequestCookieException(format("Multiple values specified for request cookie '%s' (but expected single value): %s", name, valuesAsString), name, valuesAsString); 956 } 957 } 958 959 /** 960 * Convenience method to access a decoded multipart field when at most one is expected for the given {@code name}. 961 * <p> 962 * If a {@code name} can support multiple multipart fields, {@link #getMultipartFields()} should be used instead of this method. 963 * <p> 964 * If this method is invoked for a {@code name} with multiple multipart field values, Soklet will throw {@link IllegalMultipartFieldException}. 965 * <p> 966 * <em>Note that multipart fields have case-sensitive names per the HTTP spec.</em> 967 * 968 * @param name the name of the multipart field 969 * @return the multipart field value, or {@link Optional#empty()} if none is present 970 * @throws IllegalMultipartFieldException if the multipart field with the given {@code name} has multiple values 971 */ 972 @NonNull 973 public Optional<MultipartField> getMultipartField(@NonNull String name) { 974 requireNonNull(name); 975 976 try { 977 return singleValueForName(name, getMultipartFields()); 978 } catch (MultipleValuesException e) { 979 @SuppressWarnings("unchecked") 980 MultipartField firstMultipartField = getMultipartFields().get(name).stream().findFirst().get(); 981 String valuesAsString = format("[%s]", e.getValues().stream() 982 .map(multipartField -> multipartField.toString()) 983 .collect(Collectors.joining(", "))); 984 985 throw new IllegalMultipartFieldException(format("Multiple values specified for multipart field '%s' (but expected single value): %s", name, valuesAsString), firstMultipartField); 986 } 987 } 988 989 @NonNull 990 private MultipartParser getMultipartParser() { 991 return this.multipartParser; 992 } 993 994 @NonNull 995 private IdGenerator<?> getIdGenerator() { 996 return this.idGenerator; 997 } 998 999 @NonNull 1000 private ReentrantLock getLock() { 1001 return this.lock; 1002 } 1003 1004 @NonNull 1005 private <T> Optional<T> singleValueForName(@NonNull String name, 1006 @Nullable Map<String, Set<T>> valuesByName) throws MultipleValuesException { 1007 if (valuesByName == null) 1008 return Optional.empty(); 1009 1010 Set<T> values = valuesByName.get(name); 1011 1012 if (values == null) 1013 return Optional.empty(); 1014 1015 if (values.size() > 1) 1016 throw new MultipleValuesException(name, values); 1017 1018 return values.stream().findFirst(); 1019 } 1020 1021 @NotThreadSafe 1022 private static class MultipleValuesException extends Exception { 1023 @NonNull 1024 private final String name; 1025 @NonNull 1026 private final Set<?> values; 1027 1028 private MultipleValuesException(@NonNull String name, 1029 @NonNull Set<?> values) { 1030 super(format("Expected single value but found %d values for '%s': %s", values.size(), name, values)); 1031 1032 requireNonNull(name); 1033 requireNonNull(values); 1034 1035 this.name = name; 1036 this.values = Collections.unmodifiableSet(new LinkedHashSet<>(values)); 1037 } 1038 1039 @NonNull 1040 public String getName() { 1041 return this.name; 1042 } 1043 1044 @NonNull 1045 public Set<?> getValues() { 1046 return this.values; 1047 } 1048 } 1049 1050 /** 1051 * Builder used to construct instances of {@link Request} via {@link Request#withRawUrl(HttpMethod, String)}. 1052 * <p> 1053 * This class is intended for use by a single thread. 1054 * 1055 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1056 */ 1057 @NotThreadSafe 1058 public static final class RawBuilder { 1059 @NonNull 1060 private HttpMethod httpMethod; 1061 @NonNull 1062 private String rawUrl; 1063 @Nullable 1064 private Object id; 1065 @Nullable 1066 private IdGenerator idGenerator; 1067 @Nullable 1068 private MultipartParser multipartParser; 1069 @Nullable 1070 private Map<@NonNull String, @NonNull Set<@NonNull String>> headers; 1071 @Nullable 1072 private InetSocketAddress remoteAddress; 1073 @Nullable 1074 private byte[] body; 1075 @Nullable 1076 private Boolean contentTooLarge; 1077 1078 protected RawBuilder(@NonNull HttpMethod httpMethod, 1079 @NonNull String rawUrl) { 1080 requireNonNull(httpMethod); 1081 requireNonNull(rawUrl); 1082 1083 this.httpMethod = httpMethod; 1084 this.rawUrl = rawUrl; 1085 } 1086 1087 @NonNull 1088 public RawBuilder httpMethod(@NonNull HttpMethod httpMethod) { 1089 requireNonNull(httpMethod); 1090 this.httpMethod = httpMethod; 1091 return this; 1092 } 1093 1094 @NonNull 1095 public RawBuilder rawUrl(@NonNull String rawUrl) { 1096 requireNonNull(rawUrl); 1097 this.rawUrl = rawUrl; 1098 return this; 1099 } 1100 1101 @NonNull 1102 public RawBuilder id(@Nullable Object id) { 1103 this.id = id; 1104 return this; 1105 } 1106 1107 @NonNull 1108 public RawBuilder idGenerator(@Nullable IdGenerator idGenerator) { 1109 this.idGenerator = idGenerator; 1110 return this; 1111 } 1112 1113 @NonNull 1114 public RawBuilder multipartParser(@Nullable MultipartParser multipartParser) { 1115 this.multipartParser = multipartParser; 1116 return this; 1117 } 1118 1119 @NonNull 1120 public RawBuilder headers(@Nullable Map<@NonNull String, @NonNull Set<@NonNull String>> headers) { 1121 this.headers = headers; 1122 return this; 1123 } 1124 1125 @NonNull 1126 public RawBuilder remoteAddress(@Nullable InetSocketAddress remoteAddress) { 1127 this.remoteAddress = remoteAddress; 1128 return this; 1129 } 1130 1131 @NonNull 1132 public RawBuilder body(@Nullable byte[] body) { 1133 this.body = body; 1134 return this; 1135 } 1136 1137 @NonNull 1138 public RawBuilder contentTooLarge(@Nullable Boolean contentTooLarge) { 1139 this.contentTooLarge = contentTooLarge; 1140 return this; 1141 } 1142 1143 @NonNull 1144 public Request build() { 1145 return new Request(this, null); 1146 } 1147 } 1148 1149 /** 1150 * Builder used to construct instances of {@link Request} via {@link Request#withPath(HttpMethod, String)}. 1151 * <p> 1152 * This class is intended for use by a single thread. 1153 * 1154 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1155 */ 1156 @NotThreadSafe 1157 public static final class PathBuilder { 1158 @NonNull 1159 private HttpMethod httpMethod; 1160 @NonNull 1161 private String path; 1162 @Nullable 1163 private String rawPath; 1164 @Nullable 1165 private String rawQuery; 1166 @Nullable 1167 private Object id; 1168 @Nullable 1169 private IdGenerator idGenerator; 1170 @Nullable 1171 private MultipartParser multipartParser; 1172 @Nullable 1173 private Map<@NonNull String, @NonNull Set<@NonNull String>> queryParameters; 1174 @Nullable 1175 private Map<@NonNull String, @NonNull Set<@NonNull String>> headers; 1176 @Nullable 1177 private InetSocketAddress remoteAddress; 1178 @Nullable 1179 private byte[] body; 1180 @Nullable 1181 private Boolean contentTooLarge; 1182 1183 protected PathBuilder(@NonNull HttpMethod httpMethod, 1184 @NonNull String path) { 1185 requireNonNull(httpMethod); 1186 requireNonNull(path); 1187 1188 this.httpMethod = httpMethod; 1189 this.path = path; 1190 } 1191 1192 @NonNull 1193 public PathBuilder httpMethod(@NonNull HttpMethod httpMethod) { 1194 requireNonNull(httpMethod); 1195 this.httpMethod = httpMethod; 1196 return this; 1197 } 1198 1199 @NonNull 1200 public PathBuilder path(@NonNull String path) { 1201 requireNonNull(path); 1202 this.path = path; 1203 return this; 1204 } 1205 1206 // Package-private setter for raw value (used by Copier) 1207 @NonNull 1208 PathBuilder rawPath(@Nullable String rawPath) { 1209 this.rawPath = rawPath; 1210 return this; 1211 } 1212 1213 // Package-private setter for raw value (used by Copier) 1214 @NonNull 1215 PathBuilder rawQuery(@Nullable String rawQuery) { 1216 this.rawQuery = rawQuery; 1217 return this; 1218 } 1219 1220 @NonNull 1221 public PathBuilder id(@Nullable Object id) { 1222 this.id = id; 1223 return this; 1224 } 1225 1226 @NonNull 1227 public PathBuilder idGenerator(@Nullable IdGenerator idGenerator) { 1228 this.idGenerator = idGenerator; 1229 return this; 1230 } 1231 1232 @NonNull 1233 public PathBuilder multipartParser(@Nullable MultipartParser multipartParser) { 1234 this.multipartParser = multipartParser; 1235 return this; 1236 } 1237 1238 @NonNull 1239 public PathBuilder queryParameters(@Nullable Map<@NonNull String, @NonNull Set<@NonNull String>> queryParameters) { 1240 this.queryParameters = queryParameters; 1241 return this; 1242 } 1243 1244 @NonNull 1245 public PathBuilder headers(@Nullable Map<@NonNull String, @NonNull Set<@NonNull String>> headers) { 1246 this.headers = headers; 1247 return this; 1248 } 1249 1250 @NonNull 1251 public PathBuilder remoteAddress(@Nullable InetSocketAddress remoteAddress) { 1252 this.remoteAddress = remoteAddress; 1253 return this; 1254 } 1255 1256 @NonNull 1257 public PathBuilder body(@Nullable byte[] body) { 1258 this.body = body; 1259 return this; 1260 } 1261 1262 @NonNull 1263 public PathBuilder contentTooLarge(@Nullable Boolean contentTooLarge) { 1264 this.contentTooLarge = contentTooLarge; 1265 return this; 1266 } 1267 1268 @NonNull 1269 public Request build() { 1270 return new Request(null, this); 1271 } 1272 } 1273 1274 /** 1275 * Builder used to copy instances of {@link Request} via {@link Request#copy()}. 1276 * <p> 1277 * This class is intended for use by a single thread. 1278 * 1279 * @author <a href="https://www.revetkn.com">Mark Allen</a> 1280 */ 1281 @NotThreadSafe 1282 public static final class Copier { 1283 @NonNull 1284 private final PathBuilder builder; 1285 1286 // Track original raw values and modification state 1287 @Nullable 1288 private String originalRawPath; 1289 @Nullable 1290 private String originalRawQuery; 1291 @Nullable 1292 private InetSocketAddress originalRemoteAddress; 1293 @NonNull 1294 private Boolean pathModified = false; 1295 @NonNull 1296 private Boolean queryParametersModified = false; 1297 1298 Copier(@NonNull Request request) { 1299 requireNonNull(request); 1300 1301 this.originalRawPath = request.getRawPath(); 1302 this.originalRawQuery = request.rawQuery; // Direct field access 1303 this.originalRemoteAddress = request.getRemoteAddress().orElse(null); 1304 1305 this.builder = new PathBuilder(request.getHttpMethod(), request.getPath()) 1306 .id(request.getId()) 1307 .queryParameters(new LinkedHashMap<>(request.getQueryParameters())) 1308 .headers(new LinkedCaseInsensitiveMap<>(request.getHeaders())) 1309 .body(request.body) // Direct field access to avoid array copy 1310 .multipartParser(request.getMultipartParser()) 1311 .idGenerator(request.getIdGenerator()) 1312 .contentTooLarge(request.isContentTooLarge()) 1313 .remoteAddress(this.originalRemoteAddress) 1314 // Preserve original raw values initially 1315 .rawPath(this.originalRawPath) 1316 .rawQuery(this.originalRawQuery); 1317 } 1318 1319 @NonNull 1320 public Copier httpMethod(@NonNull HttpMethod httpMethod) { 1321 requireNonNull(httpMethod); 1322 this.builder.httpMethod(httpMethod); 1323 return this; 1324 } 1325 1326 @NonNull 1327 public Copier path(@NonNull String path) { 1328 requireNonNull(path); 1329 this.builder.path(path); 1330 this.pathModified = true; 1331 // Clear preserved raw path since decoded path changed 1332 this.builder.rawPath(null); 1333 return this; 1334 } 1335 1336 @NonNull 1337 public Copier id(@Nullable Object id) { 1338 this.builder.id(id); 1339 return this; 1340 } 1341 1342 @NonNull 1343 public Copier queryParameters(@Nullable Map<@NonNull String, @NonNull Set<@NonNull String>> queryParameters) { 1344 this.builder.queryParameters(queryParameters); 1345 this.queryParametersModified = true; 1346 // Clear preserved raw query since decoded query parameters changed 1347 this.builder.rawQuery(null); 1348 return this; 1349 } 1350 1351 // Convenience method for mutation 1352 @NonNull 1353 public Copier queryParameters(@NonNull Consumer<Map<@NonNull String, @NonNull Set<@NonNull String>>> queryParametersConsumer) { 1354 requireNonNull(queryParametersConsumer); 1355 1356 if (this.builder.queryParameters == null) 1357 this.builder.queryParameters(new LinkedHashMap<>()); 1358 1359 queryParametersConsumer.accept(this.builder.queryParameters); 1360 this.queryParametersModified = true; 1361 // Clear preserved raw query since decoded query parameters changed 1362 this.builder.rawQuery(null); 1363 return this; 1364 } 1365 1366 @NonNull 1367 public Copier headers(@Nullable Map<@NonNull String, @NonNull Set<@NonNull String>> headers) { 1368 this.builder.headers(headers); 1369 return this; 1370 } 1371 1372 @NonNull 1373 public Copier remoteAddress(@Nullable InetSocketAddress remoteAddress) { 1374 this.builder.remoteAddress(remoteAddress); 1375 return this; 1376 } 1377 1378 // Convenience method for mutation 1379 @NonNull 1380 public Copier headers(@NonNull Consumer<Map<@NonNull String, @NonNull Set<@NonNull String>>> headersConsumer) { 1381 requireNonNull(headersConsumer); 1382 1383 if (this.builder.headers == null) 1384 this.builder.headers(new LinkedCaseInsensitiveMap<>()); 1385 1386 headersConsumer.accept(this.builder.headers); 1387 return this; 1388 } 1389 1390 @NonNull 1391 public Copier body(@Nullable byte[] body) { 1392 this.builder.body(body); 1393 return this; 1394 } 1395 1396 @NonNull 1397 public Copier contentTooLarge(@Nullable Boolean contentTooLarge) { 1398 this.builder.contentTooLarge(contentTooLarge); 1399 return this; 1400 } 1401 1402 @NonNull 1403 public Request finish() { 1404 if (this.queryParametersModified) { 1405 Map<String, Set<String>> queryParameters = this.builder.queryParameters; 1406 1407 if (queryParameters == null || queryParameters.isEmpty()) { 1408 this.builder.rawQuery(null); 1409 } else { 1410 this.builder.rawQuery(Utilities.encodeQueryParameters(queryParameters, QueryFormat.RFC_3986_STRICT)); 1411 } 1412 } 1413 1414 return this.builder.build(); 1415 } 1416 } 1417}