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.converter.ValueConverterRegistry; 020import org.jspecify.annotations.NonNull; 021import org.jspecify.annotations.Nullable; 022 023import javax.annotation.concurrent.NotThreadSafe; 024import javax.annotation.concurrent.ThreadSafe; 025import java.util.Collection; 026import java.util.List; 027import java.util.Optional; 028 029import static java.lang.String.format; 030import static java.util.Objects.requireNonNull; 031 032/** 033 * Defines how a Soklet system is configured. 034 * <p> 035 * Threadsafe instances can be acquired via one of the builder factory methods such as {@link #withHttpServer(HttpServer)}, 036 * {@link #withSseServer(SseServer)}, or {@link #withMcpServer(McpServer)}. 037 * 038 * @author <a href="https://www.revetkn.com">Mark Allen</a> 039 */ 040@ThreadSafe 041public final class SokletConfig { 042 @NonNull 043 private final InstanceProvider instanceProvider; 044 @NonNull 045 private final ValueConverterRegistry valueConverterRegistry; 046 @NonNull 047 private final RequestBodyMarshaler requestBodyMarshaler; 048 @NonNull 049 private final ResourceMethodResolver resourceMethodResolver; 050 @NonNull 051 private final ResourceMethodParameterProvider resourceMethodParameterProvider; 052 @NonNull 053 private final ResponseMarshaler responseMarshaler; 054 @NonNull 055 private final RequestInterceptor requestInterceptor; 056 @NonNull 057 private final List<LifecycleObserver> lifecycleObservers; 058 @NonNull 059 private final LifecycleObserver aggregateLifecycleObserver; 060 @NonNull 061 private final MetricsCollector metricsCollector; 062 @NonNull 063 private final CorsAuthorizer corsAuthorizer; 064 @Nullable 065 private final HttpServer httpServer; 066 @Nullable 067 private final SseServer sseServer; 068 @Nullable 069 private final McpServer mcpServer; 070 071 /** 072 * Vends a configuration builder, primed with the given HTTP {@link HttpServer}. 073 * 074 * @param httpServer the HTTP server necessary for construction 075 * @return a builder for {@link SokletConfig} instances 076 */ 077 @NonNull 078 public static Builder withHttpServer(@NonNull HttpServer httpServer) { 079 requireNonNull(httpServer); 080 return new Builder().httpServer(httpServer); 081 } 082 083 /** 084 * Vends a configuration builder, primed with the given {@link SseServer}. 085 * 086 * @param sseServer the SSE server necessary for construction 087 * @return a builder for {@link SokletConfig} instances 088 */ 089 @NonNull 090 public static Builder withSseServer(@NonNull SseServer sseServer) { 091 requireNonNull(sseServer); 092 return new Builder().sseServer(sseServer); 093 } 094 095 /** 096 * Vends a configuration builder, primed with the given {@link McpServer}. 097 * 098 * @param mcpServer the MCP server necessary for construction 099 * @return a builder for {@link SokletConfig} instances 100 */ 101 @NonNull 102 public static Builder withMcpServer(@NonNull McpServer mcpServer) { 103 requireNonNull(mcpServer); 104 return new Builder().mcpServer(mcpServer); 105 } 106 107 /** 108 * Package-private - used for internal Soklet tests. 109 */ 110 @NonNull 111 static Builder forSimulatorTesting() { 112 return SokletConfig.withHttpServer(HttpServer.withPort(0).build()).sseServer(SseServer.withPort(0).build()); 113 } 114 115 protected SokletConfig(@NonNull Builder builder) { 116 requireNonNull(builder); 117 118 // Wrap servers in proxies transparently 119 HttpServerProxy httpServerProxy = builder.httpServer == null ? null : new HttpServerProxy(builder.httpServer); 120 SseServerProxy sseServerProxy = builder.sseServer == null ? null : new SseServerProxy(builder.sseServer); 121 McpServerProxy mcpServerProxy = builder.mcpServer == null ? null : new McpServerProxy(builder.mcpServer); 122 123 this.httpServer = httpServerProxy; 124 this.sseServer = sseServerProxy; 125 this.mcpServer = mcpServerProxy; 126 this.instanceProvider = builder.instanceProvider != null ? builder.instanceProvider : InstanceProvider.defaultInstance(); 127 this.valueConverterRegistry = builder.valueConverterRegistry != null ? builder.valueConverterRegistry : ValueConverterRegistry.fromDefaults(); 128 this.requestBodyMarshaler = builder.requestBodyMarshaler != null ? builder.requestBodyMarshaler : RequestBodyMarshaler.fromValueConverterRegistry(getValueConverterRegistry()); 129 this.resourceMethodResolver = builder.resourceMethodResolver != null ? builder.resourceMethodResolver : ResourceMethodResolver.fromClasspathIntrospection(); 130 this.responseMarshaler = builder.responseMarshaler != null ? builder.responseMarshaler : ResponseMarshaler.defaultInstance(); 131 this.requestInterceptor = builder.requestInterceptor != null ? builder.requestInterceptor : RequestInterceptor.defaultInstance(); 132 this.lifecycleObservers = builder.lifecycleObservers != null ? builder.lifecycleObservers : List.of(LifecycleObserver.defaultInstance()); 133 this.aggregateLifecycleObserver = LifecycleObservers.aggregate(this.lifecycleObservers); 134 this.metricsCollector = builder.metricsCollector != null ? builder.metricsCollector : MetricsCollector.defaultInstance(); 135 this.corsAuthorizer = builder.corsAuthorizer != null ? builder.corsAuthorizer : CorsAuthorizer.rejectAllInstance(); 136 this.resourceMethodParameterProvider = builder.resourceMethodParameterProvider != null ? builder.resourceMethodParameterProvider : new DefaultResourceMethodParameterProvider(this); 137 } 138 139 /** 140 * Vends a mutable copy of this instance's configuration, suitable for building new instances. 141 * 142 * @return a mutable copy of this instance's configuration 143 */ 144 @NonNull 145 public Copier copy() { 146 return new Copier(this); 147 } 148 149 /** 150 * How Soklet will perform <a href="https://www.soklet.com/docs/instance-creation">instance creation</a>. 151 * 152 * @return the instance responsible for instance creation 153 */ 154 @NonNull 155 public InstanceProvider getInstanceProvider() { 156 return this.instanceProvider; 157 } 158 159 /** 160 * How Soklet will perform <a href="https://www.soklet.com/docs/value-conversions">conversions from one Java type to another</a>, like a {@link String} to a {@link java.time.LocalDate}. 161 * 162 * @return the instance responsible for value conversions 163 */ 164 @NonNull 165 public ValueConverterRegistry getValueConverterRegistry() { 166 return this.valueConverterRegistry; 167 } 168 169 /** 170 * How Soklet will <a href="https://www.soklet.com/docs/request-handling#request-body">marshal request bodies to Java types</a>. 171 * 172 * @return the instance responsible for request body marshaling 173 */ 174 @NonNull 175 public RequestBodyMarshaler getRequestBodyMarshaler() { 176 return this.requestBodyMarshaler; 177 } 178 179 /** 180 * How Soklet performs <a href="https://www.soklet.com/docs/request-handling#resource-method-resolution"><em>Resource Method</em> resolution</a> (experts only!) 181 * 182 * @return the instance responsible for <em>Resource Method</em> resolution 183 */ 184 @NonNull 185 public ResourceMethodResolver getResourceMethodResolver() { 186 return this.resourceMethodResolver; 187 } 188 189 /** 190 * How Soklet performs <a href="https://www.soklet.com/docs/request-handling#resource-method-parameter-injection"><em>Resource Method</em> parameter injection</a> (experts only!) 191 * 192 * @return the instance responsible for <em>Resource Method</em> parameter injection 193 */ 194 @NonNull 195 public ResourceMethodParameterProvider getResourceMethodParameterProvider() { 196 return this.resourceMethodParameterProvider; 197 } 198 199 /** 200 * How Soklet will <a href="https://www.soklet.com/docs/response-writing">marshal response bodies to bytes suitable for transmission over the wire</a>. 201 * 202 * @return the instance responsible for response body marshaling 203 */ 204 @NonNull 205 public ResponseMarshaler getResponseMarshaler() { 206 return this.responseMarshaler; 207 } 208 209 /** 210 * How Soklet will <a href="https://www.soklet.com/docs/request-lifecycle">perform custom behavior during request handling</a>. 211 * 212 * @return the instance responsible for request interceptor behavior 213 */ 214 @NonNull 215 public RequestInterceptor getRequestInterceptor() { 216 return this.requestInterceptor; 217 } 218 219 @NonNull 220 LifecycleObserver getAggregateLifecycleObserver() { 221 return this.aggregateLifecycleObserver; 222 } 223 224 /** 225 * How Soklet will <a href="https://www.soklet.com/docs/request-lifecycle">observe server and request lifecycle events</a>. 226 * 227 * @return the lifecycle observers that are invoked in registration order 228 */ 229 @NonNull 230 public List<LifecycleObserver> getLifecycleObservers() { 231 return this.lifecycleObservers; 232 } 233 234 /** 235 * How Soklet will collect operational metrics. 236 * 237 * @return the instance responsible for metrics collection 238 */ 239 @NonNull 240 public MetricsCollector getMetricsCollector() { 241 return this.metricsCollector; 242 } 243 244 /** 245 * How Soklet handles <a href="https://www.soklet.com/docs/cors">Cross-Origin Resource Sharing (CORS)</a>. 246 * 247 * @return the instance responsible for CORS-related processing 248 */ 249 @NonNull 250 public CorsAuthorizer getCorsAuthorizer() { 251 return this.corsAuthorizer; 252 } 253 254 /** 255 * The HTTP server managed by Soklet, if configured. 256 * 257 * @return the HTTP server, if configured 258 */ 259 @NonNull 260 public Optional<HttpServer> getHttpServer() { 261 return Optional.ofNullable(this.httpServer); 262 } 263 264 /** 265 * The SSE server managed by Soklet, if configured. 266 * 267 * @return the SSE server instance, or {@link Optional#empty()} if none was configured 268 */ 269 @NonNull 270 public Optional<SseServer> getSseServer() { 271 return Optional.ofNullable(this.sseServer); 272 } 273 274 /** 275 * The MCP server managed by Soklet, if configured. 276 * 277 * @return the MCP server, if configured 278 */ 279 @NonNull 280 public Optional<McpServer> getMcpServer() { 281 return Optional.ofNullable(this.mcpServer); 282 } 283 284 /** 285 * Builder used to construct instances of {@link SokletConfig}. 286 * <p> 287 * Instances are created by invoking one of the static factory methods on {@link SokletConfig}. 288 * <p> 289 * This class is intended for use by a single thread. 290 * 291 * @author <a href="https://www.revetkn.com">Mark Allen</a> 292 */ 293 @NotThreadSafe 294 public static final class Builder { 295 @Nullable 296 private HttpServer httpServer; 297 @Nullable 298 private SseServer sseServer; 299 @Nullable 300 private McpServer mcpServer; 301 @Nullable 302 private InstanceProvider instanceProvider; 303 @Nullable 304 private ValueConverterRegistry valueConverterRegistry; 305 @Nullable 306 private RequestBodyMarshaler requestBodyMarshaler; 307 @Nullable 308 private ResourceMethodResolver resourceMethodResolver; 309 @Nullable 310 private ResourceMethodParameterProvider resourceMethodParameterProvider; 311 @Nullable 312 private ResponseMarshaler responseMarshaler; 313 @Nullable 314 private RequestInterceptor requestInterceptor; 315 @Nullable 316 private List<LifecycleObserver> lifecycleObservers; 317 @Nullable 318 private MetricsCollector metricsCollector; 319 @Nullable 320 private CorsAuthorizer corsAuthorizer; 321 322 Builder() { 323 // No-op 324 } 325 326 @NonNull 327 public Builder httpServer(@Nullable HttpServer httpServer) { 328 this.httpServer = httpServer; 329 return this; 330 } 331 332 @NonNull 333 public Builder sseServer(@Nullable SseServer sseServer) { 334 this.sseServer = sseServer; 335 return this; 336 } 337 338 @NonNull 339 public Builder mcpServer(@Nullable McpServer mcpServer) { 340 this.mcpServer = mcpServer; 341 return this; 342 } 343 344 @NonNull 345 public Builder instanceProvider(@Nullable InstanceProvider instanceProvider) { 346 this.instanceProvider = instanceProvider; 347 return this; 348 } 349 350 @NonNull 351 public Builder valueConverterRegistry(@Nullable ValueConverterRegistry valueConverterRegistry) { 352 this.valueConverterRegistry = valueConverterRegistry; 353 return this; 354 } 355 356 @NonNull 357 public Builder requestBodyMarshaler(@Nullable RequestBodyMarshaler requestBodyMarshaler) { 358 this.requestBodyMarshaler = requestBodyMarshaler; 359 return this; 360 } 361 362 @NonNull 363 public Builder resourceMethodResolver(@Nullable ResourceMethodResolver resourceMethodResolver) { 364 this.resourceMethodResolver = resourceMethodResolver; 365 return this; 366 } 367 368 @NonNull 369 public Builder resourceMethodParameterProvider(@Nullable ResourceMethodParameterProvider resourceMethodParameterProvider) { 370 this.resourceMethodParameterProvider = resourceMethodParameterProvider; 371 return this; 372 } 373 374 @NonNull 375 public Builder responseMarshaler(@Nullable ResponseMarshaler responseMarshaler) { 376 this.responseMarshaler = responseMarshaler; 377 return this; 378 } 379 380 @NonNull 381 public Builder requestInterceptor(@Nullable RequestInterceptor requestInterceptor) { 382 this.requestInterceptor = requestInterceptor; 383 return this; 384 } 385 386 @NonNull 387 public Builder lifecycleObserver(@Nullable LifecycleObserver lifecycleObserver) { 388 this.lifecycleObservers = lifecycleObserver == null ? List.of() : List.of(lifecycleObserver); 389 return this; 390 } 391 392 @NonNull 393 public Builder lifecycleObservers(@Nullable Collection<? extends LifecycleObserver> lifecycleObservers) { 394 this.lifecycleObservers = copyLifecycleObservers(lifecycleObservers); 395 return this; 396 } 397 398 @NonNull 399 public Builder metricsCollector(@Nullable MetricsCollector metricsCollector) { 400 this.metricsCollector = metricsCollector; 401 return this; 402 } 403 404 @NonNull 405 public Builder corsAuthorizer(@Nullable CorsAuthorizer corsAuthorizer) { 406 this.corsAuthorizer = corsAuthorizer; 407 return this; 408 } 409 410 @NonNull 411 public SokletConfig build() { 412 if (this.httpServer == null && this.sseServer == null && this.mcpServer == null) 413 throw new IllegalStateException(format("At least one of %s, %s, or %s must be configured", 414 HttpServer.class.getSimpleName(), SseServer.class.getSimpleName(), McpServer.class.getSimpleName())); 415 416 return new SokletConfig(this); 417 } 418 } 419 420 /** 421 * Builder used to copy instances of {@link SokletConfig}. 422 * <p> 423 * Instances are created by invoking {@link SokletConfig#copy()}. 424 * <p> 425 * This class is intended for use by a single thread. 426 * 427 * @author <a href="https://www.revetkn.com">Mark Allen</a> 428 */ 429 @NotThreadSafe 430 public static final class Copier { 431 @NonNull 432 private final Builder builder; 433 434 /** 435 * Unwraps a HttpServer proxy to get the underlying real implementation. 436 */ 437 @NonNull 438 private static HttpServer unwrapHttpServer(@NonNull HttpServer httpServer) { 439 requireNonNull(httpServer); 440 441 if (httpServer instanceof HttpServerProxy) 442 return ((HttpServerProxy) httpServer).getRealImplementation(); 443 444 return httpServer; 445 } 446 447 /** 448 * Unwraps a SseServer proxy to get the underlying real implementation. 449 */ 450 @NonNull 451 private static SseServer unwrapSseServer(@NonNull SseServer sseServer) { 452 requireNonNull(sseServer); 453 454 if (sseServer instanceof SseServerProxy) 455 return ((SseServerProxy) sseServer).getRealImplementation(); 456 457 return sseServer; 458 } 459 460 /** 461 * Unwraps an McpServer proxy to get the underlying real implementation. 462 */ 463 @NonNull 464 private static McpServer unwrapMcpServer(@NonNull McpServer mcpServer) { 465 requireNonNull(mcpServer); 466 467 if (mcpServer instanceof McpServerProxy) 468 return ((McpServerProxy) mcpServer).getRealImplementation(); 469 470 return mcpServer; 471 } 472 473 Copier(@NonNull SokletConfig sokletConfig) { 474 requireNonNull(sokletConfig); 475 476 // Unwrap proxies to get the real implementations for copying 477 HttpServer realHttpServer = sokletConfig.getHttpServer() 478 .map(Copier::unwrapHttpServer) 479 .orElse(null); 480 SseServer realSseServer = sokletConfig.getSseServer() 481 .map(Copier::unwrapSseServer) 482 .orElse(null); 483 McpServer realMcpServer = sokletConfig.getMcpServer() 484 .map(Copier::unwrapMcpServer) 485 .orElse(null); 486 487 this.builder = new Builder() 488 .httpServer(realHttpServer) 489 .sseServer(realSseServer) 490 .mcpServer(realMcpServer) 491 .instanceProvider(sokletConfig.getInstanceProvider()) 492 .valueConverterRegistry(sokletConfig.valueConverterRegistry) 493 .requestBodyMarshaler(sokletConfig.requestBodyMarshaler) 494 .resourceMethodResolver(sokletConfig.resourceMethodResolver) 495 .resourceMethodParameterProvider(sokletConfig.resourceMethodParameterProvider) 496 .responseMarshaler(sokletConfig.responseMarshaler) 497 .requestInterceptor(sokletConfig.requestInterceptor) 498 .lifecycleObservers(sokletConfig.lifecycleObservers) 499 .metricsCollector(sokletConfig.metricsCollector) 500 .corsAuthorizer(sokletConfig.corsAuthorizer); 501 } 502 503 @NonNull 504 public Copier httpServer(@Nullable HttpServer httpServer) { 505 this.builder.httpServer(httpServer); 506 return this; 507 } 508 509 @NonNull 510 public Copier sseServer(@Nullable SseServer sseServer) { 511 this.builder.sseServer(sseServer); 512 return this; 513 } 514 515 @NonNull 516 public Copier mcpServer(@Nullable McpServer mcpServer) { 517 this.builder.mcpServer(mcpServer); 518 return this; 519 } 520 521 @NonNull 522 public Copier instanceProvider(@Nullable InstanceProvider instanceProvider) { 523 this.builder.instanceProvider(instanceProvider); 524 return this; 525 } 526 527 @NonNull 528 public Copier valueConverterRegistry(@Nullable ValueConverterRegistry valueConverterRegistry) { 529 this.builder.valueConverterRegistry(valueConverterRegistry); 530 return this; 531 } 532 533 @NonNull 534 public Copier requestBodyMarshaler(@Nullable RequestBodyMarshaler requestBodyMarshaler) { 535 this.builder.requestBodyMarshaler(requestBodyMarshaler); 536 return this; 537 } 538 539 @NonNull 540 public Copier resourceMethodResolver(@Nullable ResourceMethodResolver resourceMethodResolver) { 541 this.builder.resourceMethodResolver(resourceMethodResolver); 542 return this; 543 } 544 545 @NonNull 546 public Copier resourceMethodParameterProvider(@Nullable ResourceMethodParameterProvider resourceMethodParameterProvider) { 547 this.builder.resourceMethodParameterProvider(resourceMethodParameterProvider); 548 return this; 549 } 550 551 @NonNull 552 public Copier responseMarshaler(@Nullable ResponseMarshaler responseMarshaler) { 553 this.builder.responseMarshaler(responseMarshaler); 554 return this; 555 } 556 557 @NonNull 558 public Copier requestInterceptor(@Nullable RequestInterceptor requestInterceptor) { 559 this.builder.requestInterceptor(requestInterceptor); 560 return this; 561 } 562 563 @NonNull 564 public Copier lifecycleObserver(@Nullable LifecycleObserver lifecycleObserver) { 565 this.builder.lifecycleObserver(lifecycleObserver); 566 return this; 567 } 568 569 @NonNull 570 public Copier lifecycleObservers(@Nullable Collection<? extends LifecycleObserver> lifecycleObservers) { 571 this.builder.lifecycleObservers(lifecycleObservers); 572 return this; 573 } 574 575 @NonNull 576 public Copier metricsCollector(@Nullable MetricsCollector metricsCollector) { 577 this.builder.metricsCollector(metricsCollector); 578 return this; 579 } 580 581 @NonNull 582 public Copier corsAuthorizer(@Nullable CorsAuthorizer corsAuthorizer) { 583 this.builder.corsAuthorizer(corsAuthorizer); 584 return this; 585 } 586 587 @NonNull 588 public SokletConfig finish() { 589 return this.builder.build(); 590 } 591 } 592 593 @NonNull 594 private static List<LifecycleObserver> copyLifecycleObservers(@Nullable Collection<? extends LifecycleObserver> lifecycleObservers) { 595 if (lifecycleObservers == null || lifecycleObservers.isEmpty()) 596 return List.of(); 597 598 return List.copyOf(lifecycleObservers); 599 } 600}