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