From 8e29e563bfefeaeca9b3cc01ed30d2aaf77dabb3 Mon Sep 17 00:00:00 2001 From: Oleksandr Porunov Date: Sun, 21 Nov 2021 13:53:38 +0200 Subject: [PATCH] Add possibility to use custom annotations to detect Spring query parameters Adds possibility to detect Spring request parameters using custom annotations Adds possibility to detect Spring request body using custom annotations Fixes #747 Signed-off-by: Oleksandr Porunov --- .../typescript/generator/Settings.java | 12 ++++- .../generator/gradle/GenerateTask.java | 4 ++ .../generator/maven/GenerateMojo.java | 24 +++++++-- .../spring/SpringApplicationParser.java | 29 +++++++++- .../generator/spring/SpringTest.java | 53 ++++++++++++++++++- 5 files changed, 115 insertions(+), 7 deletions(-) diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Settings.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Settings.java index 1fc37d5a2..dad0896d7 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Settings.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Settings.java @@ -100,6 +100,8 @@ public class Settings { public boolean generateSpringApplicationInterface = false; public boolean generateSpringApplicationClient = false; public boolean scanSpringApplication; + public List> springCustomQueryParameterAnnotations = new ArrayList<>(); + public List> springCustomRequestBodyAnnotations = new ArrayList<>(); @Deprecated public RestNamespacing jaxrsNamespacing; @Deprecated public Class jaxrsNamespacingAnnotation = null; @Deprecated public String jaxrsNamespacingAnnotationElement; // default is "value" @@ -255,6 +257,14 @@ public void loadIncludePropertyAnnotations(ClassLoader classLoader, List this.includePropertyAnnotations = loadClasses(classLoader, includePropertyAnnotations, Annotation.class); } + public void loadSpringCustomQueryParameterAnnotations(ClassLoader classLoader, List springCustomQueryParameterAnnotations) { + this.springCustomQueryParameterAnnotations = loadClasses(classLoader, springCustomQueryParameterAnnotations, Annotation.class); + } + + public void loadSpringCustomRequestBodyAnnotations(ClassLoader classLoader, List springCustomRequestBodyAnnotations) { + this.springCustomRequestBodyAnnotations = loadClasses(classLoader, springCustomRequestBodyAnnotations, Annotation.class); + } + public void loadExcludePropertyAnnotations(ClassLoader classLoader, List excludePropertyAnnotations) { this.excludePropertyAnnotations = loadClasses(classLoader, excludePropertyAnnotations, Annotation.class); } @@ -292,7 +302,7 @@ public static Map convertToMap(List items, String itemNa } return result; } - + public void validate() { if (classLoader == null) { classLoader = Thread.currentThread().getContextClassLoader(); diff --git a/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java b/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java index f298e29ad..44c2f1630 100644 --- a/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java +++ b/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java @@ -59,6 +59,8 @@ public class GenerateTask extends DefaultTask { public List excludeClassPatterns; public List includePropertyAnnotations; public List excludePropertyAnnotations; + public List springCustomQueryParameterAnnotations; + public List springCustomRequestBodyAnnotations; public JsonLibrary jsonLibrary; public Jackson2Configuration jackson2Configuration; public GsonConfiguration gsonConfiguration; @@ -204,6 +206,8 @@ private Settings createSettings(URLClassLoader classLoader) { settings.loadExtensions(classLoader, Utils.concat(extensionClasses, extensions), extensionsWithConfiguration); settings.loadIncludePropertyAnnotations(classLoader, includePropertyAnnotations); settings.loadExcludePropertyAnnotations(classLoader, excludePropertyAnnotations); + settings.loadSpringCustomQueryParameterAnnotations(classLoader, springCustomQueryParameterAnnotations); + settings.loadSpringCustomRequestBodyAnnotations(classLoader, springCustomRequestBodyAnnotations); settings.loadOptionalAnnotations(classLoader, optionalAnnotations); settings.loadRequiredAnnotations(classLoader, requiredAnnotations); settings.loadNullableAnnotations(classLoader, nullableAnnotations); diff --git a/typescript-generator-maven-plugin/src/main/java/cz/habarta/typescript/generator/maven/GenerateMojo.java b/typescript-generator-maven-plugin/src/main/java/cz/habarta/typescript/generator/maven/GenerateMojo.java index bac8f5897..50ead93c9 100644 --- a/typescript-generator-maven-plugin/src/main/java/cz/habarta/typescript/generator/maven/GenerateMojo.java +++ b/typescript-generator-maven-plugin/src/main/java/cz/habarta/typescript/generator/maven/GenerateMojo.java @@ -571,6 +571,22 @@ public class GenerateMojo extends AbstractMojo { @Parameter private boolean scanSpringApplication; + /** + * Additional annotations which are treated as optional request parameters during Spring controllers parsing when Spring's + * RequestParam or ModelAttribute is not used on a specific method's argument. + * This option may be useful when Spring controller method accepts custom resolved parameters. + */ + @Parameter + private List springCustomQueryParameterAnnotations; + + /** + * Additional annotations which are treated as request body during Spring controllers parsing when Spring's + * RequestBody is not used on a specific method's argument. + * This option may be useful when Spring controller method accepts custom resolved parameters. + */ + @Parameter + private List springCustomRequestBodyAnnotations; + /** * Deprecated, use {@link #restNamespacing}. */ @@ -800,7 +816,7 @@ public class GenerateMojo extends AbstractMojo { */ @Parameter private String npmBuildScript; - + /** * List of additional NPM dependencies.
* Only applicable when {@link #generateNpmPackageJson} parameter is true and generating implementation file (.ts).
@@ -809,7 +825,7 @@ public class GenerateMojo extends AbstractMojo { */ @Parameter private List npmDependencies; - + /** * List of additional NPM devDependencies.
* Only applicable when {@link #generateNpmPackageJson} parameter is true and generating implementation file (.ts).
@@ -818,7 +834,7 @@ public class GenerateMojo extends AbstractMojo { */ @Parameter private List npmDevDependencies; - + /** * List of additional NPM peerDependencies.
* Only applicable when {@link #generateNpmPackageJson} parameter is true and generating implementation file (.ts).
@@ -973,6 +989,8 @@ private Settings createSettings(URLClassLoader classLoader) { settings.loadExtensions(classLoader, extensions, extensionsWithConfiguration); settings.loadIncludePropertyAnnotations(classLoader, includePropertyAnnotations); settings.loadExcludePropertyAnnotations(classLoader, excludePropertyAnnotations); + settings.loadSpringCustomQueryParameterAnnotations(classLoader, springCustomQueryParameterAnnotations); + settings.loadSpringCustomRequestBodyAnnotations(classLoader, springCustomRequestBodyAnnotations); settings.loadOptionalAnnotations(classLoader, optionalAnnotations); settings.loadRequiredAnnotations(classLoader, requiredAnnotations); settings.loadNullableAnnotations(classLoader, nullableAnnotations); diff --git a/typescript-generator-spring/src/main/java/cz/habarta/typescript/generator/spring/SpringApplicationParser.java b/typescript-generator-spring/src/main/java/cz/habarta/typescript/generator/spring/SpringApplicationParser.java index 99b36662f..9e4fe0253 100644 --- a/typescript-generator-spring/src/main/java/cz/habarta/typescript/generator/spring/SpringApplicationParser.java +++ b/typescript-generator-spring/src/main/java/cz/habarta/typescript/generator/spring/SpringApplicationParser.java @@ -26,6 +26,7 @@ import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; @@ -304,6 +305,7 @@ private void parseControllerMethod(JaxrsApplicationParser.Result result, JaxrsAp queryParams.add(new RestQueryParam.Single(new MethodParameterModel("size", Long.class), false)); queryParams.add(new RestQueryParam.Single(new MethodParameterModel("sort", String.class), false)); } else { + boolean queryParamAdded = false; final RequestParam requestParamAnnotation = AnnotationUtils.findAnnotation(parameter, RequestParam.class); if (requestParamAnnotation != null) { if (parameter.getType() == MultiValueMap.class) { @@ -315,6 +317,7 @@ private void parseControllerMethod(JaxrsApplicationParser.Result result, JaxrsAp parameter.getName() ), parameter.getParameterizedType()), isRequired)); foundType(result, parameter.getParameterizedType(), controllerClass, method.getName()); + queryParamAdded = true; } } @@ -330,12 +333,26 @@ private void parseControllerMethod(JaxrsApplicationParser.Result result, JaxrsAp propertyDescriptor.getPropertyType() ), false)); foundType(result, propertyDescriptor.getPropertyType(), controllerClass, method.getName()); + queryParamAdded = true; } } } catch (IntrospectionException e) { TypeScriptGenerator.getLogger().warning(String.format("Cannot introspect '%s' class: " + e.getMessage(), parameter.getAnnotatedType())); } } + + if(!queryParamAdded){ + for(Class customRequestParamAnnotationClass : settings.springCustomQueryParameterAnnotations){ + Annotation customRequestParamAnnotation = AnnotationUtils.findAnnotation(parameter, customRequestParamAnnotationClass); + if(customRequestParamAnnotation != null){ + queryParams.add(new RestQueryParam.Single(new MethodParameterModel( + parameter.getName(), + parameter.getParameterizedType()), false)); + foundType(result, parameter.getParameterizedType(), controllerClass, method.getName()); + break; + } + } + } } } @@ -373,8 +390,16 @@ private MethodParameterModel getEntityParameter(Class controller, Method meth final List parameterTypes = settings.getTypeParser().getMethodParameterTypes(method); final List> parameters = Utils.zip(Arrays.asList(method.getParameters()), parameterTypes); for (Pair pair : parameters) { - final RequestBody requestBodyAnnotation = AnnotationUtils.findAnnotation(pair.getValue1(), RequestBody.class); - if (requestBodyAnnotation != null) { + boolean hasRequestBody = AnnotationUtils.findAnnotation(pair.getValue1(), RequestBody.class) != null; + if(!hasRequestBody){ + for(Class customRequestBodyAnnotationClass : settings.springCustomRequestBodyAnnotations){ + if(AnnotationUtils.findAnnotation(pair.getValue1(), customRequestBodyAnnotationClass) != null){ + hasRequestBody = true; + break; + } + } + } + if (hasRequestBody) { final Type resolvedType = GenericsResolver.resolveType(controller, pair.getValue2(), method.getDeclaringClass()); return new MethodParameterModel(pair.getValue1().getName(), resolvedType); } diff --git a/typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTest.java b/typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTest.java index a2a932bef..f8f962b28 100644 --- a/typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTest.java +++ b/typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTest.java @@ -9,8 +9,10 @@ import cz.habarta.typescript.generator.util.Utils; import io.swagger.annotations.ApiOperation; import io.swagger.v3.oas.annotations.Operation; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; @@ -35,7 +37,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; - public class SpringTest { @Test @@ -169,6 +170,27 @@ public void testInheritance() { Assertions.assertFalse(output.contains("uriEncoding`test/b`")); } + @Test + public void testCustomQueryParameters() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateSpringApplicationClient = true; + settings.springCustomQueryParameterAnnotations = Arrays.asList(CustomRequestParam.class); + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Controller8.class)); + Assertions.assertTrue(output.contains("echo(queryParams?: { message?: string; }): RestResponse")); + } + + @Test + public void testCustomRequestBody() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateSpringApplicationClient = true; + settings.springCustomRequestBodyAnnotations = Arrays.asList(CustomRequestBody.class); + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Controller9.class)); + Assertions.assertTrue(output.contains("setEntity(data: Data1): RestResponse")); + Assertions.assertTrue(output.contains("interface Data1")); + } + @RestController @RequestMapping("/owners/{ownerId}") public static class Controller1 { @@ -203,6 +225,23 @@ public String echo( } } + @RestController + public static class Controller8 { + @RequestMapping("/echo3") + public String echo( + @CustomRequestParam String message + ) { + return message; + } + } + + @RestController + public static class Controller9 { + @RequestMapping(path = "/data2", method = RequestMethod.PUT) + public void setEntity(@CustomRequestBody Data1 data) { + } + } + @Test public void testQueryParametersWithModel() { final Settings settings = TestUtils.settings(); @@ -488,4 +527,16 @@ public String shouldBeExcluded() { } } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface CustomRequestParam { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface CustomRequestBody { + + } + }