Skip to content

Commit 8cfa97f

Browse files
committed
extends route attr to mvc routes fix #346
1 parent 9523f16 commit 8cfa97f

7 files changed

Lines changed: 196 additions & 9 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package org.jooby.issues;
2+
3+
import static org.junit.Assert.assertEquals;
4+
5+
import java.lang.annotation.ElementType;
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.RetentionPolicy;
8+
import java.lang.annotation.Target;
9+
10+
import org.jooby.Request;
11+
import org.jooby.mvc.GET;
12+
import org.jooby.mvc.Path;
13+
import org.jooby.test.ServerFeature;
14+
import org.junit.Test;
15+
16+
public class Issue346 extends ServerFeature {
17+
18+
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
19+
@Retention(RetentionPolicy.RUNTIME)
20+
public static @interface SitemapUrl {
21+
double priority() default 0.5d;
22+
23+
String changefreq() default "always";
24+
}
25+
26+
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
27+
@Retention(RetentionPolicy.RUNTIME)
28+
public static @interface Role {
29+
String value();
30+
}
31+
32+
@Path("/")
33+
@SitemapUrl
34+
public static class Resource {
35+
36+
@GET
37+
@SitemapUrl(priority = 1.0)
38+
@Path("/1")
39+
public Object sitemap1(final Request req) {
40+
return req.route().attributes();
41+
}
42+
43+
@GET
44+
@Path("/2")
45+
public Object sitemap2(final Request req) {
46+
return req.route().attributes();
47+
}
48+
49+
@GET
50+
@Role("admin")
51+
@Path("/3")
52+
public Object admin(final Request req) {
53+
return req.route().attributes();
54+
}
55+
}
56+
57+
{
58+
use("/1", (req, rsp) -> {
59+
assertEquals("{priority=1.0, changefreq=always}", req.route().attributes().toString());
60+
});
61+
62+
use("/2", (req, rsp) -> {
63+
assertEquals("{priority=0.5, changefreq=always}", req.route().attributes().toString());
64+
});
65+
66+
use("/3", (req, rsp) -> {
67+
assertEquals("{role=admin, priority=0.5, changefreq=always}",
68+
req.route().attributes().toString());
69+
});
70+
use(Resource.class);
71+
}
72+
73+
@Test
74+
public void mvcAttrs() throws Exception {
75+
request().get("/1")
76+
.expect("{priority=1.0, changefreq=always}");
77+
78+
request().get("/2")
79+
.expect("{priority=0.5, changefreq=always}");
80+
81+
request().get("/3")
82+
.expect("{role=admin, priority=0.5, changefreq=always}");
83+
}
84+
85+
}

jooby/src/main/java/org/jooby/Route.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,7 +1812,6 @@ interface Chain {
18121812
default void next(final Request req, final Response rsp) throws Throwable {
18131813
next(null, req, rsp);
18141814
}
1815-
18161815
}
18171816

18181817
/** Renderer key. */
@@ -1917,7 +1916,7 @@ default boolean apply(final String prefix) {
19171916
}
19181917

19191918
/**
1920-
* @return All the available attributes.
1919+
* @return All the available attributes in the execution chain.
19211920
*/
19221921
Map<String, String> attributes();
19231922

jooby/src/main/java/org/jooby/internal/RouteChain.java

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@
1919
package org.jooby.internal;
2020

2121
import java.util.List;
22+
import java.util.Map;
2223

2324
import org.jooby.Request;
2425
import org.jooby.Response;
2526
import org.jooby.Route;
2627

28+
import com.google.common.collect.ImmutableMap;
29+
2730
public class RouteChain implements Route.Chain {
2831

2932
private List<Route> routes;
@@ -36,10 +39,17 @@ public class RouteChain implements Route.Chain {
3639

3740
private ResponseImpl rrsp;
3841

42+
private boolean hasAttrs;
43+
3944
public RouteChain(final RequestImpl req, final ResponseImpl rsp, final List<Route> routes) {
4045
this.routes = routes;
4146
this.rreq = req;
4247
this.rrsp = rsp;
48+
49+
// eager decision if we need to wrap a route to get all the attrs within the change.
50+
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
51+
routes.forEach(r -> builder.putAll(r.attributes()));
52+
this.hasAttrs = builder.build().size() > 0;
4353
}
4454

4555
@Override
@@ -52,12 +62,12 @@ public void next(final String prefix, final Request req, final Response rsp) thr
5262
this.prefix = prefix;
5363
}
5464

55-
RouteImpl route = get(next(this.prefix));
65+
Route route = next(this.prefix);
5666
// set route
57-
rreq.route(route);
67+
rreq.route(hasAttrs ? attrs(route, routes, i - 1) : route);
5868
rrsp.route(route);
5969

60-
route.handle(req, rsp, this);
70+
get(route).handle(req, rsp, this);
6171
}
6272

6373
private Route next(final String prefix) {
@@ -75,4 +85,18 @@ private RouteImpl get(final Route next) {
7585
return (RouteImpl) Route.Forwarding.unwrap(next);
7686
}
7787

88+
private static Route attrs(final Route route, final List<Route> routes, final int i) {
89+
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
90+
for (int t = i; t < routes.size(); t++) {
91+
builder.putAll(routes.get(t).attributes());
92+
}
93+
Map<String, String> attrs = builder.build();
94+
return new Route.Forwarding(route) {
95+
@Override
96+
public Map<String, String> attributes() {
97+
return attrs;
98+
}
99+
};
100+
}
101+
78102
}

