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