Skip to content

Commit 1716ba2

Browse files
committed
MVC/apt: finish inheritance support
1 parent c6f0c73 commit 1716ba2

13 files changed

+296
-276
lines changed

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

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

88
import io.jooby.MvcFactory;
99
import io.jooby.SneakyThrows;
10-
import io.jooby.annotations.Path;
1110
import io.jooby.internal.apt.HandlerCompiler;
1211
import io.jooby.internal.apt.ModuleCompiler;
1312

@@ -20,9 +19,10 @@
2019
import javax.lang.model.element.Element;
2120
import javax.lang.model.element.ElementKind;
2221
import javax.lang.model.element.ExecutableElement;
22+
import javax.lang.model.element.Modifier;
2323
import javax.lang.model.element.TypeElement;
24-
import javax.lang.model.type.DeclaredType;
25-
import javax.lang.model.util.Elements;
24+
import javax.lang.model.type.TypeMirror;
25+
import javax.lang.model.util.Types;
2626
import javax.tools.FileObject;
2727
import javax.tools.JavaFileObject;
2828
import javax.tools.StandardLocation;
@@ -31,164 +31,197 @@
3131
import java.io.PrintWriter;
3232
import java.util.ArrayList;
3333
import java.util.Collections;
34-
import java.util.HashMap;
34+
import java.util.LinkedHashMap;
3535
import java.util.LinkedHashSet;
3636
import java.util.List;
3737
import java.util.Map;
3838
import java.util.Set;
3939
import java.util.stream.Collectors;
4040
import java.util.stream.Stream;
4141

42-
4342
/**
4443
* Jooby Annotation Processing Tool. It generates byte code for MVC routes.
4544
*
4645
* @since 2.1.0
4746
*/
4847
public class JoobyProcessor extends AbstractProcessor {
4948

50-
private ProcessingEnvironment processingEnvironment;
51-
52-
private List<String> moduleList = new ArrayList<>();
49+
private ProcessingEnvironment processingEnv;
5350

54-
private Set<TypeElement> pathAnnotations;
55-
private Set<TypeElement> httpAnnotations;
51+
private Map<Element, Map<TypeElement, List<ExecutableElement>>> routeMap = new LinkedHashMap<>();
5652

57-
final class MVCMethod {
58-
public ExecutableElement method;
59-
public TypeElement httpMethod;
53+
private boolean debug;
6054

61-
MVCMethod(ExecutableElement method, TypeElement httpMethod) {
62-
this.method = method;
63-
this.httpMethod = httpMethod;
64-
}
65-
}
55+
private int round;
6656

6757
@Override public Set<String> getSupportedAnnotationTypes() {
68-
return new LinkedHashSet<String>() {{
69-
addAll(Annotations.HTTP_METHODS);
70-
addAll(Annotations.PATH);
71-
}};
58+
return Stream.concat(Annotations.PATH.stream(), Annotations.HTTP_METHODS.stream())
59+
.collect(Collectors.toCollection(LinkedHashSet::new));
7260
}
7361

7462
@Override public SourceVersion getSupportedSourceVersion() {
7563
return SourceVersion.latestSupported();
7664
}
7765

7866
@Override public void init(ProcessingEnvironment processingEnvironment) {
79-
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-
}};
67+
this.processingEnv = processingEnvironment;
68+
debug = Boolean.parseBoolean(processingEnvironment.getOptions().getOrDefault("debug", "false"));
9869
}
9970

