summaryrefslogtreecommitdiff
path: root/src/main/java/foo/zaaarf/routecompass/RouteCompass.java
blob: a3e9667ceb2d53bb59d217e1c1d57c49a6f2b724 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package foo.zaaarf.routecompass;

import org.springframework.web.bind.annotation.*;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class RouteCompass extends AbstractProcessor {

   private final HashSet<Route> foundRoutes = new HashSet<>();
   private final HashSet<Class<? extends Annotation>> annotationClasses = new HashSet<>();

   public RouteCompass() {
      annotationClasses.add(RequestMapping.class);
      annotationClasses.add(GetMapping.class);
      annotationClasses.add(PostMapping.class);
      annotationClasses.add(PutMapping.class);
      annotationClasses.add(DeleteMapping.class);
      annotationClasses.add(PatchMapping.class);
   }

   @Override
   public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
      for(TypeElement annotationType : annotations) {
         env.getElementsAnnotatedWith(annotationType)
            .stream()
            .filter(elem -> elem instanceof ExecutableElement)
            .map(elem -> (ExecutableElement) elem)
            .forEach(elem -> this.foundRoutes.add(new Route(
               elem.getEnclosingElement().asType().toString(),
               this.getFullRoute(annotationType, elem),
               this.getRequestMethods(annotationType, elem),
               this.isDeprecated(elem)
            )));
      }

      //TODO print

      return false; //don't claim them, let spring do its job
   }

   private String getFullRoute(TypeElement annotationType, Element element) {
      @SuppressWarnings("OptionalGetWithoutIsPresent") //find matching annotation class
      Class<? extends Annotation> annClass = this.annotationClasses.stream()
         .filter(c -> annotationType.getQualifiedName().contentEquals(c.getName()))
         .findFirst()
         .get(); //should never fail

      try {
         //it can be both path and value
         String pathValue = (String) annClass.getField("path").get(element.getAnnotation(annClass));
         String route = pathValue == null
            ? (String) annClass.getField("value").get(element.getAnnotation(annClass))
            : pathValue;

         return this.parentOrFallback(element, route, (a, e) -> {
            String parent = this.getFullRoute(a, e);
            StringBuilder sb = new StringBuilder(parent);
            if(!parent.endsWith("/")) sb.append("/");
            sb.append(route);
            return sb.toString();
         });
      } catch (ReflectiveOperationException ex) {
         throw new RuntimeException(ex); //if it fails something went very wrong
      }
   }

   private RequestMethod[] getRequestMethods(TypeElement annotationType, Element element) {
      RequestMethod[] methods = annotationType.getQualifiedName().contentEquals(RequestMapping.class.getName())
         ? element.getAnnotation(RequestMapping.class).method()
         : annotationType.getAnnotation(RequestMapping.class).method();
      return methods.length == 0
         ? this.parentOrFallback(element, methods, this::getRequestMethods)
         : methods;
   }

   private boolean isDeprecated(Element elem) {
      return elem.getAnnotation(Deprecated.class) != null
         || elem.getEnclosingElement().getAnnotation(Deprecated.class) != null;
   }

   private <T> T parentOrFallback(Element element, T fallback, BiFunction<TypeElement, Element, T> fun) {
      List<Class<? extends Annotation>> found = this.annotationClasses.stream()
         .filter(annClass -> element.getEnclosingElement().getAnnotation(annClass) != null)
         .collect(Collectors.toList());

      if(found.isEmpty()) return fallback;

      if(found.size() > 1) this.processingEnv.getMessager().printMessage(
         Diagnostic.Kind.WARNING,
         "Found multiple mapping annotations on "
            + element.getSimpleName().toString()
            + ", only one of the will be considered!"
      );

      return fun.apply(
         this.processingEnv.getElementUtils()
            .getTypeElement(found.get(0).getName()),
         element
      );
   }

   @Override
   public Set<String> getSupportedAnnotationTypes() {
      return annotationClasses.stream().map(Class::getCanonicalName).collect(Collectors.toSet());
   }
}