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