Skip to content
This repository was archived by the owner on Mar 3, 2026. It is now read-only.

Commit 4734f2f

Browse files
committed
Implement Router Stack
1 parent 47aac17 commit 4734f2f

10 files changed

Lines changed: 300 additions & 59 deletions

File tree

jooby-core/src/main/java/jooby/Jooby.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ public class Jooby {
309309
private Config config;
310310

311311
/** The logging system. */
312-
private final Logger log = LoggerFactory.getLogger(getClass());
312+
protected final Logger log = LoggerFactory.getLogger(getClass());
313313

314314
/** Keep the global injector instance. */
315315
private Injector injector;

jooby-core/src/main/java/jooby/Response.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,5 @@ default ContentNegotiation when(final String mediaType, final Provider provider)
175175
*/
176176
@Nonnull Response status(@Nonnull HttpStatus status);
177177

178+
boolean committed();
178179
}

jooby-core/src/main/java/jooby/internal/ResponseImpl.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public abstract class ResponseImpl implements Response {
4949

5050
private ListMultimap<String, String> headers;
5151

52+
private boolean committed;
53+
5254
public ResponseImpl(final Request request,
5355
final BodyConverterSelector selector,
5456
final Set<RouteInterceptor> interceptors,
@@ -68,7 +70,14 @@ public ResponseImpl(final Request request,
6870
@Override
6971
public HttpHeader header(final String name) {
7072
checkArgument(!Strings.isNullOrEmpty(name), "Header's name is missing.");
71-
return new SetHeader(name, headers);
73+
return new SetHeader(name, headers.get(name), (values) -> {
74+
setHeader(name, values);
75+
});
76+
}
77+
78+
@Override
79+
public boolean committed() {
80+
return committed || doCommitted();
7281
}
7382

7483
@Override
@@ -174,6 +183,7 @@ public HttpStatus status() {
174183
@Override
175184
public Response status(final HttpStatus status) {
176185
this.status = requireNonNull(status, "A status is required.");
186+
this.committed = true;
177187
setStatus(status);
178188
return this;
179189
}
@@ -198,6 +208,8 @@ void reset() {
198208

199209
protected abstract void doReset();
200210

211+
protected abstract boolean doCommitted();
212+
201213
protected abstract void setStatus(HttpStatus status);
202214

203215
protected abstract void setContentType(MediaType contentType);

jooby-core/src/main/java/jooby/internal/RouteHandler.java

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.io.PrintWriter;
66
import java.io.StringWriter;
77
import java.nio.charset.Charset;
8+
import java.util.ArrayList;
89
import java.util.Arrays;
910
import java.util.LinkedHashMap;
1011
import java.util.List;
@@ -149,13 +150,14 @@ private void doHandle(final String verb, final String requestURI,
149150
}
150151
});
151152

152-
final Optional<RouteDescriptor> descriptor = routes(routes, path, contentType, accept);
153+
final List<RouteDescriptor> descriptors = routerChain(routes, verb, requestURI, contentType,
154+
accept);
153155

154-
final List<MediaType> produces = descriptor.map(d -> d.produces).orElse(accept);
156+
final List<MediaType> produces = descriptors.size() > 0 ? descriptors.get(0).produces : accept;
155157
log.trace(" produces: {}", produces);
156158

157159
final Request request = reqFactory.newRequest(injector,
158-
descriptor.map(d -> d.matcher).orElse(noMatch(requestURI)),
160+
descriptors.size() > 0 ? descriptors.get(0).matcher : noMatch(requestURI),
159161
selector,
160162
charset,
161163
contentType,
@@ -174,13 +176,13 @@ private void doHandle(final String verb, final String requestURI,
174176
interceptor.before(request, response);
175177
}
176178

177-
RouteDefinition routeDefinition = descriptor
178-
.orElseThrow(() -> sendError(routes, verb, requestURI, contentType, accept))
179-
.definition;
180-
Route route = routeDefinition.route();
179+
for (int idx = 0; idx < descriptors.size() && !response.committed(); idx++) {
180+
RouteDefinition routeDefinition = descriptors.get(idx).definition;
181+
Route route = routeDefinition.route();
181182

182-
// invoke route
183-
route.handle(request, response);
183+
// invoke route
184+
route.handle(request, response);
185+
}
184186

185187
// after callback
186188
for (RouteInterceptor interceptor : interceptors) {
@@ -218,7 +220,7 @@ private void doHandle(final String verb, final String requestURI,
218220
}
219221
}
220222

221-
private RouteMatcher noMatch(final String path) {
223+
private static RouteMatcher noMatch(final String path) {
222224
return new RouteMatcher() {
223225

224226
@Override
@@ -233,20 +235,52 @@ public boolean matches() {
233235
};
234236
}
235237

236-
private static Optional<RouteDescriptor> routes(final Set<RouteDefinition> routes,
238+
private static List<RouteDescriptor> routerChain(final Set<RouteDefinition> routes,
239+
final String verb,
240+
final String requestURI,
241+
final MediaType contentType,
242+
final List<MediaType> accept) {
243+
String path = verb + requestURI;
244+
List<RouteDescriptor> routers = routers(routes, path, contentType, accept);
245+
246+
// 406 or 415
247+
routers.add(new RouteDescriptor(new RouteDefinitionImpl(verb, path, (req, resp) -> {
248+
HttpException ex = throw406or415(routes, verb, requestURI, contentType, accept);
249+
if (ex != null) {
250+
throw ex;
251+
}
252+
}), noMatch(path), accept));
253+
254+
// 405
255+
routers.add(new RouteDescriptor(new RouteDefinitionImpl(verb, path, (req, resp) -> {
256+
HttpException ex = throw405(routes, verb, requestURI, contentType, accept);
257+
if (ex != null) {
258+
throw ex;
259+
}
260+
}), noMatch(path), accept));
261+
262+
// 404
263+
routers.add(new RouteDescriptor(new RouteDefinitionImpl(verb, path, (req, resp) -> {
264+
throw new HttpException(HttpStatus.NOT_FOUND, path);
265+
}), noMatch(path), accept));
266+
return routers;
267+
}
268+
269+
private static List<RouteDescriptor> routers(final Set<RouteDefinition> routes,
237270
final String path,
238271
final MediaType contentType,
239272
final List<MediaType> accept) {
273+
List<RouteDescriptor> routers = new ArrayList<RouteDescriptor>();
240274
for (RouteDefinition route : routes) {
241275
RouteMatcher matcher = route.matcher(path);
242276
if (matcher.matches()) {
243277
List<MediaType> produces = MediaType.matcher(accept).filter(route.produces());
244278
if (route.canConsume(contentType) && produces.size() > 0) {
245-
return Optional.of(new RouteDescriptor(route, matcher, produces));
279+
routers.add(new RouteDescriptor(route, matcher, produces));
246280
}
247281
}
248282
}
249-
return Optional.empty();
283+
return routers;
250284
}
251285

252286
private void defaultErrorPage(final Request request, final Response response,
@@ -363,8 +397,8 @@ private static HttpException throw405(final Set<RouteDefinition> routes,
363397
verbs.remove(verb);
364398
for (String candidate : verbs) {
365399
String path = candidate + requestURI;
366-
Optional<RouteDescriptor> holder = routes(routes, path, contentType, accept);
367-
if (holder.isPresent()) {
400+
List<RouteDescriptor> routers = routers(routes, path, contentType, accept);
401+
if (routers.size() > 0) {
368402
return new HttpException(HttpStatus.METHOD_NOT_ALLOWED, verb + requestURI);
369403
}
370404
}
@@ -378,7 +412,7 @@ private static HttpException throw406or415(final Set<RouteDefinition> routes,
378412
String path = verb + requestURI;
379413
for (RouteDefinition route : routes) {
380414
RouteMatcher matcher = route.matcher(path);
381-
if (matcher.matches()) {
415+
if (matcher.matches() && !route.path().pattern().endsWith("/**/*")) {
382416
if (!route.canProduce(accept)) {
383417
return new HttpException(HttpStatus.NOT_ACCEPTABLE, accept.stream()
384418
.map(MediaType::name)
@@ -390,16 +424,6 @@ private static HttpException throw406or415(final Set<RouteDefinition> routes,
390424
return null;
391425
}
392426

393-
private HttpException sendError(final Set<RouteDefinition> routes,
394-
final String method, final String requestURI,
395-
final MediaType contentType,
396-
final List<MediaType> accept) {
397-
return Optional.ofNullable(
398-
Optional.ofNullable(throw406or415(routes, method, requestURI, contentType, accept))
399-
.orElseGet(() -> throw405(routes, method, requestURI, contentType, accept))
400-
).orElseGet(() -> new HttpException(HttpStatus.NOT_FOUND, method + requestURI));
401-
}
402-
403427
@Override
404428
public String toString() {
405429
StringBuilder buffer = new StringBuilder();

jooby-core/src/main/java/jooby/internal/SetHeader.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,31 @@
66
import java.time.format.DateTimeFormatter;
77
import java.util.ArrayList;
88
import java.util.List;
9+
import java.util.function.Consumer;
910

1011
import jooby.HttpHeader;
1112

12-
import com.google.common.collect.ListMultimap;
13+
import com.google.common.collect.ImmutableList;
1314

1415
public class SetHeader extends GetHeader implements HttpHeader {
1516

16-
private ListMultimap<String, String> headers;
17+
private Consumer<Iterable<String>> setter;
1718

18-
public SetHeader(final String name, final ListMultimap<String, String> headers) {
19-
super(name, headers.get(name));
20-
this.headers = headers;
19+
public SetHeader(final String name, final List<String> value,
20+
final Consumer<Iterable<String>> setter) {
21+
super(name, value);
22+
this.setter = setter;
2123
}
2224

2325
@Override
2426
public HttpHeader setString(final String value) {
25-
headers.removeAll(name);
26-
headers.put(name, value);
27+
setter.accept(ImmutableList.of(value));
2728
return this;
2829
}
2930

3031
@Override
3132
public HttpHeader setString(final Iterable<String> values) {
32-
headers.removeAll(name);
33-
headers.putAll(name, values);
33+
setter.accept(values);
3434
return null;
3535
}
3636

jooby-core/src/main/java/jooby/internal/mvc/MvcRoute.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,16 @@ public void handle(final Request request, final Response response) throws Except
3636
List<Param> parameters = provider.parameters(method);
3737
Object[] args = new Object[parameters.size()];
3838
for (int i = 0; i < parameters.size(); i++) {
39-
args[i] = parameters.get(i).get(request);
39+
args[i] = parameters.get(i).get(request, response);
4040
}
4141

4242
final Object result = route.invoke(handler, args);
4343

44+
Class<?> returnType = method.getReturnType();
45+
if (returnType == void.class || returnType == Void.class) {
46+
// move on!
47+
return;
48+
}
4449
// negotiate!
4550
List<MediaType> accept = request.accept();
4651

jooby-core/src/main/java/jooby/internal/mvc/Param.java

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import jooby.HttpException;
1010
import jooby.HttpStatus;
1111
import jooby.Request;
12+
import jooby.Response;
1213
import jooby.mvc.Body;
1314
import jooby.mvc.Header;
1415

@@ -19,41 +20,59 @@ public class Param {
1920
private enum Strategy {
2021
PARAM {
2122
@Override
22-
public Object get(final Request request, final Param param) throws Exception {
23-
return request.param(param.name).get(
23+
public Object get(final Request req, final Response resp, final Param param) throws Exception {
24+
return req.param(param.name).get(
2425
TypeLiteral.get(param.parameter.getParameterizedType()));
2526
}
2627
},
2728

2829
BODY {
2930
@Override
30-
public Object get(final Request request, final Param param) throws Exception {
31-
return request.body(TypeLiteral.get(param.parameter.getParameterizedType()));
31+
public Object get(final Request req, final Response resp, final Param param) throws Exception {
32+
return req.body(TypeLiteral.get(param.parameter.getParameterizedType()));
3233
}
3334
},
3435

3536
HEADER {
3637
@Override
37-
public Object get(final Request request, final Param param) throws Exception {
38-
return request.header(param.name).get(
38+
public Object get(final Request req, final Response resp, final Param param) throws Exception {
39+
return req.header(param.name).get(
3940
TypeLiteral.get(param.parameter.getParameterizedType()));
4041
}
4142
},
4243

4344
COOKIE {
4445
@Override
45-
public Object get(final Request request, final Param param) throws Exception {
46-
Cookie cookie = request.cookie(param.name);
46+
public Object get(final Request req, final Response resp, final Param param)
47+
throws Exception {
48+
Cookie cookie = req.cookie(param.name);
4749
if (param.parameter.getType() == Optional.class) {
4850
return Optional.ofNullable(cookie);
4951
}
5052
return Optional.ofNullable(cookie)
5153
.orElseThrow(
5254
() -> new HttpException(HttpStatus.BAD_REQUEST, "Missing cookie: " + param.name));
5355
}
56+
},
57+
58+
REQUEST {
59+
@Override
60+
public Object get(final Request request, final Response resp, final Param param)
61+
throws Exception {
62+
return request;
63+
}
64+
},
65+
66+
RESPONSE {
67+
@Override
68+
public Object get(final Request request, final Response resp, final Param param)
69+
throws Exception {
70+
return resp;
71+
}
5472
};
5573

56-
public abstract Object get(final Request request, final Param param) throws Exception;
74+
public abstract Object get(final Request req, Response resp, final Param param)
75+
throws Exception;
5776

5877
}
5978

@@ -72,18 +91,19 @@ public Param(final String name, final Parameter parameter) {
7291
this.strategy = Strategy.BODY;
7392
} else if (parameter.getAnnotation(Header.class) != null) {
7493
strategy = Strategy.HEADER;
94+
} else if (parameter.getType() == Response.class) {
95+
strategy = Strategy.RESPONSE;
96+
} else if (parameter.getType() == Request.class) {
97+
strategy = Strategy.REQUEST;
98+
} else if (parameter.getType() == Cookie.class) {
99+
strategy = Strategy.COOKIE;
75100
} else {
76-
// param or cookie
77-
if (parameter.getType() == Cookie.class) {
78-
strategy = Strategy.COOKIE;
79-
} else {
80-
strategy = Strategy.PARAM;
81-
}
101+
strategy = Strategy.PARAM;
82102
}
83103
}
84104

85-
public Object get(final Request request) throws Exception {
86-
return strategy.get(request, this);
105+
public Object get(final Request request, final Response response) throws Exception {
106+
return strategy.get(request, response, this);
87107
}
88108

89109
@Override

jooby-core/src/main/java/jooby/internal/mvc/Routes.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,6 @@ public static List<RouteDefinition> route(final Mode mode, final Class<?> routeC
7070
+ " should have only one HTTP verb. Found: "
7171
+ annotations);
7272
}
73-
Class<?> returnType = m.getReturnType();
74-
if (returnType == void.class) {
75-
throw new IllegalStateException("A resouce method must have a return value: " + m);
76-
}
7773
return true;
7874
})
7975
.map(

jooby-jetty/src/main/java/jooby/internal/jetty/JettyResponse.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,4 +269,8 @@ protected void doReset() {
269269
response.reset();
270270
}
271271

272+
@Override
273+
protected boolean doCommitted() {
274+
return response.isCommitted();
275+
}
272276
}

0 commit comments

Comments
 (0)