10071
@Override
10172
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
10273
try {
74+
debug("Round #%s", round++);
10375
if (roundEnv.processingOver()) {
104-
doServices(processingEnvironment.getFiler());
76+
build(processingEnv.getFiler());
77+
10578
return false;
10679
}
10780

108-
JoobyProcessorRoundEnvironment joobyRoundEnv = new JoobyProcessorRoundEnvironment(roundEnv, processingEnvironment);
109-
110-
Map<TypeElement, List<MVCMethod>> classMap = new HashMap<>();
11181
/**
11282
* Do MVC handler: per each mvc method we create a Route.Handler.
11383
*/
114-
List<HandlerCompiler> result = new ArrayList<>();
84+
for (TypeElement annotation : annotations) {
85+
Set<? extends Element> elements = roundEnv
86+
.getElementsAnnotatedWith(annotation);
11587

116-
/**
117-
* If @Path annotation is present force inspecting all http mthods.
118-
*/
119-
if (annotations.retainAll(this.pathAnnotations)) {
120-
annotations = httpAnnotations;
121-
}
88+
/**
89+
* Add empty-subclass (edge case where you mark something with @Path and didn't add any
90+
* HTTP annotation.
91+
*/
92+
elements.stream()
93+
.filter(TypeElement.class::isInstance)
94+
.map(TypeElement.class::cast)
95+
.filter(type -> !type.getModifiers().contains(Modifier.ABSTRACT))
96+
.forEach(e -> routeMap.computeIfAbsent(e, k -> new LinkedHashMap<>()));
12297

123-
for (TypeElement httpMethod : annotations) {
124-
Set<? extends Element> methods = joobyRoundEnv.getElementsAnnotatedWith(httpMethod);
125-
for (Element e : methods) {
126-
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-
}
133-
List<String> paths = path(httpMethod, method);
134-
for (String path : paths) {
135-
HandlerCompiler compiler = new HandlerCompiler(processingEnvironment, method, httpMethod, path);
136-
result.add(compiler);
98+
if (Annotations.HTTP_METHODS.contains(annotation.asType().toString())) {
99+
List<ExecutableElement> methods = elements.stream()
100+
.filter(ExecutableElement.class::isInstance)
101+
.map(ExecutableElement.class::cast)
102+
.collect(Collectors.toList());
103+
for (ExecutableElement method : methods) {
104+
Map<TypeElement, List<ExecutableElement>> mapping = routeMap
105+
.computeIfAbsent(method.getEnclosingElement(), k -> new LinkedHashMap<>());
106+
mapping.computeIfAbsent(annotation, k -> new ArrayList<>()).add(method);
137107
}
138-
classMap.get(cls).add(new MVCMethod(method, httpMethod));
139108
}
140109
}
141110

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);
111+
return true;
112+
} catch (Exception x) {
113+
throw SneakyThrows.propagate(x);
114+
}
115+
}
116+
117+
private void build(Filer filer) throws Exception {
118+
Types typeUtils = processingEnv.getTypeUtils();
119+
Map<String, List<HandlerCompiler>> classes = new LinkedHashMap<>();
120+
for (Map.Entry<Element, Map<TypeElement, List<ExecutableElement>>> e : routeMap
121+
.entrySet()) {
122+
Element type = e.getKey();
123+
boolean isAbstract = type.getModifiers().contains(Modifier.ABSTRACT);
124+
/** Ignore abstract routes: */
125+
if (!isAbstract) {
126+
/** Expand route method from superclass(es): */
127+
Map<TypeElement, List<ExecutableElement>> mappings = e.getValue();
128+
for (Element superType : superTypes(type)) {
129+
Map<TypeElement, List<ExecutableElement>> baseMappings = routeMap
130+
.getOrDefault(superType, Collections.emptyMap());
131+
for (Map.Entry<TypeElement, List<ExecutableElement>> be : baseMappings.entrySet()) {
132+
List<ExecutableElement> methods = mappings.get(be.getKey());
133+
if (methods == null) {
134+
mappings.put(be.getKey(), be.getValue());
135+
} else {
136+
for (ExecutableElement it : be.getValue()) {
137+
String signature = signature(it);
138+
if (!methods.stream().map(this::signature).anyMatch(signature::equals)) {
139+
methods.add(it);
140+
}
152141
}
153142
}
154143
}
155144
}
145+
String typeName = typeUtils.erasure(type.asType()).toString();
146+
/** Route method ready, creates a Route.Handler for each of them: */
147+
for (Map.Entry<TypeElement, List<ExecutableElement>> mapping : mappings.entrySet()) {
148+
TypeElement httpMethod = mapping.getKey();
149+
List<ExecutableElement> methods = mapping.getValue();
150+
for (ExecutableElement method : methods) {
151+
debug("Found method %s.%s", type, method);
152+
List<String> paths = path(type, httpMethod, method);
153+
for (String path : paths) {
154+
debug(" route %s %s", httpMethod.getSimpleName(), path);
155+
HandlerCompiler compiler = new HandlerCompiler(processingEnv, type, method,
156+
httpMethod, path);
157+
classes.computeIfAbsent(typeName, k -> new ArrayList<>())
158+
.add(compiler);
159+
}
160+
}
161+
}
156162
}
163+
}
157164

158-
Filer filer = processingEnvironment.getFiler();
159-
Map<String, List<HandlerCompiler>> classes = result.stream()
160-
.collect(Collectors.groupingBy(e -> e.getController().getName()));
165+
List<String> moduleList = new ArrayList<>();
166+
for (Map.Entry<String, List<HandlerCompiler>> entry : classes.entrySet()) {
167+
List<HandlerCompiler> handlers = entry.getValue();
168+
ModuleCompiler module = new ModuleCompiler(processingEnv, entry.getKey());
169+
String moduleClass = module.getModuleClass();
170+
byte[] moduleBin = module.compile(handlers);
171+
onClass(moduleClass, moduleBin);
172+
writeClass(filer.createClassFile(moduleClass), moduleBin);
161173

162-
for (Map.Entry<String, List<HandlerCompiler>> entry : classes.entrySet()) {
163-
List<HandlerCompiler> handlers = entry.getValue();
164-
ModuleCompiler module = new ModuleCompiler(processingEnvironment, entry.getKey());
165-
String moduleClass = module.getModuleClass();
166-
byte[] moduleBin = module.compile(handlers);
167-
onClass(moduleClass, moduleBin);
168-
writeClass(filer.createClassFile(moduleClass), moduleBin);
174+
moduleList.add(moduleClass);
175+
}
169176

170-
moduleList.add(moduleClass);
171-
}
172-
return true;
173-
} catch (Exception x) {
174-
throw SneakyThrows.propagate(x);
177+
doServices(filer, moduleList);
178+
}
179+
180+
private String signature(ExecutableElement method) {
181+
return method.toString();
182+
}
183+
184+
private List<Element> superTypes(Element owner) {
185+
Types typeUtils = processingEnv.getTypeUtils();
186+
List<? extends TypeMirror> supertypes = typeUtils
187+
.directSupertypes(owner.asType());
188+
if (supertypes == null || supertypes.isEmpty()) {
189+
return Collections.emptyList();
190+
}
191+
TypeMirror supertype = supertypes.get(0);
192+
String supertypeName = typeUtils.erasure(supertype).toString();
193+
Element supertypeElement = typeUtils.asElement(supertype);
194+
if (!Object.class.getName().equals(supertypeName)
195+
&& supertypeElement.getKind() == ElementKind.CLASS) {
196+
List<Element> result = new ArrayList<>();
197+
result.addAll(superTypes(supertypeElement));
198+
result.add(supertypeElement);
199+
return result;
175200
}
201+
return Collections.emptyList();
176202
}
177203

