Skip to content

Commit ce54bb7

Browse files
authored
Merge pull request #1555 from franklx/2.x
Annotations inheritance in MVC routes #1552
2 parents facce39 + 21092de commit ce54bb7

8 files changed

Lines changed: 394 additions & 11 deletions

File tree

modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import io.jooby.MvcFactory;
99
import io.jooby.SneakyThrows;
10+
import io.jooby.annotations.Path;
1011
import io.jooby.internal.apt.HandlerCompiler;
1112
import io.jooby.internal.apt.ModuleCompiler;
1213

@@ -17,8 +18,11 @@
1718
import javax.lang.model.SourceVersion;
1819
import javax.lang.model.element.AnnotationMirror;
1920
import javax.lang.model.element.Element;
21+
import javax.lang.model.element.ElementKind;
2022
import javax.lang.model.element.ExecutableElement;
2123
import javax.lang.model.element.TypeElement;
24+
import javax.lang.model.type.DeclaredType;
25+
import javax.lang.model.util.Elements;
2226
import javax.tools.FileObject;
2327
import javax.tools.JavaFileObject;
2428
import javax.tools.StandardLocation;
@@ -27,12 +31,15 @@
2731
import java.io.PrintWriter;
2832
import java.util.ArrayList;
2933
import java.util.Collections;
34+
import java.util.HashMap;
35+
import java.util.LinkedHashSet;
3036
import java.util.List;
3137
import java.util.Map;
3238
import java.util.Set;
3339
import java.util.stream.Collectors;
3440
import java.util.stream.Stream;
3541

