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}