Ether Framework
Unified API docs for Ether modules
Loading...
Searching...
No Matches
JettyWebSocketServerFactory.java
Go to the documentation of this file.
1package dev.rafex.ether.websocket.jetty12;
2
3/*-
4 * #%L
5 * ether-websocket-jetty12
6 * %%
7 * Copyright (C) 2025 - 2026 Raúl Eduardo González Argote
8 * %%
9 * Permission is hereby granted, free of charge, to any person obtaining a copy
10 * of this software and associated documentation files (the "Software"), to deal
11 * in the Software without restriction, including without limitation the rights
12 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 * copies of the Software, and to permit persons to whom the Software is
14 * furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included in
17 * all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 * THE SOFTWARE.
26 * #L%
27 */
28
29import java.nio.charset.StandardCharsets;
30import java.util.ArrayList;
31import java.util.LinkedHashMap;
32import java.util.List;
33import java.util.Map;
34import java.util.Objects;
35
36import org.eclipse.jetty.http.pathmap.PathSpec;
37import org.eclipse.jetty.server.Handler;
38import org.eclipse.jetty.server.Server;
39import org.eclipse.jetty.server.ServerConnector;
40import org.eclipse.jetty.server.handler.ContextHandler;
41import org.eclipse.jetty.util.MultiMap;
42import org.eclipse.jetty.util.UrlEncoded;
43import org.eclipse.jetty.util.thread.QueuedThreadPool;
44import org.eclipse.jetty.websocket.server.ServerUpgradeRequest;
45import org.eclipse.jetty.websocket.server.ServerUpgradeResponse;
46import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler;
47
48import dev.rafex.ether.websocket.core.WebSocketPatterns;
49import dev.rafex.ether.websocket.core.WebSocketRoute;
50
51/**
52 * Factory that wires a Jetty {@link Server} with WebSocket routing.
53 *
54 * <p>This utility class provides static factory methods to create a
55 * {@link JettyWebSocketServerRunner} from a configuration and either a
56 * pre-built route registry or a list of {@link JettyWebSocketModule} instances.</p>
57 */
58public final class JettyWebSocketServerFactory {
59
60 private JettyWebSocketServerFactory() {
61 }
62
63 /**
64 * Creates a WebSocket server from the given configuration and route registry.
65 *
66 * <p>Configures a {@link QueuedThreadPool}, a {@link ServerConnector}, and a
67 * {@link WebSocketUpgradeHandler} that maps incoming upgrade requests to the
68 * registered endpoints.</p>
69 *
70 * @param config the server configuration (port, threads, timeout)
71 * @param routeRegistry the registry containing the WebSocket routes
72 * @return a runner that owns the configured {@link Server}
73 * @throws NullPointerException if {@code config} or {@code routeRegistry} is {@code null}
74 */
76 final JettyWebSocketRouteRegistry routeRegistry) {
77 Objects.requireNonNull(config, "config");
78 Objects.requireNonNull(routeRegistry, "routeRegistry");
79
80 final var pool = new QueuedThreadPool();
81 pool.setMaxThreads(config.maxThreads());
82 pool.setMinThreads(config.minThreads());
83 pool.setIdleTimeout(config.idleTimeoutMs());
84 pool.setName(config.threadPoolName());
85
86 final var server = new Server(pool);
87 final var connector = new ServerConnector(server);
88 connector.setPort(config.port());
89 server.addConnector(connector);
90
91 final var context = new ContextHandler("/");
92 context.setHandler(buildUpgradeHandler(server, routeRegistry.routes()));
93 server.setHandler(context);
94 return new JettyWebSocketServerRunner(server);
95 }
96
97 /**
98 * Creates a WebSocket server by invoking all given modules to populate the
99 * route registry, then delegates to {@link #create(JettyWebSocketServerConfig,
100 * JettyWebSocketRouteRegistry)}.
101 *
102 * @param config the server configuration
103 * @param modules the list of modules whose {@code registerRoutes} will be called
104 * @return a runner that owns the configured {@link Server}
105 */
107 final List<JettyWebSocketModule> modules) {
108 final var routeRegistry = new JettyWebSocketRouteRegistry();
109 final var context = new JettyWebSocketModuleContext(config);
110 for (final var module : modules == null ? List.<JettyWebSocketModule>of() : modules) {
111 module.registerRoutes(routeRegistry, context);
112 }
113 return create(config, routeRegistry);
114 }
115
116 /**
117 * Builds a {@link WebSocketUpgradeHandler} that maps each route's path spec
118 * to its endpoint adapter.
119 *
120 * @param server the Jetty server instance
121 * @param routes the WebSocket routes to register
122 * @return a configured upgrade handler
123 */
124 private static Handler buildUpgradeHandler(final Server server, final List<WebSocketRoute> routes) {
125 return WebSocketUpgradeHandler.from(server, container -> {
126 for (final var route : routes == null ? List.<WebSocketRoute>of() : new ArrayList<>(routes)) {
127 container.addMapping(PathSpec.from(route.pattern()),
128 (request, response, callback) -> createEndpoint(route, request, response));
129 }
130 });
131 }
132
133 /**
134 * Creates the endpoint adapter for a WebSocket upgrade request by extracting
135 * path params, headers, query params and negotiating subprotocols.
136 *
137 * @param route the matched route
138 * @param request the upgrade request
139 * @param response the upgrade response
140 * @return the endpoint adapter instance
141 */
142 private static Object createEndpoint(final WebSocketRoute route, final ServerUpgradeRequest request,
143 final ServerUpgradeResponse response) {
144 final var path = request.getHttpURI().getPath();
145 final var pathParams = WebSocketPatterns.match(route.pattern(), path).orElse(Map.of());
146 final var headers = headersOf(request);
147 final var queryParams = queryOf(request);
148 negotiateSubprotocol(route, request, response);
149 return new JettyWebSocketEndpointAdapter(route.endpoint(), path, pathParams, queryParams, headers);
150 }
151
152 /**
153 * Negotiates a subprotocol if the endpoint supports any. Picks the first
154 * requested subprotocol that the endpoint declares as supported.
155 *
156 * @param route the matched route
157 * @param request the upgrade request
158 * @param response the upgrade response to set the accepted subprotocol on
159 */
160 private static void negotiateSubprotocol(final WebSocketRoute route, final ServerUpgradeRequest request,
161 final ServerUpgradeResponse response) {
162 final var supported = route.endpoint().subprotocols();
163 if (supported == null || supported.isEmpty()) {
164 return;
165 }
166 for (final var requested : request.getSubProtocols()) {
167 if (supported.contains(requested)) {
168 response.setAcceptedSubProtocol(requested);
169 return;
170 }
171 }
172 }
173
174 /**
175 * Extracts HTTP headers from the upgrade request into an unmodifiable map.
176 *
177 * @param request the upgrade request
178 * @return an unmodifiable map of header name to list of values
179 */
180 private static Map<String, List<String>> headersOf(final ServerUpgradeRequest request) {
181 final var out = new LinkedHashMap<String, List<String>>();
182 for (final var field : request.getHeaders()) {
183 out.computeIfAbsent(field.getName(), ignored -> new ArrayList<>()).add(field.getValue());
184 }
185 return copyMultiMap(out);
186 }
187
188 /**
189 * Decodes the query string from the upgrade request into an unmodifiable map.
190 *
191 * @param request the upgrade request
192 * @return an unmodifiable map of query parameter name to list of values
193 */
194 private static Map<String, List<String>> queryOf(final ServerUpgradeRequest request) {
195 final MultiMap<String> params = new MultiMap<>();
196 final var rawQuery = request.getHttpURI().getQuery();
197 if (rawQuery != null && !rawQuery.isEmpty()) {
198 UrlEncoded.decodeTo(rawQuery, params, StandardCharsets.UTF_8);
199 }
200
201 final var out = new LinkedHashMap<String, List<String>>();
202 for (final var key : params.keySet()) {
203 final var values = params.getValues(key);
204 out.put(key, values == null ? List.of() : List.copyOf(values));
205 }
206 return Map.copyOf(out);
207 }
208
209 /**
210 * Returns an unmodifiable deep copy of the given multi-valued map.
211 *
212 * @param input the source map
213 * @return an unmodifiable copy
214 */
215 private static Map<String, List<String>> copyMultiMap(final Map<String, List<String>> input) {
216 final var out = new LinkedHashMap<String, List<String>>();
217 for (final var entry : input.entrySet()) {
218 out.put(entry.getKey(), entry.getValue() == null ? List.of() : List.copyOf(entry.getValue()));
219 }
220 return Map.copyOf(out);
221 }
222}
Mutable registry that collects WebSocket routes during module registration.
static JettyWebSocketServerRunner create(final JettyWebSocketServerConfig config, final JettyWebSocketRouteRegistry routeRegistry)
Creates a WebSocket server from the given configuration and route registry.
static JettyWebSocketServerRunner create(final JettyWebSocketServerConfig config, final List< JettyWebSocketModule > modules)
Creates a WebSocket server by invoking all given modules to populate the route registry,...
Thin wrapper around a Jetty Server that exposes lifecycle methods.
SPI contract for WebSocket modules that register endpoint routes.
Typed configuration entry points and composition APIs for Ether applications.
record JettyWebSocketServerConfig(int port, int minThreads, int maxThreads, int idleTimeoutMs, String threadPoolName)
Immutable configuration for the Jetty WebSocket server.
record JettyWebSocketModuleContext(JettyWebSocketServerConfig config)
Read-only context passed to JettyWebSocketModule#registerRoutes.