42+
3643
/**
3744
* Jooby Annotation Processing Tool. It generates byte code for MVC routes.
3845
*
@@ -44,8 +51,24 @@ public class JoobyProcessor extends AbstractProcessor {
4451

4552
private List<String> moduleList = new ArrayList<>();
4653

54+
private Set<TypeElement> pathAnnotations;
55+
private Set<TypeElement> httpAnnotations;
56+
57+
final class MVCMethod {
58+
public ExecutableElement method;
59+
public TypeElement httpMethod;
60+
61+
MVCMethod(ExecutableElement method, TypeElement httpMethod) {
62+
this.method = method;
63+
this.httpMethod = httpMethod;
64+
}
65+
}
66+
4767
@Override public Set<String> getSupportedAnnotationTypes() {
48-
return Annotations.HTTP_METHODS;
68+
return new LinkedHashSet<String>() {{
69+
addAll(Annotations.HTTP_METHODS);
70+
addAll(Annotations.PATH);
71+
}};
4972
}
5073

5174
@Override public SourceVersion getSupportedSourceVersion() {
@@ -54,6 +77,24 @@ public class JoobyProcessor extends AbstractProcessor {
5477

5578
@Override public void init(ProcessingEnvironment processingEnvironment) {
5679
this.processingEnvironment = processingEnvironment;
80+
81+
Elements eltUtil = processingEnvironment.getElementUtils();
82+
this.pathAnnotations = new LinkedHashSet<TypeElement>() {{
83+
for (String s: Annotations.PATH) {
84+
TypeElement t = eltUtil.getTypeElement(s);
85+
if (t != null) {
86+
add(t);
87+
}
88+
}
89+
}};
90+
this.httpAnnotations = new LinkedHashSet<TypeElement>() {{
91+
for (String s: Annotations.HTTP_METHODS) {
92+
TypeElement t = eltUtil.getTypeElement(s);
93+
if (t != null) {
94+
add(t);
95+
}
96+
}
97+
}};
5798
}
5899

59100
@Override
@@ -63,22 +104,57 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
63104
doServices(processingEnvironment.getFiler());
64105
return false;
65106
}
107+
108+
JoobyProcessorRoundEnvironment joobyRoundEnv = new JoobyProcessorRoundEnvironment(roundEnv, processingEnvironment);
109+
110+
Map<TypeElement, List<MVCMethod>> classMap = new HashMap<>();
66111
/**
67112
* Do MVC handler: per each mvc method we create a Route.Handler.
68113
*/
69114
List<HandlerCompiler> result = new ArrayList<>();
115+
116+
/**
117+
* If @Path annotation is present force inspecting all http mthods.
118+
*/
119+
if (annotations.retainAll(this.pathAnnotations)) {
120+
annotations = httpAnnotations;
121+
}
122+
70123
for (TypeElement httpMethod : annotations) {
71-
Set<? extends Element> methods = roundEnv.getElementsAnnotatedWith(httpMethod);
124+
Set<? extends Element> methods = joobyRoundEnv.getElementsAnnotatedWith(httpMethod);
72125
for (Element e : methods) {
73126
ExecutableElement method = (ExecutableElement) e;
127+
TypeElement cls = (TypeElement) method.getEnclosingElement();
128+
TypeElement superCls = (TypeElement) ((DeclaredType) cls.getSuperclass()).asElement();
129+
superCls.getEnclosedElements();
130+
if (!classMap.containsKey(cls)) {
131+
classMap.put(cls, new ArrayList<>());
132+
}
74133
List<String> paths = path(httpMethod, method);
75134
for (String path : paths) {
76-
HandlerCompiler compiler = new HandlerCompiler(processingEnvironment, method,
77-
httpMethod, path);
135+
HandlerCompiler compiler = new HandlerCompiler(processingEnvironment, method, httpMethod, path);
78136
result.add(compiler);
79137
}
138+
classMap.get(cls).add(new MVCMethod(method, httpMethod));
139+
}
140+
}
141+
142+
Set<? extends Element> pathAnnotatedElements = roundEnv.getElementsAnnotatedWith(Path.class);
143+
for (Element c : pathAnnotatedElements) {
144+
if (c.getKind() == ElementKind.CLASS) {
145+
TypeElement newOwner = (TypeElement) c;
146+
TypeElement oldOwner = (TypeElement) ((DeclaredType) newOwner.getSuperclass()).asElement();
147+
if (classMap.containsKey(oldOwner)) {
148+
for (MVCMethod e : classMap.get(oldOwner)) {
149+
for (String path : path(e.httpMethod, e.method, newOwner)) {
150+
HandlerCompiler compiler = new HandlerCompiler(processingEnvironment, e.method, newOwner, e.httpMethod, path);
151+
result.add(compiler);
152+
}
153+
}
154+
}
80155
}
81156
}
157+
82158
Filer filer = processingEnvironment.getFiler();
83159
Map<String, List<HandlerCompiler>> classes = result.stream()
84160
.collect(Collectors.groupingBy(e -> e.getController().getName()));
@@ -125,8 +201,8 @@ private void writeClass(JavaFileObject javaFileObject, byte[] bytecode) throws I
125201
}
126202
}
127203

