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.Collection;
026import java.util.List;
027import java.util.Optional;
028
029import static java.lang.String.format;
030import static java.util.Objects.requireNonNull;
031
032/**
033 * Defines how a Soklet system is configured.
034 * <p>
035 * Threadsafe instances can be acquired via one of the builder factory methods such as {@link #withHttpServer(HttpServer)},
036 * {@link #withSseServer(SseServer)}, or {@link #withMcpServer(McpServer)}.
037 *
038 * @author <a href="https://www.revetkn.com">Mark Allen</a>
039 */
040@ThreadSafe
041public final class SokletConfig {
042        @NonNull
043        private final InstanceProvider instanceProvider;
044        @NonNull
045        private final ValueConverterRegistry valueConverterRegistry;
046        @NonNull
047        private final RequestBodyMarshaler requestBodyMarshaler;
048        @NonNull
049        private final ResourceMethodResolver resourceMethodResolver;
050        @NonNull
051        private final ResourceMethodParameterProvider resourceMethodParameterProvider;
052        @NonNull
053        private final ResponseMarshaler responseMarshaler;
054        @NonNull
055        private final RequestInterceptor requestInterceptor;
056        @NonNull
057        private final List<LifecycleObserver> lifecycleObservers;
058        @NonNull
059        private final LifecycleObserver aggregateLifecycleObserver;
060        @NonNull
061        private final MetricsCollector metricsCollector;
062        @NonNull
063        private final CorsAuthorizer corsAuthorizer;
064        @Nullable
065        private final HttpServer httpServer;
066        @Nullable
067        private final SseServer sseServer;
068        @Nullable
069        private final McpServer mcpServer;
070
071        /**
072         * Vends a configuration builder, primed with the given HTTP {@link HttpServer}.
073         *
074         * @param httpServer the HTTP server necessary for construction
075         * @return a builder for {@link SokletConfig} instances
076         */
077        @NonNull
078        public static Builder withHttpServer(@NonNull HttpServer httpServer) {
079                requireNonNull(httpServer);
080                return new Builder().httpServer(httpServer);
081        }
082
083        /**
084         * Vends a configuration builder, primed with the given {@link SseServer}.
085         *
086         * @param sseServer the SSE server necessary for construction
087         * @return a builder for {@link SokletConfig} instances
088         */
089        @NonNull
090        public static Builder withSseServer(@NonNull SseServer sseServer) {
091                requireNonNull(sseServer);
092                return new Builder().sseServer(sseServer);
093        }
094
095        /**
096         * Vends a configuration builder, primed with the given {@link McpServer}.
097         *
098         * @param mcpServer the MCP server necessary for construction
099         * @return a builder for {@link SokletConfig} instances
100         */
101        @NonNull
102        public static Builder withMcpServer(@NonNull McpServer mcpServer) {
103                requireNonNull(mcpServer);
104                return new Builder().mcpServer(mcpServer);
105        }
106
107        /**
108         * Package-private - used for internal Soklet tests.
109         */
110        @NonNull
111        static Builder forSimulatorTesting() {
112                return SokletConfig.withHttpServer(HttpServer.withPort(0).build()).sseServer(SseServer.withPort(0).build());
113        }
114
115        protected SokletConfig(@NonNull Builder builder) {
116                requireNonNull(builder);
117
118                // Wrap servers in proxies transparently
119                HttpServerProxy httpServerProxy = builder.httpServer == null ? null : new HttpServerProxy(builder.httpServer);
120                SseServerProxy sseServerProxy = builder.sseServer == null ? null : new SseServerProxy(builder.sseServer);
121                McpServerProxy mcpServerProxy = builder.mcpServer == null ? null : new McpServerProxy(builder.mcpServer);
122
123                this.httpServer = httpServerProxy;
124                this.sseServer = sseServerProxy;
125                this.mcpServer = mcpServerProxy;
126                this.instanceProvider = builder.instanceProvider != null ? builder.instanceProvider : InstanceProvider.defaultInstance();
127                this.valueConverterRegistry = builder.valueConverterRegistry != null ? builder.valueConverterRegistry : ValueConverterRegistry.fromDefaults();
128                this.requestBodyMarshaler = builder.requestBodyMarshaler != null ? builder.requestBodyMarshaler : RequestBodyMarshaler.fromValueConverterRegistry(getValueConverterRegistry());
129                this.resourceMethodResolver = builder.resourceMethodResolver != null ? builder.resourceMethodResolver : ResourceMethodResolver.fromClasspathIntrospection();
130                this.responseMarshaler = builder.responseMarshaler != null ? builder.responseMarshaler : ResponseMarshaler.defaultInstance();
131                this.requestInterceptor = builder.requestInterceptor != null ? builder.requestInterceptor : RequestInterceptor.defaultInstance();
132                this.lifecycleObservers = builder.lifecycleObservers != null ? builder.lifecycleObservers : List.of(LifecycleObserver.defaultInstance());
133                this.aggregateLifecycleObserver = LifecycleObservers.aggregate(this.lifecycleObservers);
134                this.metricsCollector = builder.metricsCollector != null ? builder.metricsCollector : MetricsCollector.defaultInstance();
135                this.corsAuthorizer = builder.corsAuthorizer != null ? builder.corsAuthorizer : CorsAuthorizer.rejectAllInstance();
136                this.resourceMethodParameterProvider = builder.resourceMethodParameterProvider != null ? builder.resourceMethodParameterProvider : new DefaultResourceMethodParameterProvider(this);
137        }
138
139        /**
140         * Vends a mutable copy of this instance's configuration, suitable for building new instances.
141         *
142         * @return a mutable copy of this instance's configuration
143         */
144        @NonNull
145        public Copier copy() {
146                return new Copier(this);
147        }
148
149        /**
150         * How Soklet will perform <a href="https://www.soklet.com/docs/instance-creation">instance creation</a>.
151         *
152         * @return the instance responsible for instance creation
153         */
154        @NonNull
155        public InstanceProvider getInstanceProvider() {
156                return this.instanceProvider;
157        }
158
159        /**
160         * 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}.
161         *
162         * @return the instance responsible for value conversions
163         */
164        @NonNull
165        public ValueConverterRegistry getValueConverterRegistry() {
166                return this.valueConverterRegistry;
167        }
168
169        /**
170         * How Soklet will <a href="https://www.soklet.com/docs/request-handling#request-body">marshal request bodies to Java types</a>.
171         *
172         * @return the instance responsible for request body marshaling
173         */
174        @NonNull
175        public RequestBodyMarshaler getRequestBodyMarshaler() {
176                return this.requestBodyMarshaler;
177        }
178
179        /**
180         * How Soklet performs <a href="https://www.soklet.com/docs/request-handling#resource-method-resolution"><em>Resource Method</em> resolution</a> (experts only!)
181         *
182         * @return the instance responsible for <em>Resource Method</em> resolution
183         */
184        @NonNull
185        public ResourceMethodResolver getResourceMethodResolver() {
186                return this.resourceMethodResolver;
187        }
188
189        /**
190         * 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!)
191         *
192         * @return the instance responsible for <em>Resource Method</em> parameter injection
193         */
194        @NonNull
195        public ResourceMethodParameterProvider getResourceMethodParameterProvider() {
196                return this.resourceMethodParameterProvider;
197        }
198
199        /**
200         * How Soklet will <a href="https://www.soklet.com/docs/response-writing">marshal response bodies to bytes suitable for transmission over the wire</a>.
201         *
202         * @return the instance responsible for response body marshaling
203         */
204        @NonNull
205        public ResponseMarshaler getResponseMarshaler() {
206                return this.responseMarshaler;
207        }
208
209        /**
210         * How Soklet will <a href="https://www.soklet.com/docs/request-lifecycle">perform custom behavior during request handling</a>.
211         *
212         * @return the instance responsible for request interceptor behavior
213         */
214        @NonNull
215        public RequestInterceptor getRequestInterceptor() {
216                return this.requestInterceptor;
217        }
218
219        @NonNull
220        LifecycleObserver getAggregateLifecycleObserver() {
221                return this.aggregateLifecycleObserver;
222        }
223
224        /**
225         * How Soklet will <a href="https://www.soklet.com/docs/request-lifecycle">observe server and request lifecycle events</a>.
226         *
227         * @return the lifecycle observers that are invoked in registration order
228         */
229        @NonNull
230        public List<LifecycleObserver> getLifecycleObservers() {
231                return this.lifecycleObservers;
232        }
233
234        /**
235         * How Soklet will collect operational metrics.
236         *
237         * @return the instance responsible for metrics collection
238         */
239        @NonNull
240        public MetricsCollector getMetricsCollector() {
241                return this.metricsCollector;
242        }
243
244        /**
245         * How Soklet handles <a href="https://www.soklet.com/docs/cors">Cross-Origin Resource Sharing (CORS)</a>.
246         *
247         * @return the instance responsible for CORS-related processing
248         */
249        @NonNull
250        public CorsAuthorizer getCorsAuthorizer() {
251                return this.corsAuthorizer;
252        }
253
254        /**
255         * The HTTP server managed by Soklet, if configured.
256         *
257         * @return the HTTP server, if configured
258         */
259        @NonNull
260        public Optional<HttpServer> getHttpServer() {
261                return Optional.ofNullable(this.httpServer);
262        }
263
264        /**
265         * The SSE server managed by Soklet, if configured.
266         *
267         * @return the SSE server instance, or {@link Optional#empty()} if none was configured
268         */
269        @NonNull
270        public Optional<SseServer> getSseServer() {
271                return Optional.ofNullable(this.sseServer);
272        }
273
274        /**
275         * The MCP server managed by Soklet, if configured.
276         *
277         * @return the MCP server, if configured
278         */
279        @NonNull
280        public Optional<McpServer> getMcpServer() {
281                return Optional.ofNullable(this.mcpServer);
282        }
283
284        /**
285         * Builder used to construct instances of {@link SokletConfig}.
286         * <p>
287         * Instances are created by invoking one of the static factory methods on {@link SokletConfig}.
288         * <p>
289         * This class is intended for use by a single thread.
290         *
291         * @author <a href="https://www.revetkn.com">Mark Allen</a>
292         */
293        @NotThreadSafe
294        public static final class Builder {
295                @Nullable
296                private HttpServer httpServer;
297                @Nullable
298                private SseServer sseServer;
299                @Nullable
300                private McpServer mcpServer;
301                @Nullable
302                private InstanceProvider instanceProvider;
303                @Nullable
304                private ValueConverterRegistry valueConverterRegistry;
305                @Nullable
306                private RequestBodyMarshaler requestBodyMarshaler;
307                @Nullable
308                private ResourceMethodResolver resourceMethodResolver;
309                @Nullable
310                private ResourceMethodParameterProvider resourceMethodParameterProvider;
311                @Nullable
312                private ResponseMarshaler responseMarshaler;
313                @Nullable
314                private RequestInterceptor requestInterceptor;
315                @Nullable
316                private List<LifecycleObserver> lifecycleObservers;
317                @Nullable
318                private MetricsCollector metricsCollector;
319                @Nullable
320                private CorsAuthorizer corsAuthorizer;
321
322                Builder() {
323                        // No-op
324                }
325
326                @NonNull
327                public Builder httpServer(@Nullable HttpServer httpServer) {
328                        this.httpServer = httpServer;
329                        return this;
330                }
331
332                @NonNull
333                public Builder sseServer(@Nullable SseServer sseServer) {
334                        this.sseServer = sseServer;
335                        return this;
336                }
337
338                @NonNull
339                public Builder mcpServer(@Nullable McpServer mcpServer) {
340                        this.mcpServer = mcpServer;
341                        return this;
342                }
343
344                @NonNull
345                public Builder instanceProvider(@Nullable InstanceProvider instanceProvider) {
346                        this.instanceProvider = instanceProvider;
347                        return this;
348                }
349
350                @NonNull
351                public Builder valueConverterRegistry(@Nullable ValueConverterRegistry valueConverterRegistry) {
352                        this.valueConverterRegistry = valueConverterRegistry;
353                        return this;
354                }
355
356                @NonNull
357                public Builder requestBodyMarshaler(@Nullable RequestBodyMarshaler requestBodyMarshaler) {
358                        this.requestBodyMarshaler = requestBodyMarshaler;
359                        return this;
360                }
361
362                @NonNull
363                public Builder resourceMethodResolver(@Nullable ResourceMethodResolver resourceMethodResolver) {
364                        this.resourceMethodResolver = resourceMethodResolver;
365                        return this;
366                }
367
368                @NonNull
369                public Builder resourceMethodParameterProvider(@Nullable ResourceMethodParameterProvider resourceMethodParameterProvider) {
370                        this.resourceMethodParameterProvider = resourceMethodParameterProvider;
371                        return this;
372                }
373
374                @NonNull
375                public Builder responseMarshaler(@Nullable ResponseMarshaler responseMarshaler) {
376                        this.responseMarshaler = responseMarshaler;
377                        return this;
378                }
379
380                @NonNull
381                public Builder requestInterceptor(@Nullable RequestInterceptor requestInterceptor) {
382                        this.requestInterceptor = requestInterceptor;
383                        return this;
384                }
385
386                @NonNull
387                public Builder lifecycleObserver(@Nullable LifecycleObserver lifecycleObserver) {
388                        this.lifecycleObservers = lifecycleObserver == null ? List.of() : List.of(lifecycleObserver);
389                        return this;
390                }
391
392                @NonNull
393                public Builder lifecycleObservers(@Nullable Collection<? extends LifecycleObserver> lifecycleObservers) {
394                        this.lifecycleObservers = copyLifecycleObservers(lifecycleObservers);
395                        return this;
396                }
397
398                @NonNull
399                public Builder metricsCollector(@Nullable MetricsCollector metricsCollector) {
400                        this.metricsCollector = metricsCollector;
401                        return this;
402                }
403
404                @NonNull
405                public Builder corsAuthorizer(@Nullable CorsAuthorizer corsAuthorizer) {
406                        this.corsAuthorizer = corsAuthorizer;
407                        return this;
408                }
409
410                @NonNull
411                public SokletConfig build() {
412                        if (this.httpServer == null && this.sseServer == null && this.mcpServer == null)
413                                throw new IllegalStateException(format("At least one of %s, %s, or %s must be configured",
414                                                HttpServer.class.getSimpleName(), SseServer.class.getSimpleName(), McpServer.class.getSimpleName()));
415
416                        return new SokletConfig(this);
417                }
418        }
419
420        /**
421         * Builder used to copy instances of {@link SokletConfig}.
422         * <p>
423         * Instances are created by invoking {@link SokletConfig#copy()}.
424         * <p>
425         * This class is intended for use by a single thread.
426         *
427         * @author <a href="https://www.revetkn.com">Mark Allen</a>
428         */
429        @NotThreadSafe
430        public static final class Copier {
431                @NonNull
432                private final Builder builder;
433
434                /**
435                 * Unwraps a HttpServer proxy to get the underlying real implementation.
436                 */
437                @NonNull
438                private static HttpServer unwrapHttpServer(@NonNull HttpServer httpServer) {
439                        requireNonNull(httpServer);
440
441                        if (httpServer instanceof HttpServerProxy)
442                                return ((HttpServerProxy) httpServer).getRealImplementation();
443
444                        return httpServer;
445                }
446
447                /**
448                 * Unwraps a SseServer proxy to get the underlying real implementation.
449                 */
450                @NonNull
451                private static SseServer unwrapSseServer(@NonNull SseServer sseServer) {
452                        requireNonNull(sseServer);
453
454                        if (sseServer instanceof SseServerProxy)
455                                return ((SseServerProxy) sseServer).getRealImplementation();
456
457                        return sseServer;
458                }
459
460                /**
461                 * Unwraps an McpServer proxy to get the underlying real implementation.
462                 */
463                @NonNull
464                private static McpServer unwrapMcpServer(@NonNull McpServer mcpServer) {
465                        requireNonNull(mcpServer);
466
467                        if (mcpServer instanceof McpServerProxy)
468                                return ((McpServerProxy) mcpServer).getRealImplementation();
469
470                        return mcpServer;
471                }
472
473                Copier(@NonNull SokletConfig sokletConfig) {
474                        requireNonNull(sokletConfig);
475
476                        // Unwrap proxies to get the real implementations for copying
477                        HttpServer realHttpServer = sokletConfig.getHttpServer()
478                                        .map(Copier::unwrapHttpServer)
479                                        .orElse(null);
480                        SseServer realSseServer = sokletConfig.getSseServer()
481                                        .map(Copier::unwrapSseServer)
482                                        .orElse(null);
483                        McpServer realMcpServer = sokletConfig.getMcpServer()
484                                        .map(Copier::unwrapMcpServer)
485                                        .orElse(null);
486
487                        this.builder = new Builder()
488                                        .httpServer(realHttpServer)
489                                        .sseServer(realSseServer)
490                                        .mcpServer(realMcpServer)
491                                        .instanceProvider(sokletConfig.getInstanceProvider())
492                                        .valueConverterRegistry(sokletConfig.valueConverterRegistry)
493                                        .requestBodyMarshaler(sokletConfig.requestBodyMarshaler)
494                                        .resourceMethodResolver(sokletConfig.resourceMethodResolver)
495                                        .resourceMethodParameterProvider(sokletConfig.resourceMethodParameterProvider)
496                                        .responseMarshaler(sokletConfig.responseMarshaler)
497                                        .requestInterceptor(sokletConfig.requestInterceptor)
498                                        .lifecycleObservers(sokletConfig.lifecycleObservers)
499                                        .metricsCollector(sokletConfig.metricsCollector)
500                                        .corsAuthorizer(sokletConfig.corsAuthorizer);
501                }
502
503                @NonNull
504                public Copier httpServer(@Nullable HttpServer httpServer) {
505                        this.builder.httpServer(httpServer);
506                        return this;
507                }
508
509                @NonNull
510                public Copier sseServer(@Nullable SseServer sseServer) {
511                        this.builder.sseServer(sseServer);
512                        return this;
513                }
514
515                @NonNull
516                public Copier mcpServer(@Nullable McpServer mcpServer) {
517                        this.builder.mcpServer(mcpServer);
518                        return this;
519                }
520
521                @NonNull
522                public Copier instanceProvider(@Nullable InstanceProvider instanceProvider) {
523                        this.builder.instanceProvider(instanceProvider);
524                        return this;
525                }
526
527                @NonNull
528                public Copier valueConverterRegistry(@Nullable ValueConverterRegistry valueConverterRegistry) {
529                        this.builder.valueConverterRegistry(valueConverterRegistry);
530                        return this;
531                }
532
533                @NonNull
534                public Copier requestBodyMarshaler(@Nullable RequestBodyMarshaler requestBodyMarshaler) {
535                        this.builder.requestBodyMarshaler(requestBodyMarshaler);
536                        return this;
537                }
538
539                @NonNull
540                public Copier resourceMethodResolver(@Nullable ResourceMethodResolver resourceMethodResolver) {
541                        this.builder.resourceMethodResolver(resourceMethodResolver);
542                        return this;
543                }
544
545                @NonNull
546                public Copier resourceMethodParameterProvider(@Nullable ResourceMethodParameterProvider resourceMethodParameterProvider) {
547                        this.builder.resourceMethodParameterProvider(resourceMethodParameterProvider);
548                        return this;
549                }
550
551                @NonNull
552                public Copier responseMarshaler(@Nullable ResponseMarshaler responseMarshaler) {
553                        this.builder.responseMarshaler(responseMarshaler);
554                        return this;
555                }
556
557                @NonNull
558                public Copier requestInterceptor(@Nullable RequestInterceptor requestInterceptor) {
559                        this.builder.requestInterceptor(requestInterceptor);
560                        return this;
561                }
562
563                @NonNull
564                public Copier lifecycleObserver(@Nullable LifecycleObserver lifecycleObserver) {
565                        this.builder.lifecycleObserver(lifecycleObserver);
566                        return this;
567                }
568
569                @NonNull
570                public Copier lifecycleObservers(@Nullable Collection<? extends LifecycleObserver> lifecycleObservers) {
571                        this.builder.lifecycleObservers(lifecycleObservers);
572                        return this;
573                }
574
575                @NonNull
576                public Copier metricsCollector(@Nullable MetricsCollector metricsCollector) {
577                        this.builder.metricsCollector(metricsCollector);
578                        return this;
579                }
580
581                @NonNull
582                public Copier corsAuthorizer(@Nullable CorsAuthorizer corsAuthorizer) {
583                        this.builder.corsAuthorizer(corsAuthorizer);
584                        return this;
585                }
586
587                @NonNull
588                public SokletConfig finish() {
589                        return this.builder.build();
590                }
591        }
592
593        @NonNull
594        private static List<LifecycleObserver> copyLifecycleObservers(@Nullable Collection<? extends LifecycleObserver> lifecycleObservers) {
595                if (lifecycleObservers == null || lifecycleObservers.isEmpty())
596                        return List.of();
597
598                return List.copyOf(lifecycleObservers);
599        }
600}