Skip to content

Commit 73b6984

Browse files
authored
Merge pull request #1374 from Avalon-Lab/2.x
Provide Route attributes for MVC and Script API.
2 parents 2decf04 + 8f903ae commit 73b6984

File tree

10 files changed

+292
-8
lines changed

10 files changed

+292
-8
lines changed

docs/asciidoc/routing.adoc

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,77 @@ A javadoc:Route[] consists of three part:
6161
The javadoc:Route.Handler[text="handler"] function always produces a result, which is send it back
6262
to the client.
6363

64+
==== Route attributes
65+
66+
Attributes let you annotate a route at application bootstrap time. It functions like static metadata available at runtime:
67+
.Java
68+
[source, java, role="primary"]
69+
----
70+
{
71+
get("/foo", ctx -> "Foo")
72+
.attribute("foo", "bar");
73+
}
74+
----
75+
An attribute consist of a name and value. Values can be any object.
76+
Attributes can be accessed at runtime in a request/response cycle. For example, a security module might check for a role attribute.
77+
.Java
78+
[source, java, role="primary"]
79+
----
80+
{
81+
decorator(next -> ctx -> {
82+
User user = ...;
83+
String role = ctx.getRoute().attribute("Role");
84+
85+
if (user.hasRole(role)) {
86+
return next.apply(ctx);
87+
}
88+
89+
throw new StatusCodeException(StatusCode.FORBIDDEN);
90+
});
91+
}
92+
----
93+
94+
In MVC routes you can set attributes via annotations:
95+
.Java
96+
[source, java, role="primary"]
97+
----
98+
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
99+
@Retention(RetentionPolicy.RUNTIME)
100+
public @interface Role {
101+
String value();
102+
}
103+
104+
@Path("/path")
105+
public class AdminResource {
106+
107+
@Role("admin")
108+
public Object doSomething() {
109+
...
110+
}
111+
112+
}
113+
114+
{
115+
decorator(next -> ctx -> {
116+
System.out.println(ctx.getRoute().attribute("Role"))
117+
});
118+
}
119+
----
120+
The previous example will print: admin.
121+
You can retrieve all the attributes of the route by calling `ctx.getRoute().getAttributes()`.
122+
123+
Any runtime annotation is automatically added as route attributes following these rules:
124+
- If the annotation has a value method, then we use the annotation’s name as the attribute name.
125+
- Otherwise, we use the method name as the attribute name.
126+
127+
.request attributes vs route attributes
128+
[NOTE]
129+
====
130+
Route attributes are created at bootstrap. They are global, and once set, they won’t change.
131+
On the other hand, request attributes are created in a request/response cycle.
132+
====
133+
134+
64135
=== Path Pattern
65136

66137
==== Static

jooby/src/main/java/io/jooby/Route.java

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@
1111
import javax.annotation.Nullable;
1212
import java.io.Serializable;
1313
import java.lang.reflect.Type;
14-
import java.util.ArrayList;
15-
import java.util.Arrays;
1614
import java.util.Collection;
1715
import java.util.Collections;
16+
import java.util.Set;
1817
import java.util.HashSet;
19-
import java.util.List;
2018
import java.util.Map;
21-
import java.util.Set;
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.ArrayList;
22+
import java.util.Arrays;
23+
2224