jooby/src/main/java/org/jooby/internal/mvc/MvcRoutes.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.lang.reflect.Modifier;
2525
import java.util.ArrayList;
2626
import java.util.HashMap;
27+
import java.util.LinkedHashMap;
2728
import java.util.List;
2829
import java.util.Map;
2930
import java.util.Optional;
@@ -48,8 +49,11 @@
4849
import org.jooby.mvc.Produces;
4950
import org.jooby.mvc.TRACE;
5051

52+
import com.google.common.base.CaseFormat;
5153
import com.google.common.collect.ImmutableSet;
5254

55+
import javaslang.control.Try;
56+
5357
public class MvcRoutes {
5458

5559
private static final String[] EMPTY = new String[0];
@@ -59,6 +63,14 @@ public class MvcRoutes {
5963
POST.class, PUT.class, DELETE.class, PATCH.class, HEAD.class, OPTIONS.class, TRACE.class,
6064
CONNECT.class);
6165

66+
private static final Set<Class<? extends Annotation>> IGNORE = ImmutableSet
67+
.<Class<? extends Annotation>> builder()
68+
.addAll(VERBS)
69+
.add(Path.class)
70+
.add(Produces.class)
71+
.add(Consumes.class)
72+
.build();
73+
6274
@SuppressWarnings({"unchecked", "rawtypes" })
6375
public static List<Route.Definition> routes(final Env env, final RouteMetadata classInfo,
6476
final String rpath, final Class<?> routeClass) {
@@ -87,7 +99,7 @@ public static List<Route.Definition> routes(final Env env, final RouteMetadata c
8799
}
88100

89101
List<Definition> definitions = new ArrayList<>();
90-
102+
Map<String, String> attrs = attrs(routeClass.getAnnotations());
91103
methods
92104
.keySet()
93105
.stream()
@@ -109,6 +121,8 @@ public static List<Route.Definition> routes(final Env env, final RouteMetadata c
109121
List<Class<?>> verbs = methods.get(method);
110122
List<MediaType> produces = produces(method);
111123
List<MediaType> consumes = consumes(method);
124+
Map<String, String> localAttrs = new HashMap<>(attrs);
125+
localAttrs.putAll(attrs(method.getAnnotations()));
112126

113127
for (String path : expandPaths(rootPaths, method)) {
114128
for (Class<?> verb : verbs) {
@@ -123,6 +137,7 @@ public static List<Route.Definition> routes(final Env env, final RouteMetadata c
123137
.excludes(excludes)
124138
.name(name);
125139

140+
localAttrs.forEach((n, v) -> definition.attr(n, v));
126141
definitions.add(definition);
127142
}
128143
}
@@ -131,6 +146,36 @@ public static List<Route.Definition> routes(final Env env, final RouteMetadata c
131146
return definitions;
132147
}
133148

149+
private static Map<String, String> attrs(final Annotation[] annotations) {
150+
Map<String, String> result = new LinkedHashMap<>();
151+
for (Annotation annotation : annotations) {
152+
result.putAll(attrs(annotation));
153+
}
154+
return result;
155+
}
156+
157+
private static Map<String, String> attrs(final Annotation annotation) {
158+
Map<String, String> result = new LinkedHashMap<>();
159+
Class<? extends Annotation> annotationType = annotation.annotationType();
160+
if (!IGNORE.contains(annotationType)) {
161+
Method[] attrs = annotation.annotationType().getDeclaredMethods();
162+
for (Method attr : attrs) {
163+
Try.of(() -> attr.invoke(annotation))
164+
.onSuccess(value -> result.put(attrName(annotation, attr), value.toString()));
165+
}
166+
}
167+
return result;
168+
}
169+
170+
private static String attrName(final Annotation annotation, final Method attr) {
171+
String name = attr.getName();
172+
if (name.equals("value")) {
173+
return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL,
174+
annotation.annotationType().getSimpleName());
175+
}
176+
return name;
177+
}
178+
134179
private static List<MediaType> produces(final Method method) {
135180
Function<AnnotatedElement, Optional<List<MediaType>>> fn = (element) -> {
136181
Produces produces = element.getAnnotation(Produces.class);

md/doc/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ documentation
2424

2525
{{dynamic-routing.md}}
2626

27+
{{route-attrs.md}}
28+
2729
{{req.md}}
2830

2931
{{rsp.md}}

md/route-attrs.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ You can add as many attributes as you need. They can be accessed later and use i
1515
{
1616
use((req, rsp, chain) -> {
1717
User user = ...;
18-
String role = req.route.attr("role");
18+
String role = req.route().attr("role");
1919
if (user.hasRole(role)) {
2020
chain.next(req, rsp);
2121
}
@@ -25,6 +25,40 @@ You can add as many attributes as you need. They can be accessed later and use i
2525
}
2626
```
2727

28+
In MVC routes we use ```annotations``` to define route attributes:
29+
30+
```java
31+
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
32+
@Retention(RetentionPolicy.RUNTIME)
33+
public static @interface Role {
34+
String value();
35+
}
36+
37+
@Path("/path")
38+
public class AdminResource {
39+
40+
@Role("admin")
41+
public Object doSomething() {
42+
...
43+
}
44+
45+
}
46+
47+
{
48+
use("*", (req, rsp) -> {
49+
System.out.println(req.route().attributes())
50+
});
51+
}
52+
53+
```
54+
55+
The previous example will print: ```{role = admin}```.
56+
57+
Any runtime annotations are automatically added as route attributes. Following these rules:
58+
59+
* If the annotation has a ```value``` method, then we use the annotation's name as attribute name.
60+
* Otherwise, we use the method name as attribute name.
61+
2862
### excludes
2963

3064
The excludes attribute skip/ignore a route path match:

md/routes.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ get("/", (req, rsp, chain) -> {
6666
});
6767
```
6868

69-
{{route-attrs.md}}
70-
7169
## path patterns
7270

7371
### static patterns

0 commit comments

Comments
 (0)