diff --git a/README.md b/README.md index 56f689b..f5ca0e5 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# Route Mapper -Just a little program I needed for work. Reads Spring's route mapping annotations in a project and writes down all about them. \ No newline at end of file +# Route Compass +An annotation processor that reads Spring Web's annotations to write down a map of all the routes in your projects: their paths, parameters, methods... + +It's a small program I found myself needing at work. Don't count on it being production-ready. \ No newline at end of file diff --git a/build.gradle b/build.gradle index bff5d4a..18668a5 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'foo.zaaarf' -version = '1.0-SNAPSHOT' +version = '0.1' repositories { mavenCentral() @@ -11,10 +11,4 @@ repositories { dependencies { implementation 'org.springframework:spring-web:5.3.31' - testImplementation platform('org.junit:junit-bom:5.9.1') - testImplementation 'org.junit.jupiter:junit-jupiter' -} - -test { - useJUnitPlatform() } \ No newline at end of file diff --git a/src/main/java/foo/zaaarf/routecompass/Route.java b/src/main/java/foo/zaaarf/routecompass/Route.java index f56de0b..db1492a 100644 --- a/src/main/java/foo/zaaarf/routecompass/Route.java +++ b/src/main/java/foo/zaaarf/routecompass/Route.java @@ -4,18 +4,53 @@ import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMethod; /** - * Internal representation of a REST route. + * Representation of a REST route. */ public class Route { - public final String route; + /** + * The path of the endpoint. + */ + public final String path; + + /** + * The supported {@link RequestMethod}s, flattened to a string. + */ public final String method; + + /** + * The {@link MediaType} produced by the endpoint. + * May be null if not specified. + */ public final String produces; + + /** + * The {@link MediaType} consumed by the endpoint. + * May be null if not specified. + */ public final String consumes; + + /** + * Whether the endpoint is deprecated. + */ public final boolean deprecated; + + /** + * An array of {@link Param}s, representing parameters accepted by the endpoint. + */ public final Param[] params; - public Route(String route, RequestMethod[] methods, MediaType consumes, MediaType produces, boolean deprecated, Param... params) { - this.route = route; + /** + * The one and only constructor. + * @param path the path of the endpoint + * @param methods the {@link RequestMethod}s accepted by the endpoint + * @param consumes the {@link MediaType} consumed by the endpoint, may be null + * @param produces the {@link MediaType} produced by the endpoint, may be null + * @param deprecated whether the endpoint is deprecated + * @param params {@link Param}s of the endpoint, may be null + */ + public Route(String path, RequestMethod[] methods, MediaType consumes, MediaType produces, boolean deprecated, + Param... params) { + this.path = path; StringBuilder methodStringBuilder = new StringBuilder("["); for(RequestMethod m : methods) @@ -35,14 +70,36 @@ public class Route { this.deprecated = deprecated; - this.params = params; + if(params != null) this.params = params; + else this.params = new Param[0]; //just in case } + /** + * Representation of a parameter of a REST route. + */ public static class Param { + /** + * The fully-qualified name of the expected type of the parameter. + */ public final String typeFQN; + + /** + * The name of the parameter. + */ public final String name; + + /** + * The default value of the parameter. + * May be null, in which case the parameter is required. + */ public final String defaultValue; + /** + * The one and only constructor. + * @param typeFQN the FQN of the expected type of the parameter + * @param name the name of the parameter + * @param defaultValue the default value of the parameter, may be null if the parameter is required + */ public Param(String typeFQN, String name, String defaultValue) { this.typeFQN = typeFQN; this.name = name; diff --git a/src/main/java/foo/zaaarf/routecompass/RouteCompass.java b/src/main/java/foo/zaaarf/routecompass/RouteCompass.java index 94aeb7d..0847002 100644 --- a/src/main/java/foo/zaaarf/routecompass/RouteCompass.java +++ b/src/main/java/foo/zaaarf/routecompass/RouteCompass.java @@ -21,21 +21,42 @@ import java.util.*; import java.util.function.BiFunction; import java.util.stream.Collectors; +/** + * The main processor class. + */ @SupportedSourceVersion(SourceVersion.RELEASE_8) public class RouteCompass extends AbstractProcessor { - private final HashMap> foundRoutes = new HashMap<>(); - private final HashSet> annotationClasses = new HashSet<>(); + /** + * A {@link Map} tying each component class to the routes it contains. + */ + private final Map> foundRoutes = new HashMap<>(); + /** + * A {@link Set} containing all the supported annotation classes. + */ + private final Set> annotationClasses = new HashSet<>(); + + /** + * Default constructor, it only initialises {@link #annotationClasses}. + */ 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); + this.annotationClasses.add(RequestMapping.class); + this.annotationClasses.add(GetMapping.class); + this.annotationClasses.add(PostMapping.class); + this.annotationClasses.add(PutMapping.class); + this.annotationClasses.add(DeleteMapping.class); + this.annotationClasses.add(PatchMapping.class); } + /** + * Processes Spring's annotations, NOT claiming them for itself. + * It builds a {@link Route} object for each route and adds it to {@link #foundRoutes}, + * then proceeds to print it to a file. + * @param annotations the annotation types requested to be processed + * @param env environment for information about the current and prior round + * @return false, letting other processor process the annotations again + */ @Override public boolean process(Set annotations, RoundEnvironment env) { for(TypeElement annotationType : annotations) { @@ -70,7 +91,7 @@ public class RouteCompass extends AbstractProcessor { for(Route r : routesInClass) { out.print("\t- "); if(r.deprecated) out.print("[DEPRECATED] "); - out.print(r.method + " " + r.route); + out.print(r.method + " " + r.path); if(r.consumes != null) out.print("(expects: " + r.consumes + ")"); if(r.produces != null) out.print("(returns: " + r.produces + ")"); out.println(); @@ -92,6 +113,12 @@ public class RouteCompass extends AbstractProcessor { return false; //don't claim them, let spring do its job } + /** + * Extracts the route of an element. + * @param annotationType the {@link TypeElement} with the annotation we are processing + * @param element the {@link Element} currently being examined + * @return the full route of the endpoint + */ private String getFullRoute(TypeElement annotationType, Element element) { try { String route = this.getAnnotationFieldsValue(annotationType, element, "path", "value"); @@ -107,6 +134,27 @@ public class RouteCompass extends AbstractProcessor { } } + /** + * Finds the request methods supported by the endpoint. + * @param annotationType the {@link TypeElement} with the annotation we are processing + * @param element the {@link Element} currently being examined + * @return the {@link RequestMethod}s supported by the endpoint + */ + 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.getParentOrFallback(element, methods, this::getRequestMethods) + : methods; + } + + /** + * Finds the media type consumed by an endpoint. + * @param annotationType the {@link TypeElement} with the annotation we are processing + * @param element the {@link Element} currently being examined + * @return the {@link MediaType} consumed by the endpoint + */ private MediaType getConsumedType(TypeElement annotationType, Element element) { try { MediaType res = this.getAnnotationFieldsValue(annotationType, element, "consumes"); @@ -118,6 +166,12 @@ public class RouteCompass extends AbstractProcessor { } } + /** + * Finds the media type consumed by an endpoint. + * @param annotationType the {@link TypeElement} with the annotation we are processing + * @param element the {@link Element} currently being examined + * @return the {@link MediaType} consumed by the endpoint + */ private MediaType getProducedType(TypeElement annotationType, Element element) { try { MediaType res = this.getAnnotationFieldsValue(annotationType, element, "produces"); @@ -129,20 +183,21 @@ public class RouteCompass extends AbstractProcessor { } } - 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.getParentOrFallback(element, methods, this::getRequestMethods) - : methods; - } - - private boolean isDeprecated(Element elem) { - return elem.getAnnotation(Deprecated.class) != null - || elem.getEnclosingElement().getAnnotation(Deprecated.class) != null; + /** + * Checks whether the endpoint or its parent are deprecated + * @param element the {@link Element} currently being examined + * @return whether the given endpoint is deprecated + */ + private boolean isDeprecated(Element element) { + return element.getAnnotation(Deprecated.class) != null + || element.getEnclosingElement().getAnnotation(Deprecated.class) != null; } + /** + * Gets the parameters accepted by a request. + * @param params the {@link VariableElement}s representing the parameters of a request + * @return an array of {@link Route.Param} representing the parameters of the request. + */ private Route.Param[] getParams(List params) { return params.stream() .map(p -> { @@ -165,6 +220,15 @@ public class RouteCompass extends AbstractProcessor { }).filter(Objects::nonNull).toArray(Route.Param[]::new); } + /** + * An annotation value. + * @param annotationType the {@link TypeElement} with the annotation we are processing + * @param element the {@link Element} currently being examined + * @param fieldNames the field name(s) to look for; they are tried in order, and the first found is returned + * @return the field value, cast to the expected type + * @param the expected type of the field + * @throws ReflectiveOperationException when given non-existing or inaccessible field names (hopefully never) + */ @SuppressWarnings({"OptionalGetWithoutIsPresent", "unchecked"}) private T getAnnotationFieldsValue(TypeElement annotationType, Element element, String ... fieldNames) throws ReflectiveOperationException { @@ -183,6 +247,15 @@ public class RouteCompass extends AbstractProcessor { return result; } + /** + * Finds whether the parent of the given element has any supported annotation, then applies the given + * function to both parent and found annotation. + * @param element the {@link Element} currently being examined + * @param fallback the value to return if the parent didn't have any supported annotations + * @param fun the {@link BiFunction} to apply + * @return the output or the function, or the fallback value if the parent didn't have any supported annotation + * @param the type of the expected result + */ private T getParentOrFallback(Element element, T fallback, BiFunction fun) { List> found = this.annotationClasses.stream() .filter(annClass -> element.getEnclosingElement().getAnnotation(annClass) != null) @@ -194,7 +267,7 @@ public class RouteCompass extends AbstractProcessor { Diagnostic.Kind.WARNING, "Found multiple mapping annotations on " + element.getSimpleName().toString() - + ", only one of the will be considered!" + + ", only one of them will be considered!" ); return fun.apply( @@ -204,6 +277,9 @@ public class RouteCompass extends AbstractProcessor { ); } + /** + * @return the types of annotations supported by this processor + */ @Override public Set getSupportedAnnotationTypes() { return annotationClasses.stream().map(Class::getCanonicalName).collect(Collectors.toSet());