2325
/**
2426
* Route contains information about the HTTP method, path pattern, which content types consumes and
@@ -334,6 +336,8 @@ public interface Handler extends Serializable {
334336

335337
private List<MediaType> consumes = EMPTY_LIST;
336338

339+
private Map<String, Object> attributes = EMPTY_MAP;
340+
337341
private Set<String> supportedMethod;
338342

339343
/**
@@ -618,6 +622,58 @@ public Route(@Nonnull String method, @Nonnull String pattern, @Nonnull Handler h
618622
return this;
619623
}
620624

625+
/**
626+
* Attributes set to this route.
627+
*
628+
* @return Map of attributes set to the route.
629+
*/
630+
public @Nonnull Map<String, Object> getAttributes() {
631+
return attributes;
632+
}
633+
634+
/**
635+
* Retrieve value of this specific Attribute set to this route.
636+
*
637+
* @param name of the attribute to retrieve.
638+
* @return value of the specific attribute.
639+
*/
640+
public Object attribute(String name) {
641+
return attributes.get(name);
642+
}
643+
644+
/**
645+
* Add one or more attributes applied to this route.
646+
*
647+
* @param attributes .
648+
* @return This route.
649+
*/
650+
public @Nonnull Route setAttributes(@Nonnull Map<String, Object> attributes) {
651+
if (attributes.size() > 0) {
652+
if (this.attributes == EMPTY_MAP) {
653+
this.attributes = new HashMap<>();
654+
}
655+
this.attributes.putAll(attributes);
656+
}
657+
return this;
658+
}
659+
660+
/**
661+
* Add one or more attributes applied to this route.
662+
*
663+
* @param name attribute name
664+
* @param value attribute value
665+
* @return This route.
666+
*/
667+
public @Nonnull Route attribute(@Nonnull String name, @Nonnull Object value) {
668+
if (this.attributes == EMPTY_MAP) {
669+
this.attributes = new HashMap<>();
670+
}
671+
672+
this.attributes.put(name, value);
673+
674+
return this;
675+
}
676+
621677
/**
622678
* MessageDecoder for given media type.
623679
*

jooby/src/main/java/io/jooby/internal/RouterImpl.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import javax.annotation.Nonnull;
3939
import javax.inject.Provider;
4040
import java.io.FileNotFoundException;
41+
import java.lang.annotation.Annotation;
4142
import java.lang.reflect.Method;
4243
import java.lang.reflect.Modifier;
4344
import java.nio.file.Path;
@@ -592,6 +593,11 @@ private void mvc(String prefix, Class type, Provider provider) {
592593
if (consumes.size() > 0) {
593594
route.setConsumes(consumes);
594595
}
596+
597+
Map<String, Object> attributes = model.getAttributes();
598+
if (attributes.size() > 0) {
599+
route.setAttributes(attributes);
600+
}
595601
});
596602
}
597603

jooby/src/main/java/io/jooby/internal/mvc/JaxrsAnnotationParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public class JaxrsAnnotationParser extends MvcAnnotationParserBase {
4444
@Override protected MvcAnnotation create(Method method, Annotation annotation) {
4545
MvcAnnotation result = new MvcAnnotation(annotation.annotationType().getSimpleName(),
4646
path(method),
47-
produces(method), consumes(method));
47+
produces(method), consumes(method), attributes(method));
4848
result.setCookieParam(CookieParam.class);
4949
result.setHeaderParam(HeaderParam.class);
5050
result.setPathParam(PathParam.class);

jooby/src/main/java/io/jooby/internal/mvc/JoobyAnnotationParser.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ public class JoobyAnnotationParser extends MvcAnnotationParserBase {
4545

4646
@Override protected MvcAnnotation create(Method method, Annotation annotation) {
4747
MvcAnnotation result = new MvcAnnotation(annotation.annotationType().getSimpleName(),
48-
path(method, annotation),
49-
produces(annotation), consumes(annotation));
48+
path(method, annotation), produces(annotation),
49+
consumes(annotation), attributes(method));
5050
result.setCookieParam(CookieParam.class);
5151
result.setHeaderParam(HeaderParam.class);
5252
result.setPathParam(PathParam.class);

jooby/src/main/java/io/jooby/internal/mvc/MvcAnnotation.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.lang.reflect.Parameter;
1313
import java.util.Collections;
1414
import java.util.List;
15+
import java.util.Map;
1516
import java.util.stream.Collectors;
1617
import java.util.stream.Stream;
1718

@@ -24,6 +25,8 @@ public class MvcAnnotation {
2425

2526
private List<MediaType> consumes;
2627

28+
private Map<String, Object> attributes;
29+
2730
private Class<? extends Annotation> headerParam;
2831

2932
private Class<? extends Annotation> cookieParam;
@@ -36,11 +39,12 @@ public class MvcAnnotation {
3639

3740
private Class<? extends Annotation> flashParam;
3841

39-
public MvcAnnotation(String method, String[] path, String[] produces, String[] consumes) {
42+
public MvcAnnotation(String method, String[] path, String[] produces, String[] consumes, Map<String, Object> attributes) {
4043
this.method = method;
4144
this.path = path;
4245
this.produces = types(produces);
4346
this.consumes = types(consumes);
47+
this.attributes = attributes;
4448
}
4549

4650
private List<MediaType> types(String[] values) {
@@ -68,6 +72,10 @@ public List<MediaType> getConsumes() {
6872
return consumes;
6973
}
7074

75+
public Map<String, Object> getAttributes() {
76+
return attributes;
77+
}
78+
7179
public boolean isPathParam(Parameter parameter) {
7280
return parameter.getAnnotation(pathParam) != null;
7381
}
@@ -134,4 +142,5 @@ public String getName(Parameter parameter) {
134142
throw SneakyThrows.propagate(x);
135143
}
136144
}
145+
137146
}

jooby/src/main/java/io/jooby/internal/mvc/MvcAnnotationParserBase.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@
55
*/
66
package io.jooby.internal.mvc;
77

