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}