128-
private List<String> path(TypeElement method, ExecutableElement exec) {
129-
List<String> prefix = path(exec.getEnclosingElement());
204+
private List<String> path(TypeElement method, ExecutableElement exec, TypeElement owner) {
205+
List<String> prefix = path(owner);
130206
// Favor GET("/path") over Path("/path") at method level
131207
List<String> path = path(method.getQualifiedName().toString(), method.getAnnotationMirrors());
132208
if (path.size() == 0) {
@@ -145,6 +221,10 @@ private List<String> path(TypeElement method, ExecutableElement exec) {
145221
.collect(Collectors.toList());
146222
}
147223

224+
private List<String> path(TypeElement method, ExecutableElement exec) {
225+
return path(method, exec, (TypeElement) exec.getEnclosingElement());
226+
}
227+
148228
private List<String> path(Element element) {
149229
return path(null, element.getAnnotationMirrors());
150230
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.apt;
7+
8+
import javax.annotation.processing.ProcessingEnvironment;
9+
import javax.annotation.processing.RoundEnvironment;
10+
import javax.lang.model.element.AnnotationMirror;
11+
import javax.lang.model.element.Element;
12+
import javax.lang.model.element.ElementKind;
13+
import javax.lang.model.element.ExecutableElement;
14+
import javax.lang.model.element.TypeElement;
15+
import javax.lang.model.type.DeclaredType;
16+
import javax.lang.model.type.TypeKind;
17+
import javax.lang.model.util.ElementScanner8;
18+
import javax.lang.model.util.Elements;
19+
import javax.lang.model.util.Types;
20+
import java.util.ArrayList;
21+
import java.util.Collections;
22+
import java.util.LinkedHashSet;
23+
import java.util.List;
24+
import java.util.Set;
25+
26+
public class JoobyProcessorRoundEnvironment {
27+
28+
private final Elements eltUtils;
29+
private static Types typeUtils;
30+
private final Set<? extends Element> rootElements;
31+
32+
JoobyProcessorRoundEnvironment(RoundEnvironment roundEnv, ProcessingEnvironment processingEnv) {
33+
this.rootElements = roundEnv.getRootElements();
34+
this.eltUtils = processingEnv.getElementUtils();
35+
this.typeUtils = processingEnv.getTypeUtils();
36+
}
37+
38+
/**
39+
* Returns the elements annotated with the given annotation type.
40+
* Only type elements <i>included</i> in this round of annotation
41+
* processing, or declarations of members, parameters, or type
42+
* parameters declared within those, are returned. Included type
43+
* elements are {@linkplain #getRootElements specified
44+
* types} and any types nested within them.
45+
*
46+
* This implementation Jooby-specific and is based and supports
47+
* inherited MVC classes.
48+
*
49+
* @param a annotation type being requested
50+
* @return the elements annotated with the given annotation type,
51+
* or an empty set if there are none
52+
*/
53+
public Set<? extends Element> getElementsAnnotatedWith(TypeElement a) {
54+
55+
Set<Element> result = Collections.emptySet();
56+
ElementScanner8<Set<Element>, TypeElement> scanner = new JoobyAnnotationSetScanner(result);
57+
58+
for (Element element : rootElements) {
59+
result = scanner.scan(element, a);
60+
}
61+
62+
return result;
63+
}
64+
65+
private class JoobyAnnotationSetScanner extends ElementScanner8<Set<Element>, TypeElement> {
66+
private Set<Element> annotatedElements = new LinkedHashSet<>();
67+
68+
JoobyAnnotationSetScanner(Set<Element> defaultSet) {
69+
super(defaultSet);
70+
}
71+
72+
@Override
73+
public Set<Element> scan(Element e, TypeElement annotation) {
74+
for (AnnotationMirror annotMirror : eltUtils.getAllAnnotationMirrors(e)) {
75+
if (annotation.equals(mirrorAsElement(annotMirror))) {
76+
annotatedElements.add(e);
77+
break;
78+
}
79+
}
80+
e.accept(this, annotation);
81+
return annotatedElements;
82+
}
83+
84+
@Override
85+
public Set<Element> visitType(TypeElement e, TypeElement p) {
86+
if (e.getSuperclass().getKind() == TypeKind.DECLARED) {
87+
javax.lang.model.element.TypeElement superElement = (javax.lang.model.element.TypeElement) ((DeclaredType) e.getSuperclass()).asElement();
88+
List<Element> superElements = new ArrayList<>();
89+
for (Element enclosedElement : superElement.getEnclosedElements()) {
90+
if (enclosedElement.getKind() == ElementKind.METHOD && enclosedElement.getAnnotationMirrors().size() > 0) {
91+
superElements.add(enclosedElement);
92+
}
93+
}
94+
scan(superElements, p);
95+
}
96+
// Type parameters are not considered to be enclosed by a type
97+
scan(e.getTypeParameters(), p);
98+
return super.visitType(e, p);
99+
}
100+
101+
@Override
102+
public Set<Element> visitExecutable(ExecutableElement e, TypeElement p) {
103+
// Type parameters are not considered to be enclosed by an executable
104+
scan(e.getTypeParameters(), p);
105+
return super.visitExecutable(e, p);
106+
}
107+
}
108+
109+
private Element mirrorAsElement(AnnotationMirror annotationMirror) {
110+
return annotationMirror.getAnnotationType().asElement();
111+
}
112+
113+
}

modules/jooby-apt/src/main/java/io/jooby/internal/apt/HandlerCompiler.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ public class HandlerCompiler {
6060

6161
private static final Type CTX = getType(Context.class);
6262

63+
private final Element realOwnerElement;
64+
private final Element ownerElement;
6365
private final TypeDefinition owner;
6466
private final ExecutableElement executable;
6567
private final ProcessingEnvironment environment;
@@ -68,15 +70,22 @@ public class HandlerCompiler {
6870
private final Types typeUtils;
6971
private final TypeMirror annotation;
7072

71-
public HandlerCompiler(ProcessingEnvironment environment, ExecutableElement executable,
72-
TypeElement httpMethod, String pattern) {
73+
public HandlerCompiler(ProcessingEnvironment environment, ExecutableElement executable, TypeElement owner,
74+
TypeElement httpMethod, String pattern) {
7375
this.httpMethod = httpMethod.getSimpleName().toString().toLowerCase();
7476
this.annotation = httpMethod.asType();
7577
this.pattern = Router.leadingSlash(pattern);
7678
this.environment = environment;
7779
this.executable = executable;
7880
this.typeUtils = environment.getTypeUtils();
79-
this.owner = new TypeDefinition(typeUtils, executable.getEnclosingElement().asType());
81+
this.realOwnerElement = executable.getEnclosingElement();
82+
this.ownerElement = owner;
83+
this.owner = new TypeDefinition(typeUtils, owner.asType());
84+
}
85+
86+
public HandlerCompiler(ProcessingEnvironment environment, ExecutableElement executable,
87+
TypeElement httpMethod, String pattern) {
88+
this(environment, executable, (TypeElement) executable.getEnclosingElement(), httpMethod, pattern);
8089
}
8190

8291
public ExecutableElement getExecutable() {
@@ -313,7 +322,12 @@ private List<String> mediaType(Element element, Set<String> types) {
313322
.map(it -> Annotations.attribute(it, "value"))
314323
.orElseGet(() -> {
315324
if (element instanceof ExecutableElement) {
316-
return mediaType(element.getEnclosingElement(), types);
325+
if (element.getEnclosingElement() == realOwnerElement) {
326+
return mediaType(ownerElement, types);
327+
}
328+
else {
329+
return mediaType(element.getEnclosingElement(), types);
330+
}
317331
}
318332
return Collections.emptyList();
319333
});

modules/jooby-apt/src/test/java/io/jooby/apt/MvcModuleCompilerRunner.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,13 @@ private MvcModuleCompilerRunner module(boolean debug, SneakyThrows.Consumer<Joob
6363
Path services = Paths
6464
.get(classLoader.getResource("META-INF/services/" + MvcFactory.class.getName()).toURI());
6565
assertTrue(Files.exists(services));
66-
assertEquals(factoryName, new String(Files.readAllBytes(services), StandardCharsets.UTF_8).trim());
66+
67+
List<String> clsLst = new ArrayList<String>() {{
68+
for (Class c = clazz; c != null && c != Object.class; c = c.getSuperclass()) {
69+
add(c.getName() + "$Module");
70+
}
71+
}};
72+
assertEquals(String.join("\n", clsLst), new String(Files.readAllBytes(services), StandardCharsets.UTF_8).trim());
6773

6874
consumer.accept(application);
6975
return this;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package source;
2+
3+
import io.jooby.Context;
4+
import io.jooby.annotations.GET;
5+
import io.jooby.annotations.POST;
6+
import io.jooby.annotations.Path;
7+
8+
@Path("/inherited")
9+
public class Controller1552 extends Controller1552Base {
10+
@GET(path = "/childOnly")
11+
public String childOnly(Context ctx) {
12+
return ctx.getRequestPath();
13+
}
14+
15+
@POST(path = "/childOnly")
16+
public String childOnly_Post(Context ctx) {
17+
return ctx.getRequestPath();
18+
}
19+
}

0 commit comments

Comments
 (0)