summaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
author zaaarf <me@zaaarf.foo>2024-01-19 14:29:20 +0100
committer zaaarf <me@zaaarf.foo>2024-01-19 14:29:20 +0100
commit4da7e5990d6d932539d35082a4859a5d10d46bc6 (patch)
tree6f7279f81d48ac2c5a871aaa22e76c990169eb4f /src/main
initial commit
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/foo/zaaarf/routecompass/Route.java29
-rw-r--r--src/main/java/foo/zaaarf/routecompass/RouteCompass.java120
-rw-r--r--src/main/resources/META-INF/gradle/incremental.annotation.processors1
-rw-r--r--src/main/resources/META-INF/services/javax.annotation.processing.Processor1
4 files changed, 151 insertions, 0 deletions
diff --git a/src/main/java/foo/zaaarf/routecompass/Route.java b/src/main/java/foo/zaaarf/routecompass/Route.java
new file mode 100644
index 0000000..2b61571
--- /dev/null
+++ b/src/main/java/foo/zaaarf/routecompass/Route.java
@@ -0,0 +1,29 @@
+package foo.zaaarf.routecompass;
+
+import org.springframework.web.bind.annotation.RequestMethod;
+
+/**
+ * Internal representation of a REST route.
+ */
+public class Route {
+ public final String classFqn;
+ public final String route;
+ public final String method;
+ public final boolean deprecated;
+
+
+ public Route(String classFqn, String route, RequestMethod[] methods, boolean deprecated) {
+ this.classFqn = classFqn;
+ this.route = route;
+ StringBuilder methodStringBuilder = new StringBuilder("[");
+ for(RequestMethod m : methods)
+ methodStringBuilder
+ .append(m.name())
+ .append("|");
+ methodStringBuilder
+ .deleteCharAt(methodStringBuilder.length() - 1)
+ .append("]");
+ this.method = methodStringBuilder.toString();
+ this.deprecated = deprecated;
+ }
+}
diff --git a/src/main/java/foo/zaaarf/routecompass/RouteCompass.java b/src/main/java/foo/zaaarf/routecompass/RouteCompass.java
new file mode 100644
index 0000000..a3e9667
--- /dev/null
+++ b/src/main/java/foo/zaaarf/routecompass/RouteCompass.java
@@ -0,0 +1,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());
+ }
+}
diff --git a/src/main/resources/META-INF/gradle/incremental.annotation.processors b/src/main/resources/META-INF/gradle/incremental.annotation.processors
new file mode 100644
index 0000000..6150d23
--- /dev/null
+++ b/src/main/resources/META-INF/gradle/incremental.annotation.processors
@@ -0,0 +1 @@
+foo.zaaarf.routecompass.RouteCompass,isolating \ No newline at end of file
diff --git a/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/src/main/resources/META-INF/services/javax.annotation.processing.Processor
new file mode 100644
index 0000000..d91ebc4
--- /dev/null
+++ b/src/main/resources/META-INF/services/javax.annotation.processing.Processor
@@ -0,0 +1 @@
+foo.zaaarf.routecompass.RouteCompass \ No newline at end of file