8+
import io.jooby.SneakyThrows;
9+
810
import java.lang.annotation.Annotation;
911
import java.lang.reflect.Method;
1012
import java.util.ArrayList;
13+
import java.util.LinkedHashMap;
1114
import java.util.List;
15+
import java.util.Map;
16+
import java.util.stream.Collectors;
17+
import java.util.stream.Stream;
1218

1319
public abstract class MvcAnnotationParserBase implements MvcAnnotationParser {
1420

@@ -50,4 +56,45 @@ protected String[] merge(String[] parent, String[] path) {
5056
}
5157
return result;
5258
}
59+
60+
protected Map<String, Object> attributes(Method method) {
61+
Map<String, Object> attributes = new LinkedHashMap<>();
62+
63+
List<Annotation> specificAttributes = Stream.of(method.getDeclaredAnnotations())
64+
.filter(annotation ->
65+
!"io.jooby.annotations".equals(annotation.annotationType().getPackage().getName())
66+
&& !"javax.ws.rs".equals(annotation.annotationType().getPackage().getName()))
67+
.collect(Collectors.toList());
68+
69+
specificAttributes.forEach(annotation -> attributes.putAll(extractAttributes(annotation)));
70+
71+
return attributes;
72+
}
73+
74+
protected Map<String, Object> extractAttributes(Annotation annotation) {
75+
Map<String, Object> annotationAttributes = new LinkedHashMap<>();
76+
77+
Method[] attributesMethod = annotation.annotationType().getDeclaredMethods();
78+
for (Method attribute : attributesMethod) {
79+
try {
80+
Object value = attribute.invoke(annotation);
81+
annotationAttributes.put(attributeName(attribute, annotation), value);
82+
} catch (Exception x) {
83+
throw SneakyThrows.propagate(x);
84+
}
85+
}
86+
87+
return annotationAttributes;
88+
}
89+
90+
protected String attributeName(Method attr, Annotation annotation) {
91+
String name = attr.getName();
92+
String context = annotation.annotationType().getSimpleName();
93+
94+
if (name.equals("value")) {
95+
return context;
96+
}
97+
98+
return context + "." + name;
99+
}
53100
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package examples;
2+
3+
import io.jooby.annotations.GET;
4+
import io.jooby.annotations.Path;
5+
6+
@Path("/attr")
7+
public class MvcAttributes {
8+
9+
@GET
10+
@Path("/secured/subpath")
11+
@Role(value = "Admin", level = "two")
12+
public String subpath() {
13+
return "Got it!!";
14+
}
15+
16+
@GET
17+
@Path("/secured/otherpath")
18+
@Role("User")
19+
public String otherpath() {
20+
return "OK!";
21+
}
22+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package examples;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Retention(RetentionPolicy.RUNTIME)
9+
@Target(ElementType.METHOD)
10+
public @interface Role {
11+
String value();
12+
13+
String level() default "one";
14+
}

0 commit comments

Comments
 (0)