Ether Framework
Unified API docs for Ether modules
Loading...
Searching...
No Matches
NonBlockingResourceHandler.java
Go to the documentation of this file.
1package dev.rafex.ether.http.jetty12;
2
3/*-
4 * #%L
5 * ether-http-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.LinkedHashMap;
31import java.util.List;
32import java.util.Map;
33
34import org.eclipse.jetty.server.Handler;
35import org.eclipse.jetty.server.Request;
36import org.eclipse.jetty.server.Response;
37import org.eclipse.jetty.util.Callback;
38import org.eclipse.jetty.util.MultiMap;
39import org.eclipse.jetty.util.UrlEncoded;
40
41import dev.rafex.ether.json.JsonCodec;
42import dev.rafex.ether.http.core.DefaultErrorMapper;
43import dev.rafex.ether.http.core.ErrorMapper;
44import dev.rafex.ether.http.core.HttpResource;
45import dev.rafex.ether.http.core.Route;
46import dev.rafex.ether.http.core.RouteMatcher;
47
48public abstract class NonBlockingResourceHandler extends Handler.Abstract.NonBlocking implements HttpResource {
49
50 private final JsonCodec jsonCodec;
51 private final ErrorMapper errorMapper;
52 private final JettyApiErrorResponses errorResponses;
53
54 protected NonBlockingResourceHandler(final JsonCodec jsonCodec) {
55 this(jsonCodec, new DefaultErrorMapper());
56 }
57
58 protected NonBlockingResourceHandler(final JsonCodec jsonCodec, final ErrorMapper errorMapper) {
59 this.jsonCodec = jsonCodec;
60 this.errorMapper = errorMapper;
61 this.errorResponses = new JettyApiErrorResponses(jsonCodec);
62 }
63
64 protected abstract String basePath();
65
66 protected List<Route> routes() {
67 return List.of(Route.of("/", supportedMethods()));
68 }
69
70 @Override
71 public boolean handle(final Request request, final Response response, final Callback callback) {
72 final var path = request.getHttpURI().getPath();
73 if (!matchesBasePath(path)) {
74 return false;
75 }
76
77 final var relPath = normalizeRelPath(path);
78 final var match = RouteMatcher.match(relPath, routes());
79 if (match.isEmpty()) {
80 errorResponses.notFound(response, callback, path);
81 return true;
82 }
83
84 final var routeMatch = match.get();
85 final var x = new JettyHttpExchange(request, response, callback, routeMatch.pathParams(), parseQueryMap(request),
86 routeMatch.route().allowedMethods(), jsonCodec);
87
88 final var method = request.getMethod().toUpperCase();
89 if (!routeMatch.route().allows(method) && !"OPTIONS".equals(method)) {
90 x.methodNotAllowed();
91 return true;
92 }
93
94 try {
95 return dispatch(method, x);
96 } catch (final Exception e) {
97 final var mapped = errorMapper.map(e);
98 errorResponses.error(response, callback, mapped, path);
99 return true;
100 }
101 }
102
103 private boolean dispatch(final String method, final JettyHttpExchange x) throws Exception {
104 return switch (method) {
105 case "GET" -> get(x);
106 case "POST" -> post(x);
107 case "PUT" -> put(x);
108 case "DELETE" -> delete(x);
109 case "PATCH" -> patch(x);
110 case "OPTIONS" -> options(x);
111 default -> {
112 x.methodNotAllowed();
113 yield true;
114 }
115 };
116 }
117
118 private String normalizeRelPath(final String absolutePath) {
119 final var base = basePath();
120 if (absolutePath.length() == base.length()) {
121 return "/";
122 }
123 final var rel = absolutePath.substring(base.length());
124 return rel.isEmpty() ? "/" : rel;
125 }
126
127 private boolean matchesBasePath(final String path) {
128 final var base = basePath();
129 if ("/".equals(base)) {
130 return path != null && path.startsWith("/");
131 }
132 if (base.equals(path)) {
133 return true;
134 }
135 return path.startsWith(base + "/");
136 }
137
138 private static Map<String, List<String>> parseQueryMap(final Request request) {
139 final MultiMap<String> params = new MultiMap<>();
140 final var rawQuery = request.getHttpURI().getQuery();
141 if (rawQuery != null && !rawQuery.isEmpty()) {
142 UrlEncoded.decodeTo(rawQuery, params, StandardCharsets.UTF_8);
143 }
144
145 final var out = new LinkedHashMap<String, List<String>>();
146 for (final var key : params.keySet()) {
147 final var values = params.getValues(key);
148 out.put(key, values == null ? List.of() : List.copyOf(values));
149 }
150 return out;
151 }
152
153 protected static String queryParam(final JettyHttpExchange x, final String key) {
154 final var value = x.queryFirst(key);
155 if (value == null || value.isBlank()) {
156 return null;
157 }
158 return value;
159 }
160}
static Optional< RouteMatch > match(final String relPath, final List< Route > routes)
static String queryParam(final JettyHttpExchange x, final String key)
NonBlockingResourceHandler(final JsonCodec jsonCodec, final ErrorMapper errorMapper)
boolean handle(final Request request, final Response response, final Callback callback)
default Set< String > supportedMethods()
default boolean patch(final HttpExchange x)
default boolean delete(final HttpExchange x)
default boolean put(final HttpExchange x)
default boolean options(final HttpExchange x)
default boolean post(final HttpExchange x)