178-
private void doServices(Filer filer) throws IOException {
204+
private void debug(String format, Object... args) {
205+
if (debug) {
206+
System.out.printf(format + "\n", args);
207+
}
208+
}
209+
210+
private void doServices(Filer filer, List<String> moduleList) throws IOException {
179211
String location = "META-INF/services/" + MvcFactory.class.getName();
212+
debug("%s", location);
180213
FileObject resource = filer.createResource(StandardLocation.CLASS_OUTPUT, "", location);
181-
String content = moduleList.stream()
182-
.collect(Collectors.joining(System.getProperty("line.separator")));
183-
onResource(location, content);
214+
StringBuilder content = new StringBuilder();
215+
for (String classname : moduleList) {
216+
debug(" %s", classname);
217+
content.append(classname).append(System.getProperty("line.separator"));
218+
}
219+
onResource(location, content.toString());
184220
try (PrintWriter writer = new PrintWriter(resource.openOutputStream())) {
185221
writer.println(content);
186222
}
187223
}
188224

189-
protected void onMvcHandler(String methodDescriptor, HandlerCompiler compiler) {
190-
}
191-
192225
protected void onClass(String className, byte[] bytecode) {
193226
}
194227

@@ -201,12 +234,22 @@ private void writeClass(JavaFileObject javaFileObject, byte[] bytecode) throws I
201234
}
202235
}
203236

204-
private List<String> path(TypeElement method, ExecutableElement exec, TypeElement owner) {
237+
private List<String> path(Element owner, TypeElement annotation, ExecutableElement exec) {
205238
List<String> prefix = path(owner);
239+
if (prefix.isEmpty()) {
240+
// Look at parent @path annotation
241+
List<Element> superTypes = superTypes(owner);
242+
int i = superTypes.size() - 1;
243+
while (prefix.isEmpty() && i >= 0) {
244+
prefix = path(superTypes.get(i--));
245+
}
246+
}
247+
206248
// Favor GET("/path") over Path("/path") at method level
207-
List<String> path = path(method.getQualifiedName().toString(), method.getAnnotationMirrors());
249+
List<String> path = path(annotation.getQualifiedName().toString(),
250+
annotation.getAnnotationMirrors());
208251
if (path.size() == 0) {
209-
path = path(method.getQualifiedName().toString(), exec.getAnnotationMirrors());
252+
path = path(annotation.getQualifiedName().toString(), exec.getAnnotationMirrors());
210253
}
211254
List<String> methodPath = path;
212255
if (prefix.size() == 0) {
@@ -221,10 +264,6 @@ private List<String> path(TypeElement method, ExecutableElement exec, TypeElemen
221264
.collect(Collectors.toList());
222265
}
223266

224-
private List<String> path(TypeElement method, ExecutableElement exec) {
225-
return path(method, exec, (TypeElement) exec.getEnclosingElement());
226-
}
227-
228267
private List<String> path(Element element) {
229268
return path(null, element.getAnnotationMirrors());
230269
}

0 commit comments

Comments
 (0)