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}