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