diff options
author | zaaarf <me@zaaarf.foo> | 2024-01-19 14:29:20 +0100 |
---|---|---|
committer | zaaarf <me@zaaarf.foo> | 2024-01-19 14:29:20 +0100 |
commit | 4da7e5990d6d932539d35082a4859a5d10d46bc6 (patch) | |
tree | 6f7279f81d48ac2c5a871aaa22e76c990169eb4f /src/main/java/foo |
initial commit
Diffstat (limited to 'src/main/java/foo')
-rw-r--r-- | src/main/java/foo/zaaarf/routecompass/Route.java | 29 | ||||
-rw-r--r-- | src/main/java/foo/zaaarf/routecompass/RouteCompass.java | 120 |
2 files changed, 149 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()); + } +} |