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