77
88import io .jooby .MvcFactory ;
99import io .jooby .SneakyThrows ;
10- import io .jooby .annotations .Path ;
1110import io .jooby .internal .apt .HandlerCompiler ;
1211import io .jooby .internal .apt .ModuleCompiler ;
1312
2019import javax .lang .model .element .Element ;
2120import javax .lang .model .element .ElementKind ;
2221import javax .lang .model .element .ExecutableElement ;
22+ import javax .lang .model .element .Modifier ;
2323import 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 ;
2626import javax .tools .FileObject ;
2727import javax .tools .JavaFileObject ;
2828import javax .tools .StandardLocation ;
3131import java .io .PrintWriter ;
3232import java .util .ArrayList ;
3333import java .util .Collections ;
34- import java .util .HashMap ;
34+ import java .util .LinkedHashMap ;
3535import java .util .LinkedHashSet ;
3636import java .util .List ;
3737import java .util .Map ;
3838import java .util .Set ;
3939import java .util .stream .Collectors ;
4040import 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 */
4847public 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