001/* 002 * Copyright 2022-2025 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.soklet.core.impl; 018 019import com.soklet.SokletConfiguration; 020import com.soklet.core.HttpMethod; 021import com.soklet.core.LifecycleInterceptor; 022import com.soklet.core.LogEvent; 023import com.soklet.core.LogEventType; 024import com.soklet.core.MarshaledResponse; 025import com.soklet.core.MultipartParser; 026import com.soklet.core.Request; 027import com.soklet.core.ResponseCookie; 028import com.soklet.core.Server; 029import com.soklet.core.StatusCode; 030import com.soklet.core.Utilities; 031import com.soklet.internal.microhttp.EventLoop; 032import com.soklet.internal.microhttp.Handler; 033import com.soklet.internal.microhttp.Header; 034import com.soklet.internal.microhttp.LogEntry; 035import com.soklet.internal.microhttp.Logger; 036import com.soklet.internal.microhttp.MicrohttpRequest; 037import com.soklet.internal.microhttp.MicrohttpResponse; 038import com.soklet.internal.microhttp.Options; 039import com.soklet.internal.microhttp.OptionsBuilder; 040import com.soklet.internal.spring.LinkedCaseInsensitiveMap; 041 042import javax.annotation.Nonnull; 043import javax.annotation.Nullable; 044import javax.annotation.concurrent.NotThreadSafe; 045import javax.annotation.concurrent.ThreadSafe; 046import java.io.IOException; 047import java.io.UncheckedIOException; 048import java.nio.charset.Charset; 049import java.nio.charset.StandardCharsets; 050import java.time.Duration; 051import java.util.ArrayList; 052import java.util.Comparator; 053import java.util.LinkedHashSet; 054import java.util.List; 055import java.util.Map; 056import java.util.Optional; 057import java.util.Set; 058import java.util.SortedSet; 059import java.util.TreeSet; 060import java.util.concurrent.ExecutorService; 061import java.util.concurrent.Executors; 062import java.util.concurrent.ThreadFactory; 063import java.util.concurrent.TimeUnit; 064import java.util.concurrent.atomic.AtomicBoolean; 065import java.util.concurrent.atomic.AtomicInteger; 066import java.util.concurrent.locks.ReentrantLock; 067import java.util.function.Supplier; 068 069import static com.soklet.core.Utilities.emptyByteArray; 070import static com.soklet.core.Utilities.trimAggressivelyToNull; 071import static java.lang.String.format; 072import static java.util.Locale.ENGLISH; 073import static java.util.Objects.requireNonNull; 074 075/** 076 * @author <a href="https://www.revetkn.com">Mark Allen</a> 077 */ 078@ThreadSafe 079public class DefaultServer implements Server { 080 @Nonnull 081 private static final String DEFAULT_HOST; 082 @Nonnull 083 private static final Integer DEFAULT_CONCURRENCY; 084 @Nonnull 085 private static final Duration DEFAULT_REQUEST_TIMEOUT; 086 @Nonnull 087 private static final Duration DEFAULT_SOCKET_SELECT_TIMEOUT; 088 @Nonnull 089 private static final Integer DEFAULT_MAXIMUM_REQUEST_SIZE_IN_BYTES; 090 @Nonnull 091 private static final Integer DEFAULT_REQUEST_READ_BUFFER_SIZE_IN_BYTES; 092 @Nonnull 093 private static final Integer DEFAULT_SOCKET_PENDING_CONNECTION_LIMIT; 094 @Nonnull 095 private static final Duration DEFAULT_SHUTDOWN_TIMEOUT; 096 097 static { 098 DEFAULT_HOST = "0.0.0.0"; 099 DEFAULT_CONCURRENCY = Runtime.getRuntime().availableProcessors(); 100 DEFAULT_REQUEST_TIMEOUT = Duration.ofSeconds(60); 101 DEFAULT_SOCKET_SELECT_TIMEOUT = Duration.ofMillis(100); 102 DEFAULT_MAXIMUM_REQUEST_SIZE_IN_BYTES = 1_024 * 1_024 * 10; 103 DEFAULT_REQUEST_READ_BUFFER_SIZE_IN_BYTES = 1_024 * 64; 104 DEFAULT_SOCKET_PENDING_CONNECTION_LIMIT = 0; 105 DEFAULT_SHUTDOWN_TIMEOUT = Duration.ofSeconds(5); 106 } 107 108 @Nonnull 109 private final Integer port; 110 @Nonnull 111 private final String host; 112 @Nonnull 113 private final Integer concurrency; 114 @Nonnull 115 private final Duration requestTimeout; 116 @Nonnull 117 private final Duration socketSelectTimeout; 118 @Nonnull 119 private final Duration shutdownTimeout; 120 @Nonnull 121 private final Integer maximumRequestSizeInBytes; 122 @Nonnull 123 private final Integer requestReadBufferSizeInBytes; 124 @Nonnull 125 private final Integer socketPendingConnectionLimit; 126 @Nonnull 127 private final MultipartParser multipartParser; 128 @Nonnull 129 private final ReentrantLock lock; 130 @Nonnull 131 private final Supplier<ExecutorService> requestHandlerExecutorServiceSupplier; 132 @Nullable 133 private volatile ExecutorService requestHandlerExecutorService; 134 @Nullable 135 private volatile RequestHandler requestHandler; 136 @Nullable 137 private volatile LifecycleInterceptor lifecycleInterceptor; 138 @Nullable 139 private volatile EventLoop eventLoop; 140 141 @Nonnull 142 public static Builder withPort(@Nonnull Integer port) { 143 requireNonNull(port); 144 return new Builder(port); 145 } 146 147 protected DefaultServer(@Nonnull Builder builder) { 148 requireNonNull(builder); 149 150 this.lock = new ReentrantLock(); 151 152 this.port = builder.port; 153 this.host = builder.host != null ? builder.host : DEFAULT_HOST; 154 this.concurrency = builder.concurrency != null ? builder.concurrency : DEFAULT_CONCURRENCY; 155 this.maximumRequestSizeInBytes = builder.maximumRequestSizeInBytes != null ? builder.maximumRequestSizeInBytes : DEFAULT_MAXIMUM_REQUEST_SIZE_IN_BYTES; 156 this.requestReadBufferSizeInBytes = builder.requestReadBufferSizeInBytes != null ? builder.requestReadBufferSizeInBytes : DEFAULT_REQUEST_READ_BUFFER_SIZE_IN_BYTES; 157 this.requestTimeout = builder.requestTimeout != null ? builder.requestTimeout : DEFAULT_REQUEST_TIMEOUT; 158 this.socketSelectTimeout = builder.socketSelectTimeout != null ? builder.socketSelectTimeout : DEFAULT_SOCKET_SELECT_TIMEOUT; 159 this.socketPendingConnectionLimit = builder.socketPendingConnectionLimit != null ? builder.socketPendingConnectionLimit : DEFAULT_SOCKET_PENDING_CONNECTION_LIMIT; 160 this.shutdownTimeout = builder.shutdownTimeout != null ? builder.shutdownTimeout : DEFAULT_SHUTDOWN_TIMEOUT; 161 this.multipartParser = builder.multipartParser != null ? builder.multipartParser : DefaultMultipartParser.sharedInstance(); 162 this.requestHandlerExecutorServiceSupplier = builder.requestHandlerExecutorServiceSupplier != null ? builder.requestHandlerExecutorServiceSupplier : () -> { 163 String threadNamePrefix = "request-handler-"; 164 165 if (Utilities.virtualThreadsAvailable()) 166 return Utilities.createVirtualThreadsNewThreadPerTaskExecutor(threadNamePrefix, (Thread thread, Throwable throwable) -> { 167 safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "Unexpected exception occurred during server HTTP request processing") 168 .throwable(throwable) 169 .build()); 170 }); 171 172 return Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new NonvirtualThreadFactory(threadNamePrefix)); 173 }; 174 } 175 176 @Override 177 public void start() { 178 getLock().lock(); 179 180 try { 181 if (isStarted()) 182 return; 183 184 Options options = OptionsBuilder.newBuilder() 185 .withHost(getHost()) 186 .withPort(getPort()) 187 .withConcurrency(getConcurrency()) 188 .withRequestTimeout(getRequestTimeout()) 189 .withResolution(getSocketSelectTimeout()) 190 .withReadBufferSize(getRequestReadBufferSizeInBytes()) 191 .withMaxRequestSize(getMaximumRequestSizeInBytes()) 192 .withAcceptLength(getSocketPendingConnectionLimit()) 193 .build(); 194 195 Logger logger = new Logger() { 196 @Override 197 public boolean enabled() { 198 return false; 199 } 200 201 @Override 202 public void log(@Nullable LogEntry... logEntries) { 203 // No-op 204 } 205 206 @Override 207 public void log(@Nullable Exception e, 208 @Nullable LogEntry... logEntries) { 209 // No-op 210 } 211 }; 212 213 Handler handler = ((microhttpRequest, microHttpCallback) -> { 214 ExecutorService requestHandlerExecutorServiceReference = this.requestHandlerExecutorService; 215 216 if (requestHandlerExecutorServiceReference == null) 217 return; 218 219 requestHandlerExecutorServiceReference.submit(() -> { 220 RequestHandler requestHandler = getRequestHandler().orElse(null); 221 222 if (requestHandler == null) 223 return; 224 225 AtomicBoolean shouldWriteFailsafeResponse = new AtomicBoolean(true); 226 227 try { 228 // Normalize body 229 byte[] body = microhttpRequest.body(); 230 231 if (body != null && body.length == 0) 232 body = null; 233 234 // Special case: look for a poison-pill header that indicates "content too large", 235 // make a note of it, and then remove it from the request. 236 // This header is specially set for Soklet inside of Microhttp's connection event loop. 237 Map<String, Set<String>> headers = headersFromMicrohttpRequest(microhttpRequest); 238 boolean contentTooLarge = false; 239 240 for (Header header : microhttpRequest.headers()) { 241 if (header.name().equals("com.soklet.CONTENT_TOO_LARGE")) { 242 headers.remove("com.soklet.CONTENT_TOO_LARGE"); 243 contentTooLarge = true; 244 } 245 } 246 247 Request request = Request.with(HttpMethod.valueOf(microhttpRequest.method().toUpperCase(ENGLISH)), microhttpRequest.uri()) 248 .multipartParser(getMultipartParser()) 249 .headers(headers) 250 .body(body) 251 .contentTooLarge(contentTooLarge) 252 .build(); 253 254 requestHandler.handleRequest(request, (requestResult -> { 255 try { 256 MicrohttpResponse microhttpResponse = toMicrohttpResponse(requestResult.getMarshaledResponse()); 257 shouldWriteFailsafeResponse.set(false); 258 259 try { 260 microHttpCallback.accept(microhttpResponse); 261 } catch (Throwable t) { 262 safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "Unable to write response") 263 .throwable(t) 264 .build()); 265 } 266 } catch (Throwable t) { 267 safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "An error occurred while marshaling to a response") 268 .throwable(t) 269 .build()); 270 271 try { 272 microHttpCallback.accept(provideMicrohttpFailsafeResponse(microhttpRequest, t)); 273 } catch (Throwable t2) { 274 safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "An error occurred while writing a failsafe response") 275 .throwable(t2) 276 .build()); 277 } 278 } 279 })); 280 } catch (Throwable t) { 281 safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "An unexpected error occurred during request handling") 282 .throwable(t) 283 .build()); 284 285 if (shouldWriteFailsafeResponse.get()) { 286 try { 287 microHttpCallback.accept(provideMicrohttpFailsafeResponse(microhttpRequest, t)); 288 } catch (Throwable t2) { 289 safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "An error occurred while writing a failsafe response") 290 .throwable(t2) 291 .build()); 292 } 293 } 294 } 295 }); 296 }); 297 298 this.requestHandlerExecutorService = getRequestHandlerExecutorServiceSupplier().get(); 299 300 try { 301 this.eventLoop = new EventLoop(options, logger, handler); 302 eventLoop.start(); 303 } catch (IOException e) { 304 throw new UncheckedIOException(e); 305 } 306 } finally { 307 getLock().unlock(); 308 } 309 } 310 311 @Override 312 public void stop() { 313 getLock().lock(); 314 315 try { 316 if (!isStarted()) 317 return; 318 319 try { 320 getEventLoop().get().stop(); 321 } catch (Exception e) { 322 safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "Unable to shut down server event loop") 323 .throwable(e) 324 .build()); 325 } 326 327 boolean interrupted = false; 328 329 try { 330 getRequestHandlerExecutorService().get().shutdown(); 331 getRequestHandlerExecutorService().get().awaitTermination(getShutdownTimeout().getSeconds(), TimeUnit.SECONDS); 332 } catch (InterruptedException e) { 333 interrupted = true; 334 } catch (Exception e) { 335 safelyLog(LogEvent.with(LogEventType.SERVER_INTERNAL_ERROR, "Unable to shut down server request handler executor service") 336 .throwable(e) 337 .build()); 338 } finally { 339 if (interrupted) 340 Thread.currentThread().interrupt(); 341 } 342 } finally { 343 this.eventLoop = null; 344 this.requestHandlerExecutorService = null; 345 346 getLock().unlock(); 347 } 348 } 349 350 @Nonnull 351 protected MicrohttpResponse provideMicrohttpFailsafeResponse(@Nonnull MicrohttpRequest microhttpRequest, 352 @Nonnull Throwable throwable) { 353 requireNonNull(microhttpRequest); 354 requireNonNull(throwable); 355 356 Integer statusCode = 500; 357 Charset charset = StandardCharsets.UTF_8; 358 String reasonPhrase = StatusCode.fromStatusCode(statusCode).get().getReasonPhrase(); 359 List<Header> headers = List.of(new Header("Content-Type", format("text/plain; charset=%s", charset.name()))); 360 byte[] body = format("HTTP %d: %s", statusCode, StatusCode.fromStatusCode(statusCode).get().getReasonPhrase()).getBytes(charset); 361 362 return new MicrohttpResponse(statusCode, reasonPhrase, headers, body); 363 } 364 365 @Nonnull 366 @Override 367 public Boolean isStarted() { 368 getLock().lock(); 369 370 try { 371 return getEventLoop().isPresent(); 372 } finally { 373 getLock().unlock(); 374 } 375 } 376 377 @Override 378 public void initialize(@Nonnull SokletConfiguration sokletConfiguration, 379 @Nonnull RequestHandler requestHandler) { 380 requireNonNull(requestHandler); 381 requireNonNull(sokletConfiguration); 382 383 this.requestHandler = requestHandler; 384 this.lifecycleInterceptor = sokletConfiguration.getLifecycleInterceptor(); 385 } 386 387 @Nonnull 388 protected Map<String, Set<String>> headersFromMicrohttpRequest(@Nonnull MicrohttpRequest microhttpRequest) { 389 requireNonNull(microhttpRequest); 390 391 Map<String, Set<String>> headers = new LinkedCaseInsensitiveMap<>(microhttpRequest.headers().size()); 392 393 for (Header header : microhttpRequest.headers()) { 394 Set<String> values = headers.computeIfAbsent(header.name(), k -> new LinkedHashSet<>()); 395 396 String value = trimAggressivelyToNull(header.value()); 397 398 if (value != null) 399 values.add(value); 400 } 401 402 return headers; 403 } 404 405 @Nonnull 406 protected MicrohttpResponse toMicrohttpResponse(@Nonnull MarshaledResponse marshaledResponse) { 407 requireNonNull(marshaledResponse); 408 409 List<Header> headers = new ArrayList<>(); 410 411 // Non-cookies headers get their values comma-separated 412 for (Map.Entry<String, Set<String>> entry : marshaledResponse.getHeaders().entrySet()) { 413 String name = entry.getKey(); 414 Set<String> values = entry.getValue(); 415 416 // Force natural ordering for consistent output if the set is not already sorted. 417 if (!isAlreadySorted(values)) 418 values = new TreeSet<>(entry.getValue()); 419 420 headers.add(new Header(name, String.join(", ", values))); 421 } 422 423 // ResponseCookie headers are split into multiple instances of Set-Cookie. 424 // Force natural ordering for consistent output if the set is not already sorted. 425 Set<ResponseCookie> cookies = marshaledResponse.getCookies(); 426 List<ResponseCookie> sortedCookies = new ArrayList<>(cookies); 427 428 if (!isAlreadySorted(cookies)) 429 sortedCookies.sort(Comparator.comparing(ResponseCookie::getName)); 430 431 for (ResponseCookie cookie : sortedCookies) 432 headers.add(new Header("Set-Cookie", cookie.toSetCookieHeaderRepresentation())); 433 434 // Force natural order for consistent output 435 headers.sort(Comparator.comparing(Header::name)); 436 437 String reasonPhrase = reasonPhraseForStatusCode(marshaledResponse.getStatusCode()); 438 byte[] body = marshaledResponse.getBody().orElse(emptyByteArray()); 439 440 return new MicrohttpResponse(marshaledResponse.getStatusCode(), reasonPhrase, headers, body); 441 } 442 443 @Nonnull 444 protected String reasonPhraseForStatusCode(@Nonnull Integer statusCode) { 445 requireNonNull(statusCode); 446 447 StatusCode formalStatusCode = StatusCode.fromStatusCode(statusCode).orElse(null); 448 return formalStatusCode == null ? "Unknown" : formalStatusCode.getReasonPhrase(); 449 } 450 451 @Nonnull 452 protected Boolean isAlreadySorted(@Nonnull Set<?> set) { 453 requireNonNull(set); 454 return set instanceof SortedSet || set instanceof LinkedHashSet; 455 } 456 457 protected void safelyLog(@Nonnull LogEvent logEvent) { 458 requireNonNull(logEvent); 459 460 try { 461 getLifecycleInterceptor().get().didReceiveLogEvent(logEvent); 462 } catch (Throwable throwable) { 463 // The LifecycleInterceptor implementation errored out, but we can't let that affect us - swallow its exception. 464 // Not much else we can do here but dump to stderr 465 throwable.printStackTrace(); 466 } 467 } 468 469 @Nonnull 470 protected Integer getPort() { 471 return this.port; 472 } 473 474 @Nonnull 475 protected Integer getConcurrency() { 476 return this.concurrency; 477 } 478 479 @Nonnull 480 protected String getHost() { 481 return this.host; 482 } 483 484 @Nonnull 485 protected Duration getRequestTimeout() { 486 return this.requestTimeout; 487 } 488 489 @Nonnull 490 protected Duration getSocketSelectTimeout() { 491 return this.socketSelectTimeout; 492 } 493 494 @Nonnull 495 protected Duration getShutdownTimeout() { 496 return this.shutdownTimeout; 497 } 498 499 @Nonnull 500 protected Integer getMaximumRequestSizeInBytes() { 501 return this.maximumRequestSizeInBytes; 502 } 503 504 @Nonnull 505 protected Integer getRequestReadBufferSizeInBytes() { 506 return this.requestReadBufferSizeInBytes; 507 } 508 509 @Nonnull 510 protected Integer getSocketPendingConnectionLimit() { 511 return this.socketPendingConnectionLimit; 512 } 513 514 @Nonnull 515 protected MultipartParser getMultipartParser() { 516 return this.multipartParser; 517 } 518 519 @Nonnull 520 protected Optional<ExecutorService> getRequestHandlerExecutorService() { 521 return Optional.ofNullable(this.requestHandlerExecutorService); 522 } 523 524 @Nonnull 525 protected ReentrantLock getLock() { 526 return this.lock; 527 } 528 529 @Nonnull 530 protected Optional<RequestHandler> getServerListener() { 531 return Optional.ofNullable(this.requestHandler); 532 } 533 534 @Nonnull 535 protected Optional<EventLoop> getEventLoop() { 536 return Optional.ofNullable(this.eventLoop); 537 } 538 539 @Nonnull 540 protected Supplier<ExecutorService> getRequestHandlerExecutorServiceSupplier() { 541 return this.requestHandlerExecutorServiceSupplier; 542 } 543 544 @Nonnull 545 protected Optional<LifecycleInterceptor> getLifecycleInterceptor() { 546 return Optional.ofNullable(this.lifecycleInterceptor); 547 } 548 549 @Nonnull 550 protected Optional<RequestHandler> getRequestHandler() { 551 return Optional.ofNullable(this.requestHandler); 552 } 553 554 @ThreadSafe 555 protected static class NonvirtualThreadFactory implements ThreadFactory { 556 @Nonnull 557 private final String namePrefix; 558 @Nonnull 559 private final AtomicInteger idGenerator; 560 561 public NonvirtualThreadFactory(@Nonnull String namePrefix) { 562 requireNonNull(namePrefix); 563 564 this.namePrefix = namePrefix; 565 this.idGenerator = new AtomicInteger(0); 566 } 567 568 @Override 569 @Nonnull 570 public Thread newThread(@Nonnull Runnable runnable) { 571 String name = format("%s-%s", getNamePrefix(), getIdGenerator().incrementAndGet()); 572 return new Thread(runnable, name); 573 } 574 575 @Nonnull 576 protected String getNamePrefix() { 577 return this.namePrefix; 578 } 579 580 @Nonnull 581 protected AtomicInteger getIdGenerator() { 582 return this.idGenerator; 583 } 584 } 585 586 /** 587 * Builder used to construct instances of {@link DefaultServer}. 588 * <p> 589 * This class is intended for use by a single thread. 590 * 591 * @author <a href="https://www.revetkn.com">Mark Allen</a> 592 */ 593 @NotThreadSafe 594 public static class Builder { 595 @Nonnull 596 private Integer port; 597 @Nullable 598 private String host; 599 @Nullable 600 private Integer concurrency; 601 @Nullable 602 private Duration requestTimeout; 603 @Nullable 604 private Duration socketSelectTimeout; 605 @Nullable 606 private Duration shutdownTimeout; 607 @Nullable 608 private Integer maximumRequestSizeInBytes; 609 @Nullable 610 private Integer requestReadBufferSizeInBytes; 611 @Nullable 612 private Integer socketPendingConnectionLimit; 613 @Nullable 614 private MultipartParser multipartParser; 615 @Nullable 616 private Supplier<ExecutorService> requestHandlerExecutorServiceSupplier; 617 618 @Nonnull 619 protected Builder(@Nonnull Integer port) { 620 requireNonNull(port); 621 this.port = port; 622 } 623 624 @Nonnull 625 public Builder port(@Nonnull Integer port) { 626 requireNonNull(port); 627 this.port = port; 628 return this; 629 } 630 631 @Nonnull 632 public Builder host(@Nullable String host) { 633 this.host = host; 634 return this; 635 } 636 637 @Nonnull 638 public Builder concurrency(@Nullable Integer concurrency) { 639 this.concurrency = concurrency; 640 return this; 641 } 642 643 @Nonnull 644 public Builder requestTimeout(@Nullable Duration requestTimeout) { 645 this.requestTimeout = requestTimeout; 646 return this; 647 } 648 649 @Nonnull 650 public Builder socketSelectTimeout(@Nullable Duration socketSelectTimeout) { 651 this.socketSelectTimeout = socketSelectTimeout; 652 return this; 653 } 654 655 @Nonnull 656 public Builder socketPendingConnectionLimit(@Nullable Integer socketPendingConnectionLimit) { 657 this.socketPendingConnectionLimit = socketPendingConnectionLimit; 658 return this; 659 } 660 661 @Nonnull 662 public Builder shutdownTimeout(@Nullable Duration shutdownTimeout) { 663 this.shutdownTimeout = shutdownTimeout; 664 return this; 665 } 666 667 @Nonnull 668 public Builder maximumRequestSizeInBytes(@Nullable Integer maximumRequestSizeInBytes) { 669 this.maximumRequestSizeInBytes = maximumRequestSizeInBytes; 670 return this; 671 } 672 673 @Nonnull 674 public Builder requestReadBufferSizeInBytes(@Nullable Integer requestReadBufferSizeInBytes) { 675 this.requestReadBufferSizeInBytes = requestReadBufferSizeInBytes; 676 return this; 677 } 678 679 @Nonnull 680 public Builder multipartParser(@Nullable MultipartParser multipartParser) { 681 this.multipartParser = multipartParser; 682 return this; 683 } 684 685 @Nonnull 686 public Builder requestHandlerExecutorServiceSupplier(@Nullable Supplier<ExecutorService> requestHandlerExecutorServiceSupplier) { 687 this.requestHandlerExecutorServiceSupplier = requestHandlerExecutorServiceSupplier; 688 return this; 689 } 690 691 @Nonnull 692 public DefaultServer build() { 693 return new DefaultServer(this); 694 } 695 } 696}