Ether Framework
Unified API docs for Ether modules
Loading...
Searching...
No Matches
JettyWebSocketSession.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.ByteBuffer;
30import java.util.Collections;
31import java.util.LinkedHashMap;
32import java.util.List;
33import java.util.Map;
34import java.util.Objects;
35import java.util.concurrent.CompletableFuture;
36import java.util.concurrent.CompletionStage;
37import java.util.concurrent.ConcurrentHashMap;
38import java.util.concurrent.atomic.AtomicLong;
39
40import org.eclipse.jetty.websocket.api.Callback;
41import org.eclipse.jetty.websocket.api.Session;
42
43import dev.rafex.ether.websocket.core.WebSocketCloseStatus;
44import dev.rafex.ether.websocket.core.WebSocketSession;
45
46/**
47 * Jetty 12 implementation of {@link WebSocketSession}.
48 *
49 * <p>Wraps the native Jetty {@link Session} and exposes the framework's
50 * transport-agnostic WebSocket session contract. Each instance carries the
51 * request path, path parameters, query parameters, headers and a
52 * thread-safe attribute bag.</p>
53 */
54public final class JettyWebSocketSession implements WebSocketSession {
55
56 private final Session session;
57 private final String path;
58 private final Map<String, String> pathParams;
59 private final Map<String, List<String>> queryParams;
60 private final Map<String, List<String>> headers;
61 private final Map<String, Object> attributes = new ConcurrentHashMap<>();
62 private final String id;
63
64 private static final AtomicLong ID_COUNTER = new AtomicLong(System.nanoTime());
65
66 /**
67 * Creates a session wrapper.
68 *
69 * @param session the native Jetty WebSocket session
70 * @param path the matched request path
71 * @param pathParams extracted path parameters (e.g. {@code "channel" -> "abc"})
72 * @param queryParams query string parameters
73 * @param headers HTTP headers from the upgrade request
74 * @throws NullPointerException if {@code session} or {@code path} is {@code null}
75 */
76 public JettyWebSocketSession(final Session session, final String path, final Map<String, String> pathParams,
77 final Map<String, List<String>> queryParams, final Map<String, List<String>> headers) {
78 this.session = Objects.requireNonNull(session, "session");
79 this.path = Objects.requireNonNull(path, "path");
80 this.pathParams = Map.copyOf(pathParams);
81 this.queryParams = copyMultiMap(queryParams);
82 this.headers = copyMultiMap(headers);
83 this.id = Long.toHexString(ID_COUNTER.incrementAndGet());
84 }
85
86 /**
87 * Returns a unique session identifier derived from the native session's
88 * identity hash code.
89 *
90 * @return a hex-encoded identity hash of the underlying session
91 */
92 @Override
93 public String id() {
94 return id;
95 }
96
97 /**
98 * Returns the matched request path.
99 *
100 * @return the request path
101 */
102 @Override
103 public String path() {
104 return path;
105 }
106
107 /**
108 * Returns the accepted subprotocol, or an empty string if none was negotiated.
109 *
110 * @return the negotiated subprotocol, or {@code ""}
111 */
112 @Override
113 public String subprotocol() {
114 final var protocol = session.getUpgradeResponse() == null ? null
115 : session.getUpgradeResponse().getAcceptedSubProtocol();
116 return protocol == null ? "" : protocol;
117 }
118
119 /**
120 * Returns whether the underlying session is still open.
121 *
122 * @return {@code true} if the session is open
123 */
124 @Override
125 public boolean isOpen() {
126 return session.isOpen();
127 }
128
129 /**
130 * Returns the value of a single path parameter by name.
131 *
132 * @param name the path parameter name
133 * @return the value, or {@code null} if the parameter does not exist
134 */
135 @Override
136 public String pathParam(final String name) {
137 return pathParams.get(name);
138 }
139
140 /**
141 * Returns the first value of a query parameter.
142 *
143 * @param name the query parameter name
144 * @return the first value, or {@code null} if absent
145 */
146 @Override
147 public String queryFirst(final String name) {
148 final var values = queryParams.get(name);
149 if (values == null || values.isEmpty()) {
150 return null;
151 }
152 return values.get(0);
153 }
154
155 /**
156 * Returns all values of a query parameter.
157 *
158 * @param name the query parameter name
159 * @return an unmodifiable list of values (never {@code null})
160 */
161 @Override
162 public List<String> queryAll(final String name) {
163 return queryParams.getOrDefault(name, List.of());
164 }
165
166 /**
167 * Returns the first value of an HTTP header.
168 *
169 * @param name the header name (case-sensitive)
170 * @return the first value, or {@code null} if absent
171 */
172 @Override
173 public String headerFirst(final String name) {
174 final var values = headers.get(name);
175 if (values == null || values.isEmpty()) {
176 return null;
177 }
178 return values.get(0);
179 }
180
181 /**
182 * Returns a session-scoped attribute by name.
183 *
184 * @param name the attribute name
185 * @return the attribute value, or {@code null} if not set
186 */
187 @Override
188 public Object attribute(final String name) {
189 return attributes.get(name);
190 }
191
192 /**
193 * Sets or removes a session-scoped attribute.
194 *
195 * <p>Passing {@code null} as the value removes the attribute.</p>
196 *
197 * @param name the attribute name
198 * @param value the attribute value, or {@code null} to remove
199 */
200 @Override
201 public void attribute(final String name, final Object value) {
202 if (value == null) {
203 attributes.remove(name);
204 return;
205 }
206 attributes.put(name, value);
207 }
208
209 /**
210 * Returns an unmodifiable view of all path parameters.
211 *
212 * @return the path parameter map
213 */
214 @Override
215 public Map<String, String> pathParams() {
216 return pathParams;
217 }
218
219 /**
220 * Returns an unmodifiable view of all query parameters.
221 *
222 * @return the query parameter map
223 */
224 @Override
225 public Map<String, List<String>> queryParams() {
226 return queryParams;
227 }
228
229 /**
230 * Returns an unmodifiable view of all HTTP headers.
231 *
232 * @return the header map
233 */
234 @Override
235 public Map<String, List<String>> headers() {
236 return headers;
237 }
238
239 /**
240 * Sends a text message asynchronously.
241 *
242 * @param text the text payload
243 * @return a completion stage that completes when the message is sent
244 */
245 @Override
246 public CompletionStage<Void> sendText(final String text) {
247 final var future = new CompletableFuture<Void>();
248 session.sendText(text, callbackOf(future));
249 return future;
250 }
251
252 /**
253 * Sends a binary message asynchronously.
254 *
255 * @param data the binary payload (an empty buffer is sent if {@code null})
256 * @return a completion stage that completes when the message is sent
257 */
258 @Override
259 public CompletionStage<Void> sendBinary(final ByteBuffer data) {
260 final var future = new CompletableFuture<Void>();
261 session.sendBinary(data == null ? ByteBuffer.allocate(0) : data.slice(), callbackOf(future));
262 return future;
263 }
264
265 /**
266 * Closes the session with the given status code and reason.
267 *
268 * @param status the close status; defaults to {@link WebSocketCloseStatus#NORMAL} if {@code null}
269 * @return a completion stage that completes when the close handshake finishes
270 */
271 @Override
272 public CompletionStage<Void> close(final WebSocketCloseStatus status) {
273 final var future = new CompletableFuture<Void>();
274 final var closeStatus = status == null ? WebSocketCloseStatus.NORMAL : status;
275 session.close(closeStatus.code(), closeStatus.reason(), callbackOf(future));
276 return future;
277 }
278
279 /**
280 * Adapts a {@link CompletableFuture} into a Jetty {@link Callback}.
281 *
282 * @param future the future to complete on success or failure
283 * @return a Jetty callback
284 */
285 private static Callback callbackOf(final CompletableFuture<Void> future) {
286 return Callback.from(() -> future.complete(null), future::completeExceptionally);
287 }
288
289 /**
290 * Returns an unmodifiable deep copy of the given multi-valued map.
291 *
292 * @param input the source map
293 * @return an unmodifiable copy
294 */
295 private static Map<String, List<String>> copyMultiMap(final Map<String, List<String>> input) {
296 final var out = new LinkedHashMap<String, List<String>>();
297 if (input != null) {
298 for (final var entry : input.entrySet()) {
299 out.put(entry.getKey(), entry.getValue() == null ? List.of() : List.copyOf(entry.getValue()));
300 }
301 }
302 return Collections.unmodifiableMap(out);
303 }
304}
List< String > queryAll(final String name)
Returns all values of a query parameter.
boolean isOpen()
Returns whether the underlying session is still open.
CompletionStage< Void > sendBinary(final ByteBuffer data)
Sends a binary message asynchronously.
CompletionStage< Void > close(final WebSocketCloseStatus status)
Closes the session with the given status code and reason.
CompletionStage< Void > sendText(final String text)
Sends a text message asynchronously.
Map< String, String > pathParams()
Returns an unmodifiable view of all path parameters.
Object attribute(final String name)
Returns a session-scoped attribute by name.
String subprotocol()
Returns the accepted subprotocol, or an empty string if none was negotiated.
String pathParam(final String name)
Returns the value of a single path parameter by name.
Map< String, List< String > > queryParams()
Returns an unmodifiable view of all query parameters.
JettyWebSocketSession(final Session session, final String path, final Map< String, String > pathParams, final Map< String, List< String > > queryParams, final Map< String, List< String > > headers)
Creates a session wrapper.
String queryFirst(final String name)
Returns the first value of a query parameter.
void attribute(final String name, final Object value)
Sets or removes a session-scoped attribute.
Map< String, List< String > > headers()
Returns an unmodifiable view of all HTTP headers.
String headerFirst(final String name)
Returns the first value of an HTTP header.
String id()
Returns a unique session identifier derived from the native session's identity hash code.
Representa una sesión WebSocket activa.