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