From afeeab150f70d0c37e6fdee4b3df95287c0aae8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Habarta?= Date: Wed, 13 Mar 2019 09:24:13 +0100 Subject: [PATCH] Spring support, refactoring (#253) - `typescript-generator-spring` module - configuration parameters: - generateSpringApplicationInterface - generateSpringApplicationClient - scanSpringApplication - restNamespacing (deprecating jaxrsNamespacing) - restNamespacingAnnotation (deprecating jaxrsNamespacingAnnotation) - changed how parsers are instantiated and theirs `TypeProcessor`s are combined - renamed several `Jaxrs*` classes to `Rest*` - deleted `cz.habarta.typescript.generator.util.Predicate` class - `sample-maven-spring`, `sample-gradle-spring` example modules --- .gitignore | 4 + pom.xml | 1 + sample-gradle-spring/build.gradle | 40 +++ .../sample/spring/SpringTestApplication.java | 51 ++++ sample-maven-spring/pom.xml | 73 +++++ .../sample/spring/SpringTestApplication.java | 51 ++++ typescript-generator-core/pom.xml | 11 + .../generator/DefaultTypeProcessor.java | 47 +--- .../generator/ExcludingTypeProcessor.java | 2 +- .../habarta/typescript/generator/Input.java | 2 +- .../generator/JaxrsApplicationScanner.java | 2 +- ...sNamespacing.java => RestNamespacing.java} | 2 +- .../typescript/generator/Settings.java | 125 +++++++-- .../typescript/generator/TypeProcessor.java | 13 + .../generator/TypeScriptGenerator.java | 57 ++-- .../generator/compiler/ModelCompiler.java | 89 ++++--- .../generator/parser/Jackson1Parser.java | 26 +- .../generator/parser/Jackson2Parser.java | 74 +++-- .../typescript/generator/parser/Javadoc.java | 2 +- .../parser/JaxrsApplicationParser.java | 203 ++++++-------- .../typescript/generator/parser/Model.java | 10 +- .../generator/parser/ModelParser.java | 63 ++--- ...onModel.java => RestApplicationModel.java} | 15 +- .../parser/RestApplicationParser.java | 89 +++++++ .../generator/parser/RestApplicationType.java | 21 ++ ...sMethodModel.java => RestMethodModel.java} | 4 +- .../typescript/generator/parser/Swagger.java | 2 +- .../typescript/generator/util/Predicate.java | 9 - .../typescript/generator/util/Utils.java | 72 ++++- .../generator/CustomTypeConversionTest.java | 2 +- .../typescript/generator/ExtensionTest.java | 2 +- .../generator/JaxrsApplicationTest.java | 21 +- .../ext/AxiosClientExtensionTest.java | 4 +- .../generator/gradle/GenerateTask.java | 14 +- .../generator/maven/GenerateMojo.java | 52 +++- typescript-generator-spring/pom.xml | 70 +++++ .../spring/SpringApplicationParser.java | 252 ++++++++++++++++++ .../generator/spring/SpringTest.java | 137 ++++++++++ .../spring/SpringTestApplication.java | 51 ++++ 39 files changed, 1424 insertions(+), 341 deletions(-) create mode 100644 sample-gradle-spring/build.gradle create mode 100644 sample-gradle-spring/src/main/java/cz/habarta/typescript/generator/sample/spring/SpringTestApplication.java create mode 100644 sample-maven-spring/pom.xml create mode 100644 sample-maven-spring/src/main/java/cz/habarta/typescript/generator/sample/spring/SpringTestApplication.java rename typescript-generator-core/src/main/java/cz/habarta/typescript/generator/{JaxrsNamespacing.java => RestNamespacing.java} (74%) rename typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/{JaxrsApplicationModel.java => RestApplicationModel.java} (61%) create mode 100644 typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationParser.java create mode 100644 typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationType.java rename typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/{JaxrsMethodModel.java => RestMethodModel.java} (91%) delete mode 100644 typescript-generator-core/src/main/java/cz/habarta/typescript/generator/util/Predicate.java create mode 100644 typescript-generator-spring/pom.xml create mode 100644 typescript-generator-spring/src/main/java/cz/habarta/typescript/generator/spring/SpringApplicationParser.java create mode 100644 typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTest.java create mode 100644 typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTestApplication.java diff --git a/.gitignore b/.gitignore index 7939a8c00..9105c5f1f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ release.properties /sample-gradle/.nb-gradle/ /sample-gradle/bin/ /sample-gradle/build/ +/sample-gradle-spring/.gradle/ +/sample-gradle-spring/.nb-gradle/ +/sample-gradle-spring/bin/ +/sample-gradle-spring/build/ # npm node_modules diff --git a/pom.xml b/pom.xml index aacbed046..36e4adbd9 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,7 @@ typescript-generator-core typescript-generator-maven-plugin typescript-generator-gradle-plugin + typescript-generator-spring diff --git a/sample-gradle-spring/build.gradle b/sample-gradle-spring/build.gradle new file mode 100644 index 000000000..d7736ca4f --- /dev/null +++ b/sample-gradle-spring/build.gradle @@ -0,0 +1,40 @@ + +apply plugin: 'java' +apply plugin: 'cz.habarta.typescript-generator' + +version = '2.0' +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + jcenter() +} + +dependencies { + compile 'org.springframework.boot:spring-boot-starter-web:2.1.1.RELEASE' +} + +buildscript { + repositories { + mavenLocal() + jcenter() + } + + dependencies { + classpath 'cz.habarta.typescript-generator:typescript-generator-gradle-plugin:2.12-SNAPSHOT' + classpath 'cz.habarta.typescript-generator:typescript-generator-spring:2.12-SNAPSHOT' + } +} + +generateTypeScript { + classes = [ + 'cz.habarta.typescript.generator.sample.spring.SpringTestApplication' + ] + outputFileType = 'implementationFile' + jsonLibrary = 'jackson2' + outputKind = 'module' + scanSpringApplication = true + generateSpringApplicationClient = true +} + +build.dependsOn generateTypeScript diff --git a/sample-gradle-spring/src/main/java/cz/habarta/typescript/generator/sample/spring/SpringTestApplication.java b/sample-gradle-spring/src/main/java/cz/habarta/typescript/generator/sample/spring/SpringTestApplication.java new file mode 100644 index 000000000..3cf2e7360 --- /dev/null +++ b/sample-gradle-spring/src/main/java/cz/habarta/typescript/generator/sample/spring/SpringTestApplication.java @@ -0,0 +1,51 @@ + +package cz.habarta.typescript.generator.sample.spring; + +import java.util.concurrent.atomic.AtomicLong; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + + +@SpringBootApplication +public class SpringTestApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringTestApplication.class, args); + } + + @RestController + public static class GreetingController { + + private static final String template = "Hello, %s!"; + private final AtomicLong counter = new AtomicLong(); + + @RequestMapping("/greeting") + public Greeting greeting(@RequestParam(value="name", defaultValue="World") String name) { + return new Greeting(counter.incrementAndGet(), String.format(template, name)); + } + + } + + public static class Greeting { + + private final long id; + private final String content; + + public Greeting(long id, String content) { + this.id = id; + this.content = content; + } + + public long getId() { + return id; + } + + public String getContent() { + return content; + } + } + +} diff --git a/sample-maven-spring/pom.xml b/sample-maven-spring/pom.xml new file mode 100644 index 000000000..3cfa2f24a --- /dev/null +++ b/sample-maven-spring/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + cz.habarta.typescript-generator + sample-maven-spring + 2.0-SNAPSHOT + jar + sample-maven-spring + + + 2.12-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-web + 2.1.1.RELEASE + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + 1.8 + 1.8 + + -parameters + + + + + cz.habarta.typescript-generator + typescript-generator-maven-plugin + ${typescript-generator.version} + + + generate + + generate + + process-classes + + + + jackson2 + implementationFile + module + + cz.habarta.typescript.generator.sample.spring.SpringTestApplication + + true + true + + cz.habarta.typescript.generator.ext.AxiosClientExtension + + + + + cz.habarta.typescript-generator + typescript-generator-spring + ${typescript-generator.version} + + + + + + diff --git a/sample-maven-spring/src/main/java/cz/habarta/typescript/generator/sample/spring/SpringTestApplication.java b/sample-maven-spring/src/main/java/cz/habarta/typescript/generator/sample/spring/SpringTestApplication.java new file mode 100644 index 000000000..3cf2e7360 --- /dev/null +++ b/sample-maven-spring/src/main/java/cz/habarta/typescript/generator/sample/spring/SpringTestApplication.java @@ -0,0 +1,51 @@ + +package cz.habarta.typescript.generator.sample.spring; + +import java.util.concurrent.atomic.AtomicLong; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + + +@SpringBootApplication +public class SpringTestApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringTestApplication.class, args); + } + + @RestController + public static class GreetingController { + + private static final String template = "Hello, %s!"; + private final AtomicLong counter = new AtomicLong(); + + @RequestMapping("/greeting") + public Greeting greeting(@RequestParam(value="name", defaultValue="World") String name) { + return new Greeting(counter.incrementAndGet(), String.format(template, name)); + } + + } + + public static class Greeting { + + private final long id; + private final String content; + + public Greeting(long id, String content) { + this.id = id; + this.content = content; + } + + public long getId() { + return id; + } + + public String getContent() { + return content; + } + } + +} diff --git a/typescript-generator-core/pom.xml b/typescript-generator-core/pom.xml index afe28a65b..bd2bd3356 100644 --- a/typescript-generator-core/pom.xml +++ b/typescript-generator-core/pom.xml @@ -173,6 +173,17 @@ + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + org.apache.maven.plugins maven-checkstyle-plugin diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/DefaultTypeProcessor.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/DefaultTypeProcessor.java index 5b7b97f23..360d89ddf 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/DefaultTypeProcessor.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/DefaultTypeProcessor.java @@ -1,11 +1,10 @@ package cz.habarta.typescript.generator; -import cz.habarta.typescript.generator.parser.JaxrsApplicationParser; import cz.habarta.typescript.generator.util.UnionType; -import cz.habarta.typescript.generator.util.Utils; import java.lang.reflect.*; import java.math.*; +import java.time.temporal.Temporal; import java.util.*; import java.util.stream.Collectors; import javax.xml.bind.JAXBElement; @@ -18,27 +17,10 @@ public Result processType(Type javaType, Context context) { if (KnownTypes.containsKey(javaType)) return new Result(KnownTypes.get(javaType)); if (javaType instanceof Class) { final Class javaClass = (Class) javaType; - if (JavaTimeTemporal != null && JavaTimeTemporal.isAssignableFrom(javaClass)) { + if (Temporal.class.isAssignableFrom(javaClass)) { return new Result(TsType.Date); } } - if (javaType instanceof ParameterizedType) { - final ParameterizedType parameterizedType = (ParameterizedType) javaType; - if (parameterizedType.getRawType() instanceof Class) { - final Class javaClass = (Class) parameterizedType.getRawType(); - if (JAXBElement.class.isAssignableFrom(javaClass)) { - final Result result = context.processType(parameterizedType.getActualTypeArguments()[0]); - return new Result(result.getTsType(), result.getDiscoveredClasses()); - } - } - } - // map JAX-RS standard types to `any` - for (Class cls : JaxrsApplicationParser.getStandardEntityClasses()) { - final Class rawClass = Utils.getRawClassOrNull(javaType); - if (rawClass != null && cls.isAssignableFrom(rawClass)) { - return new Result(TsType.Any); - } - } if (javaType instanceof Class) { final Class javaClass = (Class) javaType; if (javaClass.isArray()) { @@ -54,11 +36,14 @@ public Result processType(Type javaType, Context context) { if (Map.class.isAssignableFrom(javaClass)) { return new Result(new TsType.IndexedArrayType(TsType.String, TsType.Any)); } - if (javaClass.getName().equals("java.util.OptionalInt") || - javaClass.getName().equals("java.util.OptionalLong") || - javaClass.getName().equals("java.util.OptionalDouble")) { + if (OptionalInt.class.isAssignableFrom(javaClass) || + OptionalLong.class.isAssignableFrom(javaClass) || + OptionalDouble.class.isAssignableFrom(javaClass)) { return new Result(TsType.Number.optional()); } + if (JAXBElement.class.isAssignableFrom(javaClass)) { + return new Result(TsType.Any); + } // generic structural type used without type arguments if (javaClass.getTypeParameters().length > 0) { final List tsTypeArguments = new ArrayList<>(); @@ -82,10 +67,14 @@ public Result processType(Type javaType, Context context) { final Result result = context.processType(parameterizedType.getActualTypeArguments()[1]); return new Result(new TsType.IndexedArrayType(TsType.String, result.getTsType()), result.getDiscoveredClasses()); } - if (javaClass.getName().equals("java.util.Optional")) { + if (Optional.class.isAssignableFrom(javaClass)) { final Result result = context.processType(parameterizedType.getActualTypeArguments()[0]); return new Result(result.getTsType().optional(), result.getDiscoveredClasses()); } + if (JAXBElement.class.isAssignableFrom(javaClass)) { + final Result result = context.processType(parameterizedType.getActualTypeArguments()[0]); + return new Result(result.getTsType(), result.getDiscoveredClasses()); + } // generic structural type final List> discoveredClasses = new ArrayList<>(); discoveredClasses.add(javaClass); @@ -169,14 +158,4 @@ private static Map getKnownTypes() { private static final Map KnownTypes = getKnownTypes(); - private static Class getTemporalIfAvailable() { - try { - return Class.forName("java.time.temporal.Temporal"); - } catch (ClassNotFoundException e) { - return null; - } - } - - private static final Class JavaTimeTemporal = getTemporalIfAvailable(); - } diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/ExcludingTypeProcessor.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/ExcludingTypeProcessor.java index 05f6aaa0a..e37601b4e 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/ExcludingTypeProcessor.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/ExcludingTypeProcessor.java @@ -1,13 +1,13 @@ package cz.habarta.typescript.generator; -import cz.habarta.typescript.generator.util.Predicate; import cz.habarta.typescript.generator.util.Utils; import java.lang.reflect.Type; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.function.Predicate; public class ExcludingTypeProcessor implements TypeProcessor { diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Input.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Input.java index dd3181255..c1392dcb2 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Input.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Input.java @@ -2,7 +2,6 @@ package cz.habarta.typescript.generator; import cz.habarta.typescript.generator.parser.SourceType; -import cz.habarta.typescript.generator.util.Predicate; import cz.habarta.typescript.generator.util.Utils; import io.github.classgraph.ClassGraph; import io.github.classgraph.ScanResult; @@ -14,6 +13,7 @@ import java.util.Date; import java.util.List; import java.util.Objects; +import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/JaxrsApplicationScanner.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/JaxrsApplicationScanner.java index 7168358d5..ad9bc6b7a 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/JaxrsApplicationScanner.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/JaxrsApplicationScanner.java @@ -2,10 +2,10 @@ package cz.habarta.typescript.generator; import cz.habarta.typescript.generator.parser.*; -import cz.habarta.typescript.generator.util.Predicate; import io.github.classgraph.ScanResult; import java.lang.reflect.*; import java.util.*; +import java.util.function.Predicate; import javax.ws.rs.*; import javax.ws.rs.core.*; diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/JaxrsNamespacing.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/RestNamespacing.java similarity index 74% rename from typescript-generator-core/src/main/java/cz/habarta/typescript/generator/JaxrsNamespacing.java rename to typescript-generator-core/src/main/java/cz/habarta/typescript/generator/RestNamespacing.java index 20132d953..a2b9c26a8 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/JaxrsNamespacing.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/RestNamespacing.java @@ -2,7 +2,7 @@ package cz.habarta.typescript.generator; -public enum JaxrsNamespacing { +public enum RestNamespacing { singleObject, perResource, byAnnotation 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 356d01267..cbf7f63a5 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 @@ -5,13 +5,16 @@ import cz.habarta.typescript.generator.compiler.ModelCompiler; import cz.habarta.typescript.generator.emitter.EmitterExtension; import cz.habarta.typescript.generator.emitter.EmitterExtensionFeatures; -import cz.habarta.typescript.generator.util.Predicate; +import cz.habarta.typescript.generator.parser.JaxrsApplicationParser; +import cz.habarta.typescript.generator.parser.RestApplicationParser; +import cz.habarta.typescript.generator.util.Pair; import cz.habarta.typescript.generator.util.Utils; import java.io.File; import java.lang.annotation.Annotation; import java.net.URL; import java.net.URLClassLoader; import java.util.*; +import java.util.function.Predicate; import java.util.regex.Pattern; @@ -59,12 +62,19 @@ public class Settings { public boolean ignoreSwaggerAnnotations = false; public boolean generateJaxrsApplicationInterface = false; public boolean generateJaxrsApplicationClient = false; - public JaxrsNamespacing jaxrsNamespacing; - public Class jaxrsNamespacingAnnotation = null; - public String jaxrsNamespacingAnnotationElement; // default is "value" + public boolean generateSpringApplicationInterface = false; + public boolean generateSpringApplicationClient = false; + public boolean scanSpringApplication; + @Deprecated public RestNamespacing jaxrsNamespacing; + @Deprecated public Class jaxrsNamespacingAnnotation = null; + @Deprecated public String jaxrsNamespacingAnnotationElement; // default is "value" + public RestNamespacing restNamespacing; + public Class restNamespacingAnnotation = null; + public String restNamespacingAnnotationElement; // default is "value" public String restResponseType = null; public String restOptionsType = null; public boolean restOptionsTypeIsGeneric; + private List restApplicationParserFactories; public TypeProcessor customTypeProcessor = null; public boolean sortDeclarations = false; public boolean sortTypeDeclarations = false; @@ -260,21 +270,35 @@ public void validate() { if (generateJaxrsApplicationClient && outputFileType != TypeScriptFileType.implementationFile) { throw new RuntimeException("'generateJaxrsApplicationClient' can only be used when generating implementation file ('outputFileType' parameter is 'implementationFile')."); } - final boolean generateJaxrs = generateJaxrsApplicationClient || generateJaxrsApplicationInterface; - if (jaxrsNamespacing != null && !generateJaxrs) { - throw new RuntimeException("'jaxrsNamespacing' parameter can only be used when generating JAX-RS client or interface."); + if (generateSpringApplicationClient && outputFileType != TypeScriptFileType.implementationFile) { + throw new RuntimeException("'generateSpringApplicationClient' can only be used when generating implementation file ('outputFileType' parameter is 'implementationFile')."); } - if (jaxrsNamespacingAnnotation != null && jaxrsNamespacing != JaxrsNamespacing.byAnnotation) { - throw new RuntimeException("'jaxrsNamespacingAnnotation' parameter can only be used when 'jaxrsNamespacing' parameter is set to 'byAnnotation'."); + if (jaxrsNamespacing != null) { + TypeScriptGenerator.getLogger().warning("Parameter 'jaxrsNamespacing' is deprecated. Use 'restNamespacing' parameter."); + if (restNamespacing == null) { + restNamespacing = jaxrsNamespacing; + } + } + if (jaxrsNamespacingAnnotation != null) { + TypeScriptGenerator.getLogger().warning("Parameter 'jaxrsNamespacingAnnotation' is deprecated. Use 'restNamespacingAnnotation' parameter."); + if (restNamespacingAnnotation == null) { + restNamespacingAnnotation = jaxrsNamespacingAnnotation; + } } - if (jaxrsNamespacingAnnotation == null && jaxrsNamespacing == JaxrsNamespacing.byAnnotation) { - throw new RuntimeException("'jaxrsNamespacingAnnotation' must be specified when 'jaxrsNamespacing' parameter is set to 'byAnnotation'."); + if (restNamespacing != null && !isGenerateRest()) { + throw new RuntimeException("'restNamespacing' parameter can only be used when generating REST client or interface."); } - if (restResponseType != null && !generateJaxrs) { - throw new RuntimeException("'restResponseType' parameter can only be used when generating JAX-RS client or interface."); + if (restNamespacingAnnotation != null && restNamespacing != RestNamespacing.byAnnotation) { + throw new RuntimeException("'restNamespacingAnnotation' parameter can only be used when 'restNamespacing' parameter is set to 'byAnnotation'."); } - if (restOptionsType != null && !generateJaxrs) { - throw new RuntimeException("'restOptionsType' parameter can only be used when generating JAX-RS client or interface."); + if (restNamespacingAnnotation == null && restNamespacing == RestNamespacing.byAnnotation) { + throw new RuntimeException("'restNamespacingAnnotation' must be specified when 'restNamespacing' parameter is set to 'byAnnotation'."); + } + if (restResponseType != null && !isGenerateRest()) { + throw new RuntimeException("'restResponseType' parameter can only be used when generating REST client or interface."); + } + if (restOptionsType != null && !isGenerateRest()) { + throw new RuntimeException("'restOptionsType' parameter can only be used when generating REST client or interface."); } if (generateInfoJson && outputKind != TypeScriptOutputKind.module) { throw new RuntimeException("'generateInfoJson' can only be used when generating proper module ('outputKind' parameter is 'module')."); @@ -381,14 +405,32 @@ public boolean test(String className) { return mapClassesAsClassesFilter; } + @Deprecated public void setJaxrsNamespacingAnnotation(ClassLoader classLoader, String jaxrsNamespacingAnnotation) { - if (jaxrsNamespacingAnnotation != null) { - final String[] split = jaxrsNamespacingAnnotation.split("#"); - final String className = split[0]; - final String elementName = split.length > 1 ? split[1] : "value"; - this.jaxrsNamespacingAnnotation = loadClass(classLoader, className, Annotation.class); - this.jaxrsNamespacingAnnotationElement = elementName; + final Pair, String> pair = resolveRestNamespacingAnnotation(classLoader, jaxrsNamespacingAnnotation); + if (pair != null) { + this.jaxrsNamespacingAnnotation = pair.getValue1(); + this.jaxrsNamespacingAnnotationElement = pair.getValue2(); + } + } + + public void setRestNamespacingAnnotation(ClassLoader classLoader, String restNamespacingAnnotation) { + final Pair, String> pair = resolveRestNamespacingAnnotation(classLoader, restNamespacingAnnotation); + if (pair != null) { + this.restNamespacingAnnotation = pair.getValue1(); + this.restNamespacingAnnotationElement = pair.getValue2(); + } + } + + private static Pair, String> resolveRestNamespacingAnnotation(ClassLoader classLoader, String restNamespacingAnnotation) { + if (restNamespacingAnnotation == null) { + return null; } + final String[] split = restNamespacingAnnotation.split("#"); + final String className = split[0]; + final String elementName = split.length > 1 ? split[1] : "value"; + final Class annotationClass = loadClass(classLoader, className, Annotation.class); + return Pair.of(annotationClass, elementName); } public void setRestOptionsType(String restOptionsType) { @@ -403,6 +445,47 @@ public void setRestOptionsType(String restOptionsType) { } } + public List getRestApplicationParserFactories() { + if (restApplicationParserFactories == null) { + final List factories = new ArrayList<>(); + if (isGenerateJaxrs() || !isGenerateSpring()) { + factories.add(new JaxrsApplicationParser.Factory()); + } + if (isGenerateSpring()) { + final String springClassName = "cz.habarta.typescript.generator.spring.SpringApplicationParser$Factory"; + final Class springClass; + try { + springClass = Class.forName(springClassName); + } catch (ClassNotFoundException e) { + throw new RuntimeException("'generateStringApplicationInterface' or 'generateStringApplicationClient' parameter " + + "was specified but '" + springClassName + "' was not found. " + + "Please add 'cz.habarta.typescript-generator:typescript-generator-spring' artifact " + + "to typescript-generator plugin dependencies (not module dependencies)."); + } + try { + final Object instance = springClass.getConstructor().newInstance(); + factories.add((RestApplicationParser.Factory) instance); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + restApplicationParserFactories = factories; + } + return restApplicationParserFactories; + } + + public boolean isGenerateJaxrs() { + return generateJaxrsApplicationInterface || generateJaxrsApplicationClient; + } + + public boolean isGenerateSpring() { + return generateSpringApplicationInterface || generateSpringApplicationClient; + } + + public boolean isGenerateRest() { + return isGenerateJaxrs() || isGenerateSpring(); + } + public boolean areDefaultStringEnumsOverriddenByExtension() { return defaultStringEnumsOverriddenByExtension; } diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/TypeProcessor.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/TypeProcessor.java index 3287e5862..fc88e7ceb 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/TypeProcessor.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/TypeProcessor.java @@ -14,6 +14,19 @@ public interface TypeProcessor { */ public Result processType(Type javaType, Context context); + public default Result processTypeInTemporaryContext(Type type, Settings settings) { + return processType(type, new Context(new SymbolTable(settings), this)); + } + + public default List> discoverClassesUsedInType(Type type, Settings settings) { + final TypeProcessor.Result result = processTypeInTemporaryContext(type, settings); + return result != null ? result.getDiscoveredClasses() : Collections.emptyList(); + } + + public default boolean isTypeExcluded(Type type, Settings settings) { + final TypeProcessor.Result result = processTypeInTemporaryContext(type, settings); + return result != null && result.tsType == TsType.Any; + } public static class Context { diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/TypeScriptGenerator.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/TypeScriptGenerator.java index 8f496ceb6..91a02b40c 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/TypeScriptGenerator.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/TypeScriptGenerator.java @@ -8,6 +8,8 @@ import java.io.*; import java.util.*; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class TypeScriptGenerator { @@ -17,7 +19,7 @@ public class TypeScriptGenerator { private static Logger logger = new Logger(); private final Settings settings; - private TypeProcessor typeProcessor = null; + private TypeProcessor commonTypeProcessor = null; private ModelParser modelParser = null; private ModelCompiler modelCompiler = null; private Emitter emitter = null; @@ -55,6 +57,7 @@ public void generateTypeScript(Input input, Output output) { generateTypeScript(input, output, false, 0); } + @Deprecated public void generateEmbeddableTypeScript(Input input, Output output, boolean addExportKeyword, int initialIndentationLevel) { generateTypeScript(input, output, addExportKeyword, initialIndentationLevel); } @@ -117,17 +120,32 @@ private void generateNpmPackageJson(Output output) { } } - public TypeProcessor getTypeProcessor() { - if (typeProcessor == null) { - final List processors = new ArrayList<>(); - processors.add(new ExcludingTypeProcessor(settings.getExcludeFilter())); - if (settings.customTypeProcessor != null) { - processors.add(settings.customTypeProcessor); - } - processors.add(new CustomMappingTypeProcessor(settings.customTypeMappings)); - processors.add(new DefaultTypeProcessor()); - typeProcessor = new TypeProcessor.Chain(processors); + public TypeProcessor getCommonTypeProcessor() { + if (commonTypeProcessor == null) { + final List restFactories = settings.getRestApplicationParserFactories(); + final ModelParser.Factory modelParserFactory = getModelParserFactory(); + final List specificTypeProcessors = Stream + .concat( + restFactories.stream().map(factory -> factory.getSpecificTypeProcessor()), + Stream.of(modelParserFactory.getSpecificTypeProcessor()) + ) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + commonTypeProcessor = createTypeProcessor(specificTypeProcessors); + } + return commonTypeProcessor; + } + + private TypeProcessor createTypeProcessor(List specificTypeProcessors) { + final List processors = new ArrayList<>(); + processors.add(new ExcludingTypeProcessor(settings.getExcludeFilter())); + if (settings.customTypeProcessor != null) { + processors.add(settings.customTypeProcessor); } + processors.add(new CustomMappingTypeProcessor(settings.customTypeMappings)); + processors.addAll(specificTypeProcessors); + processors.add(new DefaultTypeProcessor()); + final TypeProcessor typeProcessor = new TypeProcessor.Chain(processors); return typeProcessor; } @@ -139,13 +157,21 @@ public ModelParser getModelParser() { } private ModelParser createModelParser() { + final List factories = settings.getRestApplicationParserFactories(); + final List restApplicationParsers = factories.stream() + .map(factory -> factory.create(settings, getCommonTypeProcessor())) + .collect(Collectors.toList()); + return getModelParserFactory().create(settings, getCommonTypeProcessor(), restApplicationParsers); + } + + private ModelParser.Factory getModelParserFactory() { switch (settings.jsonLibrary) { case jackson1: - return new Jackson1Parser(settings, getTypeProcessor()); + return new Jackson1Parser.Factory(); case jackson2: - return new Jackson2Parser(settings, getTypeProcessor()); + return new Jackson2Parser.Jackson2ParserFactory(); case jaxb: - return new Jackson2Parser(settings, getTypeProcessor(), /*useJaxbAnnotations*/ true); + return new Jackson2Parser.JaxbParserFactory(); default: throw new RuntimeException(); } @@ -153,8 +179,7 @@ private ModelParser createModelParser() { public ModelCompiler getModelCompiler() { if (modelCompiler == null) { - final TypeProcessor specificTypeProcessor = getModelParser().getSpecificTypeProcessor(); - modelCompiler = new ModelCompiler(settings, specificTypeProcessor); + modelCompiler = new ModelCompiler(settings, getCommonTypeProcessor()); } return modelCompiler; } diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/compiler/ModelCompiler.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/compiler/ModelCompiler.java index 214718957..540d7142c 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/compiler/ModelCompiler.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/compiler/ModelCompiler.java @@ -4,6 +4,7 @@ import cz.habarta.typescript.generator.*; import cz.habarta.typescript.generator.emitter.*; import cz.habarta.typescript.generator.parser.*; +import cz.habarta.typescript.generator.util.Pair; import cz.habarta.typescript.generator.util.Utils; import java.lang.annotation.Annotation; import java.lang.reflect.*; @@ -57,22 +58,26 @@ public TsModel javaToTypeScript(Model model) { tsModel = removeInheritedProperties(symbolTable, tsModel); tsModel = addImplementedProperties(symbolTable, tsModel); - // JAX-RS - if (settings.generateJaxrsApplicationInterface || settings.generateJaxrsApplicationClient) { - final JaxrsApplicationModel jaxrsApplication = model.getJaxrsApplication() != null ? model.getJaxrsApplication() : new JaxrsApplicationModel(); - final Symbol responseSymbol = createJaxrsResponseType(symbolTable, tsModel); + // REST + if (settings.isGenerateRest()) { + final Symbol responseSymbol = createRestResponseType(symbolTable, tsModel); final TsType optionsType = settings.restOptionsType != null ? new TsType.VerbatimType(settings.restOptionsType) : null; final TsType.GenericVariableType optionsGenericVariable = settings.restOptionsTypeIsGeneric ? new TsType.GenericVariableType(settings.restOptionsType) : null; - - if (settings.generateJaxrsApplicationInterface) { - tsModel = createJaxrsInterfaces(symbolTable, tsModel, jaxrsApplication, responseSymbol, optionsGenericVariable, optionsType); + final List restApplicationsWithInterface = model.getRestApplications().stream() + .filter(restApplication -> restApplication.getType().generateInterface.apply(settings)) + .collect(Collectors.toList()); + final List restApplicationsWithClient = model.getRestApplications().stream() + .filter(restApplication -> restApplication.getType().generateClient.apply(settings)) + .collect(Collectors.toList()); + if (!restApplicationsWithInterface.isEmpty()) { + tsModel = createRestInterfaces(symbolTable, tsModel, restApplicationsWithInterface, responseSymbol, optionsGenericVariable, optionsType); } - if (settings.generateJaxrsApplicationClient) { - tsModel = createJaxrsClients(symbolTable, tsModel, jaxrsApplication, responseSymbol, optionsGenericVariable, optionsType); + if (!restApplicationsWithClient.isEmpty()) { + tsModel = createRestClients(symbolTable, tsModel, restApplicationsWithClient, responseSymbol, optionsGenericVariable, optionsType); } } @@ -420,7 +425,7 @@ private static List getImplementedProperties(SymbolTable symbol return properties; } - private Symbol createJaxrsResponseType(SymbolTable symbolTable, TsModel tsModel) { + private Symbol createRestResponseType(SymbolTable symbolTable, TsModel tsModel) { // response type final Symbol responseSymbol = symbolTable.getSyntheticSymbol("RestResponse"); final TsType.GenericVariableType varR = new TsType.GenericVariableType("R"); @@ -435,10 +440,10 @@ private Symbol createJaxrsResponseType(SymbolTable symbolTable, TsModel tsModel) return responseSymbol; } - private TsModel createJaxrsInterfaces(SymbolTable symbolTable, TsModel tsModel, JaxrsApplicationModel jaxrsApplication, + private TsModel createRestInterfaces(SymbolTable symbolTable, TsModel tsModel, List restApplications, Symbol responseSymbol, TsType.GenericVariableType optionsGenericVariable, TsType optionsType) { final List typeParameters = Utils.listFromNullable(optionsGenericVariable); - final Map> groupedMethods = processJaxrsMethods(jaxrsApplication, symbolTable, null, responseSymbol, optionsType, false); + final Map> groupedMethods = processRestMethods(restApplications, symbolTable, null, responseSymbol, optionsType, false); for (Map.Entry> entry : groupedMethods.entrySet()) { final TsBeanModel interfaceModel = new TsBeanModel(null, TsBeanCategory.Service, false, entry.getKey(), typeParameters, null, null, null, null, null, entry.getValue(), null); tsModel.getBeans().add(interfaceModel); @@ -446,7 +451,7 @@ private TsModel createJaxrsInterfaces(SymbolTable symbolTable, TsModel tsModel, return tsModel; } - private TsModel createJaxrsClients(SymbolTable symbolTable, TsModel tsModel, JaxrsApplicationModel jaxrsApplication, + private TsModel createRestClients(SymbolTable symbolTable, TsModel tsModel, List restApplications, Symbol responseSymbol, TsType.GenericVariableType optionsGenericVariable, TsType optionsType) { final Symbol httpClientSymbol = symbolTable.getSyntheticSymbol("HttpClient"); final List typeParameters = Utils.listFromNullable(optionsGenericVariable); @@ -476,11 +481,12 @@ private TsModel createJaxrsClients(SymbolTable symbolTable, TsModel tsModel, Jax Collections.emptyList(), null ); - final String groupingSuffix = settings.generateJaxrsApplicationInterface ? null : "Client"; - final Map> groupedMethods = processJaxrsMethods(jaxrsApplication, symbolTable, groupingSuffix, responseSymbol, optionsType, true); + final boolean bothInterfacesAndClients = settings.generateJaxrsApplicationInterface || settings.generateSpringApplicationInterface; + final String groupingSuffix = bothInterfacesAndClients ? null : "Client"; + final Map> groupedMethods = processRestMethods(restApplications, symbolTable, groupingSuffix, responseSymbol, optionsType, true); for (Map.Entry> entry : groupedMethods.entrySet()) { - final Symbol symbol = settings.generateJaxrsApplicationInterface ? symbolTable.addSuffixToSymbol(entry.getKey(), "Client") : entry.getKey(); - final TsType interfaceType = settings.generateJaxrsApplicationInterface ? new TsType.ReferenceType(entry.getKey()) : null; + final Symbol symbol = bothInterfacesAndClients ? symbolTable.addSuffixToSymbol(entry.getKey(), "Client") : entry.getKey(); + final TsType interfaceType = bothInterfacesAndClients ? new TsType.ReferenceType(entry.getKey()) : null; final TsBeanModel clientModel = new TsBeanModel(null, TsBeanCategory.Service, true, symbol, typeParameters, null, null, Utils.listFromNullable(interfaceType), null, constructor, entry.getValue(), null); tsModel.getBeans().add(clientModel); @@ -490,40 +496,43 @@ private TsModel createJaxrsClients(SymbolTable symbolTable, TsModel tsModel, Jax return tsModel; } - private Map> processJaxrsMethods(JaxrsApplicationModel jaxrsApplication, SymbolTable symbolTable, String nameSuffix, Symbol responseSymbol, TsType optionsType, boolean implement) { + private Map> processRestMethods(List restApplications, SymbolTable symbolTable, String nameSuffix, Symbol responseSymbol, TsType optionsType, boolean implement) { final Map> result = new LinkedHashMap<>(); - final Map> groupedMethods = groupingByMethodContainer(jaxrsApplication, symbolTable, nameSuffix); - for (Map.Entry> entry : groupedMethods.entrySet()) { - result.put(entry.getKey(), processJaxrsMethodGroup(jaxrsApplication, entry.getValue(), symbolTable, responseSymbol, optionsType, implement)); + final Map>> groupedMethods = groupingByMethodContainer(restApplications, symbolTable, nameSuffix); + for (Map.Entry>> entry : groupedMethods.entrySet()) { + result.put(entry.getKey(), processRestMethodGroup(entry.getValue(), symbolTable, responseSymbol, optionsType, implement)); } return result; } - private List processJaxrsMethodGroup(JaxrsApplicationModel jaxrsApplication, List methods, SymbolTable symbolTable, Symbol responseSymbol, TsType optionsType, boolean implement) { + private List processRestMethodGroup(List> methods, SymbolTable symbolTable, Symbol responseSymbol, TsType optionsType, boolean implement) { final List resultMethods = new ArrayList<>(); final Map methodNamesCount = groupingByMethodName(methods); - for (JaxrsMethodModel method : methods) { + for (Pair pair : methods) { + final RestApplicationModel restApplication = pair.getValue1(); + final RestMethodModel method = pair.getValue2(); final boolean createLongName = methodNamesCount.get(method.getName()) > 1; - resultMethods.add(processJaxrsMethod(symbolTable, jaxrsApplication.getApplicationPath(), responseSymbol, method, createLongName, optionsType, implement)); + resultMethods.add(processRestMethod(symbolTable, restApplication.getApplicationPath(), responseSymbol, method, createLongName, optionsType, implement)); } return resultMethods; } - private Map> groupingByMethodContainer(JaxrsApplicationModel jaxrsApplication, SymbolTable symbolTable, String nameSuffix) { - return jaxrsApplication.getMethods().stream() + private Map>> groupingByMethodContainer(List restApplications, SymbolTable symbolTable, String nameSuffix) { + return restApplications.stream() + .flatMap(restApplication -> restApplication.getMethods().stream().map(method -> Pair.of(restApplication, method))) .collect(Collectors.groupingBy( - method -> getContainerSymbol(jaxrsApplication, symbolTable, nameSuffix, method), - Utils.toSortedList(Comparator.comparing(method -> method.getPath())) + pair -> getContainerSymbol(pair.getValue1(), symbolTable, nameSuffix, pair.getValue2()), + Utils.toSortedList(Comparator.comparing(pair -> pair.getValue2().getPath())) )); } - private Symbol getContainerSymbol(JaxrsApplicationModel jaxrsApplication, SymbolTable symbolTable, String nameSuffix, JaxrsMethodModel method) { - if (settings.jaxrsNamespacing == JaxrsNamespacing.perResource) { + private Symbol getContainerSymbol(RestApplicationModel restApplication, SymbolTable symbolTable, String nameSuffix, RestMethodModel method) { + if (settings.restNamespacing == RestNamespacing.perResource) { return symbolTable.getSymbol(method.getRootResource(), nameSuffix); } - if (settings.jaxrsNamespacing == JaxrsNamespacing.byAnnotation) { - final Annotation annotation = method.getRootResource().getAnnotation(settings.jaxrsNamespacingAnnotation); - final String element = settings.jaxrsNamespacingAnnotationElement != null ? settings.jaxrsNamespacingAnnotationElement : "value"; + if (settings.restNamespacing == RestNamespacing.byAnnotation) { + final Annotation annotation = method.getRootResource().getAnnotation(settings.restNamespacingAnnotation); + final String element = settings.restNamespacingAnnotationElement != null ? settings.restNamespacingAnnotationElement : "value"; final String annotationValue = Utils.getAnnotationElementValue(annotation, element, String.class); if (annotationValue != null) { if (isValidIdentifierName(annotationValue)) { @@ -533,19 +542,21 @@ private Symbol getContainerSymbol(JaxrsApplicationModel jaxrsApplication, Symbol } } } - final String applicationName = getApplicationName(jaxrsApplication); + final String applicationName = getApplicationName(restApplication); return symbolTable.getSyntheticSymbol(applicationName, nameSuffix); } - private static String getApplicationName(JaxrsApplicationModel jaxrsApplication) { - return jaxrsApplication.getApplicationName() != null ? jaxrsApplication.getApplicationName() : "RestApplication"; + private static String getApplicationName(RestApplicationModel restApplication) { + return restApplication.getApplicationName() != null ? restApplication.getApplicationName() : "RestApplication"; } - private static Map groupingByMethodName(List methods) { - return methods.stream().collect(Collectors.groupingBy(JaxrsMethodModel::getName, Collectors.counting())); + private static Map groupingByMethodName(List> methods) { + return methods.stream() + .map(pair -> pair.getValue2()) + .collect(Collectors.groupingBy(RestMethodModel::getName, Collectors.counting())); } - private TsMethodModel processJaxrsMethod(SymbolTable symbolTable, String pathPrefix, Symbol responseSymbol, JaxrsMethodModel method, boolean createLongName, TsType optionsType, boolean implement) { + private TsMethodModel processRestMethod(SymbolTable symbolTable, String pathPrefix, Symbol responseSymbol, RestMethodModel method, boolean createLongName, TsType optionsType, boolean implement) { final String path = Utils.joinPath(pathPrefix, method.getPath()); final PathTemplate pathTemplate = PathTemplate.parse(path); final List comments = Utils.concat(method.getComments(), Arrays.asList( diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Jackson1Parser.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Jackson1Parser.java index 11050f3e9..d0c005c65 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Jackson1Parser.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Jackson1Parser.java @@ -19,8 +19,26 @@ public class Jackson1Parser extends ModelParser { private final ObjectMapper objectMapper = new ObjectMapper(); - public Jackson1Parser(Settings settings, TypeProcessor typeProcessor) { - super(settings, typeProcessor, Arrays.asList(JsonNode.class.getName())); + public static class Factory extends ModelParser.Factory { + + @Override + public TypeProcessor getSpecificTypeProcessor() { + return createSpecificTypeProcessor(); + } + + @Override + public Jackson1Parser create(Settings settings, TypeProcessor commonTypeProcessor, List restApplicationParsers) { + return new Jackson1Parser(settings, commonTypeProcessor, restApplicationParsers); + } + + } + + public Jackson1Parser(Settings settings, TypeProcessor commonTypeProcessor) { + this(settings, commonTypeProcessor, Collections.emptyList()); + } + + public Jackson1Parser(Settings settings, TypeProcessor commonTypeProcessor, List restApplicationParsers) { + super(settings, commonTypeProcessor, restApplicationParsers); if (!settings.optionalAnnotations.isEmpty()) { final AnnotationIntrospector defaultAnnotationIntrospector = objectMapper.getSerializationConfig().getAnnotationIntrospector(); final AnnotationIntrospector allAnnotationIntrospector = new NopAnnotationIntrospector() { @@ -33,6 +51,10 @@ public boolean isHandled(Annotation ann) { } } + private static TypeProcessor createSpecificTypeProcessor() { + return new ExcludingTypeProcessor(Arrays.asList(JsonNode.class.getName())); + } + @Override protected DeclarationModel parseClass(SourceType> sourceClass) { if (sourceClass.type.isEnum()) { diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Jackson2Parser.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Jackson2Parser.java index e3dcb3bd3..4bdc24451 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Jackson2Parser.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Jackson2Parser.java @@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.ser.BeanSerializerFactory; import com.fasterxml.jackson.databind.ser.DefaultSerializerProvider; import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; +import cz.habarta.typescript.generator.ExcludingTypeProcessor; import cz.habarta.typescript.generator.Jackson2ConfigurationResolved; import cz.habarta.typescript.generator.OptionalProperties; import cz.habarta.typescript.generator.Settings; @@ -35,7 +36,6 @@ import cz.habarta.typescript.generator.TypeScriptGenerator; import cz.habarta.typescript.generator.compiler.EnumKind; import cz.habarta.typescript.generator.compiler.EnumMemberModel; -import cz.habarta.typescript.generator.util.Predicate; import cz.habarta.typescript.generator.util.UnionType; import cz.habarta.typescript.generator.util.Utils; import java.lang.annotation.Annotation; @@ -46,19 +46,71 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; public class Jackson2Parser extends ModelParser { + public static class Jackson2ParserFactory extends ModelParser.Factory { + + private final boolean useJaxbAnnotations; + + public Jackson2ParserFactory() { + this(false); + } + + private Jackson2ParserFactory(boolean useJaxbAnnotations) { + this.useJaxbAnnotations = useJaxbAnnotations; + } + + @Override + public TypeProcessor getSpecificTypeProcessor() { + return createSpecificTypeProcessor(); + } + + @Override + public Jackson2Parser create(Settings settings, TypeProcessor commonTypeProcessor, List restApplicationParsers) { + return new Jackson2Parser(settings, commonTypeProcessor, restApplicationParsers, useJaxbAnnotations); + } + + } + + public static class JaxbParserFactory extends Jackson2ParserFactory { + + public JaxbParserFactory() { + super(true); + } + + } + private final ObjectMapper objectMapper = new ObjectMapper(); public Jackson2Parser(Settings settings, TypeProcessor typeProcessor) { - this(settings, typeProcessor, false); + this(settings, typeProcessor, Collections.emptyList(), false); + } + + public Jackson2Parser(Settings settings, TypeProcessor commonTypeProcessor, List restApplicationParsers, boolean useJaxbAnnotations) { + super(settings, commonTypeProcessor, restApplicationParsers); + if (settings.jackson2ModuleDiscovery) { + objectMapper.registerModules(ObjectMapper.findModules(settings.classLoader)); + } + for (Class moduleClass : settings.jackson2Modules) { + try { + objectMapper.registerModule(moduleClass.newInstance()); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(String.format("Cannot instantiate Jackson2 module '%s'", moduleClass.getName()), e); + } + } + if (useJaxbAnnotations) { + AnnotationIntrospector introspector = new JaxbAnnotationIntrospector(objectMapper.getTypeFactory()); + objectMapper.setAnnotationIntrospector(introspector); + } final Jackson2ConfigurationResolved config = settings.jackson2Configuration; if (config != null) { setVisibility(PropertyAccessor.FIELD, config.fieldVisibility); @@ -91,22 +143,8 @@ private void setShapeOverride(Class cls, JsonFormat.Shape shape) { JsonFormat.Value.forShape(shape))); } - public Jackson2Parser(Settings settings, TypeProcessor typeProcessor, boolean useJaxbAnnotations) { - super(settings, typeProcessor, Arrays.asList(JsonNode.class.getName())); - if (settings.jackson2ModuleDiscovery) { - objectMapper.registerModules(ObjectMapper.findModules(settings.classLoader)); - } - for (Class moduleClass : settings.jackson2Modules) { - try { - objectMapper.registerModule(moduleClass.newInstance()); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(String.format("Cannot instantiate Jackson2 module '%s'", moduleClass.getName()), e); - } - } - if (useJaxbAnnotations) { - AnnotationIntrospector introspector = new JaxbAnnotationIntrospector(objectMapper.getTypeFactory()); - objectMapper.setAnnotationIntrospector(introspector); - } + private static TypeProcessor createSpecificTypeProcessor() { + return new ExcludingTypeProcessor(Arrays.asList(JsonNode.class.getName())); } @Override diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Javadoc.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Javadoc.java index 269960eec..90aaec725 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Javadoc.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Javadoc.java @@ -51,7 +51,7 @@ public Model enrichModel(Model model) { final EnumModel dEnumModel = enrichEnum(enumModel); dEnums.add(dEnumModel); } - return new Model(dBeans, dEnums, model.getJaxrsApplication()); + return new Model(dBeans, dEnums, model.getRestApplications()); } private BeanModel enrichBean(BeanModel bean) { diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JaxrsApplicationParser.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JaxrsApplicationParser.java index 2caa3cec3..cb59c8bdf 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JaxrsApplicationParser.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JaxrsApplicationParser.java @@ -3,8 +3,9 @@ import cz.habarta.typescript.generator.JaxrsApplicationScanner; import cz.habarta.typescript.generator.Settings; +import cz.habarta.typescript.generator.TsType; +import cz.habarta.typescript.generator.TypeProcessor; import cz.habarta.typescript.generator.TypeScriptGenerator; -import cz.habarta.typescript.generator.util.Predicate; import cz.habarta.typescript.generator.util.Utils; import java.lang.annotation.Annotation; import java.lang.reflect.Field; @@ -12,7 +13,13 @@ import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; import javax.ws.rs.ApplicationPath; import javax.ws.rs.BeanParam; import javax.ws.rs.CookieParam; @@ -31,34 +38,42 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; -public class JaxrsApplicationParser { +public class JaxrsApplicationParser extends RestApplicationParser { - private final Settings settings; - private final Predicate isClassNameExcluded; - private final Set defaultExcludes; - private final JaxrsApplicationModel model; + public static class Factory extends RestApplicationParser.Factory { - public JaxrsApplicationParser(Settings settings) { - this.settings = settings; - this.isClassNameExcluded = settings.getExcludeFilter(); - this.defaultExcludes = new LinkedHashSet<>(getDefaultExcludedClassNames()); - this.model = new JaxrsApplicationModel(); - } - - public JaxrsApplicationModel getModel() { - return model; - } - - public static class Result { - public List> discoveredTypes; - public Result() { - discoveredTypes = new ArrayList<>(); + @Override + public TypeProcessor getSpecificTypeProcessor() { + return (javaType, context) -> { + final Class rawClass = Utils.getRawClassOrNull(javaType); + if (rawClass != null) { + for (Map.Entry, TsType> entry : getStandardEntityClassesMapping().entrySet()) { + final Class cls = entry.getKey(); + final TsType type = entry.getValue(); + if (cls.isAssignableFrom(rawClass)) { + return type != null ? new TypeProcessor.Result(type) : null; + } + } + if (getDefaultExcludedClassNames().contains(rawClass.getName())) { + return new TypeProcessor.Result(TsType.Any); + } + } + return null; + }; } - public Result(List> discoveredTypes) { - this.discoveredTypes = discoveredTypes; + + @Override + public JaxrsApplicationParser create(Settings settings, TypeProcessor commonTypeProcessor) { + return new JaxrsApplicationParser(settings, commonTypeProcessor); } + + }; + + public JaxrsApplicationParser(Settings settings, TypeProcessor commonTypeProcessor) { + super(settings, commonTypeProcessor, new RestApplicationModel(RestApplicationType.Jaxrs)); } + @Override public Result tryParse(SourceType sourceType) { if (!(sourceType.type instanceof Class)) { return null; @@ -100,20 +115,7 @@ private void parseResource(Result result, ResourceContext context, Class reso final ResourceContext subContext = context.subPathParamTypes(pathParamTypes); // parse resource methods final List methods = Arrays.asList(resourceClass.getMethods()); - Collections.sort(methods, new Comparator() { - @Override - public int compare(Method o1, Method o2) { - final int nameDiff = o1.getName().compareToIgnoreCase(o2.getName()); - if (nameDiff != 0) { - return nameDiff; - } - final int parameterTypesDiff = Arrays.asList(o1.getParameterTypes()).toString().compareTo(Arrays.asList(o2.getParameterTypes()).toString()); - if (parameterTypesDiff != 0) { - return parameterTypesDiff; - } - return 0; - } - }); + Collections.sort(methods, Utils.methodComparator()); for (Method method : methods) { parseResourceMethod(result, subContext, resourceClass, method); } @@ -122,7 +124,7 @@ public int compare(Method o1, Method o2) { private void parseResourceMethod(Result result, ResourceContext context, Class resourceClass, Method method) { final Path pathAnnotation = method.getAnnotation(Path.class); // subContext - context = context.subPath(pathAnnotation); + context = context.subPath(pathAnnotation != null ? pathAnnotation.value() : null); final Map pathParamTypes = new LinkedHashMap<>(); for (Parameter parameter : method.getParameters()) { final PathParam pathParamAnnotation = parameter.getAnnotation(PathParam.class); @@ -208,7 +210,7 @@ private void parseResourceMethod(Result result, ResourceContext context, Class comments = Swagger.getOperationComments(swaggerOperation); // create method - model.getMethods().add(new JaxrsMethodModel(resourceClass, method.getName(), modelReturnType, + model.getMethods().add(new RestMethodModel(resourceClass, method.getName(), modelReturnType, context.rootResource, httpMethod.value(), context.path, pathParams, queryParams, entityParameter, comments)); } // JAX-RS specification - 3.4.1 Sub Resources @@ -217,32 +219,6 @@ private void parseResourceMethod(Result result, ResourceContext context, Class usedInClass, String usedInMember) { - if (!isExcluded(type)) { - result.discoveredTypes.add(new SourceType<>(type, usedInClass, usedInMember)); - } - } - - private boolean isExcluded(Type type) { - final Class cls = Utils.getRawClassOrNull(type); - if (cls == null) { - return false; - } - if (isClassNameExcluded != null && isClassNameExcluded.test(cls.getName())) { - return true; - } - if (defaultExcludes.contains(cls.getName())) { - return true; - } - - for (Class standardEntityClass : getStandardEntityClasses()) { - if (standardEntityClass.isAssignableFrom(cls)) { - return true; - } - } - return false; - } - private static HttpMethod getHttpMethod(Method method) { for (Annotation annotation : method.getAnnotations()) { final HttpMethod httpMethodAnnotation = annotation.annotationType().getAnnotation(HttpMethod.class); @@ -255,7 +231,7 @@ private static HttpMethod getHttpMethod(Method method) { private static MethodParameterModel getEntityParameter(Method method) { for (Parameter parameter : method.getParameters()) { - if (!hasAnyAnnotation(parameter, Arrays.asList( + if (!Utils.hasAnyAnnotation(parameter::getAnnotation, Arrays.asList( MatrixParam.class, QueryParam.class, PathParam.class, @@ -265,7 +241,7 @@ private static MethodParameterModel getEntityParameter(Method method) { Context.class, FormParam.class, BeanParam.class - ))) { + ))) { return new MethodParameterModel(parameter.getName(), parameter.getParameterizedType()); } } @@ -273,41 +249,42 @@ private static MethodParameterModel getEntityParameter(Method method) { } private static boolean hasAnyAnnotation(Parameter[] parameters, List> annotationClasses) { - for (Parameter parameter : parameters) { - if (hasAnyAnnotation(parameter, annotationClasses)) { - return true; - } - } - return false; + return Stream.of(parameters) + .anyMatch(parameter -> Utils.hasAnyAnnotation(parameter::getAnnotation, annotationClasses)); } - private static boolean hasAnyAnnotation(Parameter parameter, List> annotationClasses) { - for (Class annotationClass : annotationClasses) { - for (Annotation parameterAnnotation : parameter.getAnnotations()) { - if (annotationClass.isInstance(parameterAnnotation)) { - return true; - } - } + private static Map, TsType> getStandardEntityClassesMapping() { + // JAX-RS specification - 4.2.4 Standard Entity Providers + if (standardEntityClassesMapping == null) { + final Map, TsType> map = new LinkedHashMap<>(); + // null value means that class is handled by DefaultTypeProcessor + map.put(byte[].class, TsType.Any); + map.put(java.lang.String.class, null); + map.put(java.io.InputStream.class, TsType.Any); + map.put(java.io.Reader.class, TsType.Any); + map.put(java.io.File.class, TsType.Any); + map.put(javax.activation.DataSource.class, TsType.Any); + map.put(javax.xml.transform.Source.class, TsType.Any); + map.put(javax.xml.bind.JAXBElement.class, null); + map.put(MultivaluedMap.class, TsType.Any); + map.put(StreamingOutput.class, TsType.Any); + map.put(java.lang.Boolean.class, null); + map.put(java.lang.Character.class, null); + map.put(java.lang.Number.class, null); + map.put(long.class, null); + map.put(int.class, null); + map.put(short.class, null); + map.put(byte.class, null); + map.put(double.class, null); + map.put(float.class, null); + map.put(boolean.class, null); + map.put(char.class, null); + standardEntityClassesMapping = map; } - return false; + return standardEntityClassesMapping; } - public static List> getStandardEntityClasses() { - // JAX-RS specification - 4.2.4 Standard Entity Providers - return Arrays.asList( - byte[].class, - java.lang.String.class, - java.io.InputStream.class, - java.io.Reader.class, - java.io.File.class, - javax.activation.DataSource.class, - javax.xml.transform.Source.class, - javax.xml.bind.JAXBElement.class, - MultivaluedMap.class, - StreamingOutput.class, - java.lang.Boolean.class, java.lang.Character.class, java.lang.Number.class, - long.class, int.class, short.class, byte.class, double.class, float.class, boolean.class, char.class); - } + private static Map, TsType> standardEntityClassesMapping; private static List getDefaultExcludedClassNames() { return Arrays.asList( @@ -315,34 +292,4 @@ private static List getDefaultExcludedClassNames() { ); } - private static class ResourceContext { - public final Class rootResource; - public final String path; - public final Map pathParamTypes; - - public ResourceContext(Class rootResource, String path) { - this(rootResource, path, new LinkedHashMap()); - } - - private ResourceContext(Class rootResource, String path, Map pathParamTypes) { - this.rootResource = rootResource; - this.path = path; - this.pathParamTypes = pathParamTypes; - } - - ResourceContext subPath(Path pathAnnotation) { - final String subPath = pathAnnotation != null ? pathAnnotation.value() : null; - return new ResourceContext(rootResource, Utils.joinPath(path, subPath), pathParamTypes); - } - - ResourceContext subPathParamTypes(Map subPathParamTypes) { - final Map newPathParamTypes = new LinkedHashMap<>(); - newPathParamTypes.putAll(pathParamTypes); - if (subPathParamTypes != null) { - newPathParamTypes.putAll(subPathParamTypes); - } - return new ResourceContext(rootResource, path, newPathParamTypes); - } - } - } diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Model.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Model.java index 721184ab4..2e22a43d9 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Model.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Model.java @@ -8,14 +8,14 @@ public class Model { private final List beans; private final List enums; - private final JaxrsApplicationModel jaxrsApplication; + private final List restApplications; - public Model(List beans, List enums, JaxrsApplicationModel jaxrsApplication) { + public Model(List beans, List enums, List restApplications) { if (beans == null) throw new NullPointerException(); if (enums == null) throw new NullPointerException(); this.beans = beans; this.enums = enums; - this.jaxrsApplication = jaxrsApplication; + this.restApplications = restApplications; } public List getBeans() { @@ -35,8 +35,8 @@ public List getEnums() { return enums; } - public JaxrsApplicationModel getJaxrsApplication() { - return jaxrsApplication; + public List getRestApplications() { + return restApplications; } @Override diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/ModelParser.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/ModelParser.java index d4a37f969..b909668c7 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/ModelParser.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/ModelParser.java @@ -4,7 +4,6 @@ import cz.habarta.typescript.generator.*; import cz.habarta.typescript.generator.compiler.EnumKind; import cz.habarta.typescript.generator.compiler.EnumMemberModel; -import cz.habarta.typescript.generator.compiler.SymbolTable; import cz.habarta.typescript.generator.util.Utils; import java.lang.annotation.Annotation; import java.lang.reflect.Field; @@ -13,30 +12,33 @@ import java.lang.reflect.Type; import java.util.*; import java.util.function.Function; +import java.util.stream.Collectors; public abstract class ModelParser { protected final Settings settings; - protected final TypeProcessor typeProcessor; private final Javadoc javadoc; - private final Queue> typeQueue = new LinkedList<>(); + private final Queue> typeQueue; + private final TypeProcessor commonTypeProcessor; + private final List restApplicationParsers; + + public static abstract class Factory { + + public TypeProcessor getSpecificTypeProcessor() { + return null; + } + + public abstract ModelParser create(Settings settings, TypeProcessor commonTypeProcessor, List restApplicationParsers); - public ModelParser(Settings settings, TypeProcessor typeProcessor) { - this(settings, typeProcessor, null); } - public ModelParser(Settings settings, TypeProcessor typeProcessor, List parserSpecificExcludes) { + public ModelParser(Settings settings, TypeProcessor commonTypeProcessor, List restApplicationParsers) { this.settings = settings; - this.typeProcessor = new TypeProcessor.Chain( - new ExcludingTypeProcessor(parserSpecificExcludes), - typeProcessor - ); this.javadoc = new Javadoc(settings.javadocXmlFiles); - } - - public TypeProcessor getSpecificTypeProcessor() { - return typeProcessor; + this.typeQueue = new LinkedList<>(); + this.restApplicationParsers = restApplicationParsers; + this.commonTypeProcessor = commonTypeProcessor; } public Model parseModel(Type type) { @@ -54,7 +56,6 @@ public Model parseModel(List> types) { } private Model parseQueue() { - final JaxrsApplicationParser jaxrsApplicationParser = new JaxrsApplicationParser(settings); final Collection parsedTypes = new ArrayList<>(); // do not use hashcodes, we can only count on `equals` since we use custom `ParameterizedType`s final List beans = new ArrayList<>(); final List enums = new ArrayList<>(); @@ -65,14 +66,20 @@ private Model parseQueue() { } parsedTypes.add(sourceType.type); - // JAX-RS resource - final JaxrsApplicationParser.Result jaxrsResult = jaxrsApplicationParser.tryParse(sourceType); - if (jaxrsResult != null) { - typeQueue.addAll(jaxrsResult.discoveredTypes); + // REST resource + boolean parsedByRestApplicationParser = false; + for (RestApplicationParser restApplicationParser : restApplicationParsers) { + final JaxrsApplicationParser.Result jaxrsResult = restApplicationParser.tryParse(sourceType); + if (jaxrsResult != null) { + typeQueue.addAll(jaxrsResult.discoveredTypes); + parsedByRestApplicationParser = true; + } + } + if (parsedByRestApplicationParser) { continue; } - final TypeProcessor.Result result = processType(sourceType.type); + final TypeProcessor.Result result = commonTypeProcessor.processTypeInTemporaryContext(sourceType.type, settings); if (result != null) { if (sourceType.type instanceof Class && result.getTsType() instanceof TsType.ReferenceType) { final Class cls = (Class) sourceType.type; @@ -92,7 +99,10 @@ private Model parseQueue() { } } } - return new Model(beans, enums, jaxrsApplicationParser.getModel()); + final List restModels = restApplicationParsers.stream() + .map(RestApplicationParser::getModel) + .collect(Collectors.toList()); + return new Model(beans, enums, restModels); } protected abstract DeclarationModel parseClass(SourceType> sourceClass); @@ -149,22 +159,13 @@ protected void addBeanToQueue(SourceType sourceType) { } protected PropertyModel processTypeAndCreateProperty(String name, Type type, boolean optional, Class usedInClass, Member originalMember, PropertyModel.PullProperties pullProperties) { - List> classes = discoverClassesUsedInType(type); + final List> classes = commonTypeProcessor.discoverClassesUsedInType(type, settings); for (Class cls : classes) { typeQueue.add(new SourceType<>(cls, usedInClass, name)); } return new PropertyModel(name, type, optional, originalMember, pullProperties, null); } - private List> discoverClassesUsedInType(Type type) { - final TypeProcessor.Result result = processType(type); - return result != null ? result.getDiscoveredClasses() : Collections.>emptyList(); - } - - private TypeProcessor.Result processType(Type type) { - return typeProcessor.processType(type, new TypeProcessor.Context(new SymbolTable(settings), typeProcessor)); - } - public static boolean containsProperty(List properties, String propertyName) { for (PropertyModel property : properties) { if (property.getName().equals(propertyName)) { diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JaxrsApplicationModel.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationModel.java similarity index 61% rename from typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JaxrsApplicationModel.java rename to typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationModel.java index 2c4cdb453..b1d18ad0d 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JaxrsApplicationModel.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationModel.java @@ -4,11 +4,20 @@ import java.util.*; -public class JaxrsApplicationModel { +public class RestApplicationModel { + private final RestApplicationType type; private String applicationPath; private String applicationName; - private final List methods = new ArrayList<>(); + private final List methods = new ArrayList<>(); + + public RestApplicationModel(RestApplicationType type) { + this.type = type; + } + + public RestApplicationType getType() { + return type; + } public String getApplicationPath() { return applicationPath; @@ -26,7 +35,7 @@ public void setApplicationName(String applicationName) { this.applicationName = applicationName; } - public List getMethods() { + public List getMethods() { return methods; } diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationParser.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationParser.java new file mode 100644 index 000000000..2b0dfe198 --- /dev/null +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationParser.java @@ -0,0 +1,89 @@ + +package cz.habarta.typescript.generator.parser; + +import cz.habarta.typescript.generator.Settings; +import cz.habarta.typescript.generator.TypeProcessor; +import cz.habarta.typescript.generator.util.Utils; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; + +public abstract class RestApplicationParser { + + protected final Settings settings; + protected final Predicate isClassNameExcluded; + protected final TypeProcessor commonTypeProcessor; + protected final RestApplicationModel model; + + public static abstract class Factory { + + public TypeProcessor getSpecificTypeProcessor() { + return null; + } + + public abstract RestApplicationParser create(Settings settings, TypeProcessor commonTypeProcessor); + + } + + public RestApplicationParser(Settings settings, TypeProcessor commonTypeProcessor, RestApplicationModel model) { + this.settings = settings; + this.isClassNameExcluded = settings.getExcludeFilter(); + this.commonTypeProcessor = commonTypeProcessor; + this.model = model; + } + + public RestApplicationModel getModel() { + return model; + } + + protected abstract Result tryParse(SourceType sourceType); + + public static class Result { + public List> discoveredTypes; + public Result() { + discoveredTypes = new ArrayList<>(); + } + public Result(List> discoveredTypes) { + this.discoveredTypes = discoveredTypes; + } + } + + protected void foundType(Result result, Type type, Class usedInClass, String usedInMember) { + if (!commonTypeProcessor.isTypeExcluded(type, settings)) { + result.discoveredTypes.add(new SourceType<>(type, usedInClass, usedInMember)); + } + } + + protected static class ResourceContext { + public final Class rootResource; + public final String path; + public final Map pathParamTypes; + + public ResourceContext(Class rootResource, String path) { + this(rootResource, path, new LinkedHashMap()); + } + + private ResourceContext(Class rootResource, String path, Map pathParamTypes) { + this.rootResource = rootResource; + this.path = path; + this.pathParamTypes = pathParamTypes; + } + + public ResourceContext subPath(String subPath) { + return new ResourceContext(rootResource, Utils.joinPath(path, subPath), pathParamTypes); + } + + public ResourceContext subPathParamTypes(Map subPathParamTypes) { + final Map newPathParamTypes = new LinkedHashMap<>(); + newPathParamTypes.putAll(pathParamTypes); + if (subPathParamTypes != null) { + newPathParamTypes.putAll(subPathParamTypes); + } + return new ResourceContext(rootResource, path, newPathParamTypes); + } + } + +} diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationType.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationType.java new file mode 100644 index 000000000..209538e18 --- /dev/null +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestApplicationType.java @@ -0,0 +1,21 @@ + +package cz.habarta.typescript.generator.parser; + +import cz.habarta.typescript.generator.Settings; +import java.util.function.Function; + + +public enum RestApplicationType { + + Jaxrs(settings -> settings.generateJaxrsApplicationInterface, settings -> settings.generateJaxrsApplicationClient), + Spring(settings -> settings.generateSpringApplicationInterface, settings -> settings.generateSpringApplicationClient); + + private RestApplicationType(Function generateInterface, Function generateClient) { + this.generateInterface = generateInterface; + this.generateClient = generateClient; + } + + public final Function generateInterface; + public final Function generateClient; + +} diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JaxrsMethodModel.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestMethodModel.java similarity index 91% rename from typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JaxrsMethodModel.java rename to typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestMethodModel.java index 828cda12f..b50c60bc1 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JaxrsMethodModel.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/RestMethodModel.java @@ -5,7 +5,7 @@ import java.util.*; -public class JaxrsMethodModel extends MethodModel { +public class RestMethodModel extends MethodModel { private final Class rootResource; private final String httpMethod; @@ -14,7 +14,7 @@ public class JaxrsMethodModel extends MethodModel { private final List queryParams; private final MethodParameterModel entityParam; - public JaxrsMethodModel(Class originClass, String name, Type returnType, + public RestMethodModel(Class originClass, String name, Type returnType, Class rootResource, String httpMethod, String path, List pathParams, List queryParams, MethodParameterModel entityParam, List comments) { super(originClass, name, null, returnType, comments); diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Swagger.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Swagger.java index 17d5d7cf3..297e960c4 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Swagger.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/Swagger.java @@ -76,7 +76,7 @@ public static Model enrichModel(Model model) { final BeanModel dBean = enrichBean(bean); dBeans.add(dBean); } - return new Model(dBeans, model.getEnums(), model.getJaxrsApplication()); + return new Model(dBeans, model.getEnums(), model.getRestApplications()); } private static BeanModel enrichBean(BeanModel bean) { diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/util/Predicate.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/util/Predicate.java deleted file mode 100644 index 7bbf35683..000000000 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/util/Predicate.java +++ /dev/null @@ -1,9 +0,0 @@ - -package cz.habarta.typescript.generator.util; - - -public interface Predicate { - - boolean test(T value); - -} diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/util/Utils.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/util/Utils.java index dfafb0a91..da444765f 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/util/Utils.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/util/Utils.java @@ -14,10 +14,14 @@ import java.lang.reflect.Type; import java.util.*; import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collector; import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; public class Utils { @@ -74,15 +78,67 @@ public static Class getRawClassOrNull(Type type) { return null; } + public static Comparator methodComparator() { + return (Method m1, Method m2) -> { + final int nameDiff = m1.getName().compareToIgnoreCase(m2.getName()); + if (nameDiff != 0) { + return nameDiff; + } + final int parameterTypesDiff = Arrays.asList(m1.getParameterTypes()).toString().compareTo(Arrays.asList(m2.getParameterTypes()).toString()); + if (parameterTypesDiff != 0) { + return parameterTypesDiff; + } + return 0; + }; + } + + public static List getAllMethods(Class cls) { + return getInheritanceChain(cls) + .flatMap(c -> Stream.of(c.getDeclaredMethods())) + .collect(Collectors.toList()); + } + + private static Stream> getInheritanceChain(Class cls) { + return generateStream(cls, c -> c != null, (Class c) -> c.getSuperclass()) + .collect(toReversedCollection()) + .stream(); + } + + public static Collector> toReversedCollection() { + return Collector., Collection>of( + ArrayDeque::new, + (deque, item) -> deque.addFirst(item), + (deque1, deque2) -> { deque2.addAll(deque1); return deque2; }, + deque -> deque); + } + + // remove on Java 9 and replace with Stream.iterate + private static Stream generateStream(T seed, Predicate hasNext, UnaryOperator next) { + final Spliterator spliterator = Spliterators.spliteratorUnknownSize(new Iterator() { + private T last = seed; + + @Override + public boolean hasNext() { + return hasNext.test(last); + } + + @Override + public T next() { + final T current = last; + last = next.apply(last); + return current; + } + }, Spliterator.ORDERED); + + return StreamSupport.stream(spliterator, false); + } + public static boolean hasAnyAnnotation( Function, Annotation> getAnnotationFunction, List> annotations) { - for (Class annotation : annotations) { - if (getAnnotationFunction.apply(annotation) != null) { - return true; - } - } - return false; + return annotations.stream() + .map(getAnnotationFunction) + .anyMatch(Objects::nonNull); } public static T getAnnotationElementValue(AnnotatedElement annotatedElement, String annotationClassName, String annotationElementName, Class annotationElementType) { @@ -230,11 +286,11 @@ private static String normalizeLineEndings(String text, String lineEndings) { return text.replaceAll("\\r\\n|\\n|\\r", lineEndings); } - public static List splitMultiline(String text, boolean trimLines) { + public static List splitMultiline(String text, boolean trimOneLeadingSpaceOnLines) { final List result = new ArrayList<>(); final String[] lines = text.split("\\r\\n|\\n|\\r"); for (String line : lines) { - result.add(trimLines ? trimOneLeadingSpaceOnly(line) : line); + result.add(trimOneLeadingSpaceOnLines ? trimOneLeadingSpaceOnly(line) : line); } return result; } diff --git a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/CustomTypeConversionTest.java b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/CustomTypeConversionTest.java index f9b969e12..6b9b372ce 100644 --- a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/CustomTypeConversionTest.java +++ b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/CustomTypeConversionTest.java @@ -57,7 +57,7 @@ public TypeProcessor.Result processType(Type javaType, TypeProcessor.Context con return null; } }; - final TypeProcessor typeProcessor = new TypeScriptGenerator(settings).getTypeProcessor(); + final TypeProcessor typeProcessor = new TypeScriptGenerator(settings).getCommonTypeProcessor(); final TypeProcessor.Context context = DefaultTypeProcessorTest.getTestContext(typeProcessor); { final Type maybeObjectFieldType = CustomOptionalUsage.class.getField("maybeObject").getGenericType(); diff --git a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ExtensionTest.java b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ExtensionTest.java index 736e49662..77a535bbd 100644 --- a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ExtensionTest.java +++ b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ExtensionTest.java @@ -48,7 +48,7 @@ public Model transformModel(SymbolTable symbolTable, Model model) { beans.remove(implementationBean); beans.add(beanWithComments); - return new Model(beans, model.getEnums(), model.getJaxrsApplication()); + return new Model(beans, model.getEnums(), model.getRestApplications()); } })); } diff --git a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/JaxrsApplicationTest.java b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/JaxrsApplicationTest.java index 9d5a115d2..969edafcf 100644 --- a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/JaxrsApplicationTest.java +++ b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/JaxrsApplicationTest.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.core.type.*; import cz.habarta.typescript.generator.compiler.ModelCompiler; import cz.habarta.typescript.generator.parser.*; -import cz.habarta.typescript.generator.util.Predicate; import io.github.classgraph.ClassGraph; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -12,6 +11,7 @@ import java.lang.reflect.*; import java.net.URI; import java.util.*; +import java.util.function.Predicate; import javax.activation.*; import javax.ws.rs.*; import javax.ws.rs.container.AsyncResponse; @@ -40,7 +40,8 @@ public void testReturnedTypesFromApplication() { @Test public void testReturnedTypesFromResource() { - final JaxrsApplicationParser.Result result = new JaxrsApplicationParser(TestUtils.settings()).tryParse(new SourceType<>(TestResource1.class)); + JaxrsApplicationParser jaxrsApplicationParser = createJaxrsApplicationParser(TestUtils.settings()); + final JaxrsApplicationParser.Result result = jaxrsApplicationParser.tryParse(new SourceType<>(TestResource1.class)); Assert.assertNotNull(result); List types = getTypes(result.discoveredTypes); final List expectedTypes = Arrays.asList( @@ -54,7 +55,9 @@ public void testReturnedTypesFromResource() { G.class, new TypeReference>(){}.getType(), I.class, - J[].class + J[].class, + // types handled by DefaultTypeProcessor + String.class, Boolean.class, Character.class, Number.class, Integer.class, int.class ); assertHasSameItems(expectedTypes, types); } @@ -114,13 +117,19 @@ public void testExcludedType() { A.class.getName(), J.class.getName() ), null); - final JaxrsApplicationParser jaxrsApplicationParser = new JaxrsApplicationParser(settings); + final JaxrsApplicationParser jaxrsApplicationParser = createJaxrsApplicationParser(settings); final JaxrsApplicationParser.Result result = jaxrsApplicationParser.tryParse(new SourceType<>(TestResource1.class)); Assert.assertNotNull(result); Assert.assertTrue(!getTypes(result.discoveredTypes).contains(A.class)); Assert.assertTrue(getTypes(result.discoveredTypes).contains(J[].class)); } + private static JaxrsApplicationParser createJaxrsApplicationParser(Settings settings) { + final TypeProcessor typeProcessor = new TypeScriptGenerator(settings).getCommonTypeProcessor(); + final JaxrsApplicationParser jaxrsApplicationParser = new JaxrsApplicationParser(settings, typeProcessor); + return jaxrsApplicationParser; + } + private List getTypes(final List> sourceTypes) { final List types = new ArrayList<>(); for (SourceType sourceType : sourceTypes) { @@ -369,7 +378,7 @@ public void testNamespacingPerResource() { settings.outputFileType = TypeScriptFileType.implementationFile; settings.generateJaxrsApplicationInterface = true; settings.generateJaxrsApplicationClient = true; - settings.jaxrsNamespacing = JaxrsNamespacing.perResource; + settings.jaxrsNamespacing = RestNamespacing.perResource; final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(OrganizationApplication.class)); final String errorMessage = "Unexpected output: " + output; Assert.assertTrue(errorMessage, !output.contains("class OrganizationApplicationClient")); @@ -384,7 +393,7 @@ public void testNamespacingByAnnotation() { settings.outputFileType = TypeScriptFileType.implementationFile; settings.generateJaxrsApplicationInterface = true; settings.generateJaxrsApplicationClient = true; - settings.jaxrsNamespacing = JaxrsNamespacing.byAnnotation; + settings.jaxrsNamespacing = RestNamespacing.byAnnotation; settings.jaxrsNamespacingAnnotation = Api.class; final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(OrganizationApplication.class)); final String errorMessage = "Unexpected output: " + output; diff --git a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ext/AxiosClientExtensionTest.java b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ext/AxiosClientExtensionTest.java index 8beb2c8ac..3855c7bee 100644 --- a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ext/AxiosClientExtensionTest.java +++ b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ext/AxiosClientExtensionTest.java @@ -3,7 +3,7 @@ import cz.habarta.typescript.generator.Input; import cz.habarta.typescript.generator.JaxrsApplicationTest; -import cz.habarta.typescript.generator.JaxrsNamespacing; +import cz.habarta.typescript.generator.RestNamespacing; import cz.habarta.typescript.generator.Settings; import cz.habarta.typescript.generator.TestUtils; import cz.habarta.typescript.generator.TypeScriptFileType; @@ -21,7 +21,7 @@ public void test() { settings.outputFileType = TypeScriptFileType.implementationFile; settings.outputKind = TypeScriptOutputKind.module; settings.generateJaxrsApplicationClient = true; - settings.jaxrsNamespacing = JaxrsNamespacing.perResource; + settings.jaxrsNamespacing = RestNamespacing.perResource; settings.extensions.add(new AxiosClientExtension()); final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(JaxrsApplicationTest.OrganizationApplication.class)); final String errorMessage = "Unexpected output: " + output; 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 c1add64fe..da28eef8f 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 @@ -56,8 +56,13 @@ public class GenerateTask extends DefaultTask { public boolean ignoreSwaggerAnnotations; public boolean generateJaxrsApplicationInterface; public boolean generateJaxrsApplicationClient; - public JaxrsNamespacing jaxrsNamespacing; - public String jaxrsNamespacingAnnotation; + public boolean generateSpringApplicationInterface; + public boolean generateSpringApplicationClient; + public boolean scanSpringApplication; + @Deprecated public RestNamespacing jaxrsNamespacing; + @Deprecated public String jaxrsNamespacingAnnotation; + public RestNamespacing restNamespacing; + public String restNamespacingAnnotation; public String restResponseType; public String restOptionsType; public String customTypeProcessor; @@ -148,8 +153,13 @@ public void generate() throws Exception { settings.ignoreSwaggerAnnotations = ignoreSwaggerAnnotations; settings.generateJaxrsApplicationInterface = generateJaxrsApplicationInterface; settings.generateJaxrsApplicationClient = generateJaxrsApplicationClient; + settings.generateSpringApplicationInterface = generateSpringApplicationInterface; + settings.generateSpringApplicationClient = generateSpringApplicationClient; + settings.scanSpringApplication = scanSpringApplication; settings.jaxrsNamespacing = jaxrsNamespacing; settings.setJaxrsNamespacingAnnotation(classLoader, jaxrsNamespacingAnnotation); + settings.restNamespacing = restNamespacing; + settings.setRestNamespacingAnnotation(classLoader, restNamespacingAnnotation); settings.restResponseType = restResponseType; settings.setRestOptionsType(restOptionsType); settings.loadCustomTypeProcessor(classLoader, customTypeProcessor); 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 760300c35..4af8a31bc 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 @@ -92,13 +92,13 @@ public class GenerateMojo extends AbstractMojo { private List moduleDependencies; /** - * JSON classes to process. + * Classes to process. */ @Parameter private List classes; /** - * JSON classes to process specified using glob patterns + * Classes to process specified using glob patterns * so it is possible to specify package or class name suffix. * Glob patterns support two wildcards: *
    @@ -111,19 +111,19 @@ public class GenerateMojo extends AbstractMojo { private List classPatterns; /** - * JSON classes to process specified by annotations. + * Classes to process specified by annotations. */ @Parameter private List classesWithAnnotations; /** - * JSON classes to process specified by implemented interface. + * Classes to process specified by implemented interface. */ @Parameter private List classesImplementingInterfaces; /** - * Scans specified JAX-RS {@link javax.ws.rs.core.Application} for JSON classes to process. + * Scans specified JAX-RS {@link javax.ws.rs.core.Application} for classes to process. * Parameter contains fully-qualified class name. * It is possible to exclude particular REST resource classes using {@link #excludeClasses} parameter. */ @@ -387,6 +387,39 @@ public class GenerateMojo extends AbstractMojo { @Parameter private boolean generateJaxrsApplicationClient; + /** + * If true interface for Spring REST application will be generated. + */ + @Parameter + private boolean generateSpringApplicationInterface; + + /** + * If true client for Spring REST application will be generated. + */ + @Parameter + private boolean generateSpringApplicationClient; + + /** + * If true Spring REST application will be loaded and scanned for classes to process. + * It is needed to specify application class using another parameter (for example {@link #classes}). + */ + @Parameter + private boolean scanSpringApplication; + + /** + * Deprecated, use {@link #restNamespacing}. + */ + @Deprecated + @Parameter + private RestNamespacing jaxrsNamespacing; + + /** + * Deprecated, use {@link #restNamespacingAnnotation}. + */ + @Deprecated + @Parameter + private String jaxrsNamespacingAnnotation; + /** * Specifies how JAX-RS REST operations will be grouped into objects. * Supported values are: @@ -398,7 +431,7 @@ public class GenerateMojo extends AbstractMojo { * Default value is singleObject. */ @Parameter - private JaxrsNamespacing jaxrsNamespacing; + private RestNamespacing restNamespacing; /** * Specifies annotation used for grouping JAX-RS REST operations. @@ -411,7 +444,7 @@ public class GenerateMojo extends AbstractMojo { *
*/ @Parameter - private String jaxrsNamespacingAnnotation; + private String restNamespacingAnnotation; /** * Specifies HTTP response type in JAXRS application. @@ -681,8 +714,13 @@ public void execute() { settings.ignoreSwaggerAnnotations = ignoreSwaggerAnnotations; settings.generateJaxrsApplicationInterface = generateJaxrsApplicationInterface; settings.generateJaxrsApplicationClient = generateJaxrsApplicationClient; + settings.generateSpringApplicationInterface = generateSpringApplicationInterface; + settings.generateSpringApplicationClient = generateSpringApplicationClient; + settings.scanSpringApplication = scanSpringApplication; settings.jaxrsNamespacing = jaxrsNamespacing; settings.setJaxrsNamespacingAnnotation(classLoader, jaxrsNamespacingAnnotation); + settings.restNamespacing = restNamespacing; + settings.setRestNamespacingAnnotation(classLoader, restNamespacingAnnotation); settings.restResponseType = restResponseType; settings.setRestOptionsType(restOptionsType); settings.loadCustomTypeProcessor(classLoader, customTypeProcessor); diff --git a/typescript-generator-spring/pom.xml b/typescript-generator-spring/pom.xml new file mode 100644 index 000000000..0ab693858 --- /dev/null +++ b/typescript-generator-spring/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + + cz.habarta.typescript-generator + typescript-generator + 2.12-SNAPSHOT + + + typescript-generator-spring + jar + typescript-generator-spring + + + + + + + + cz.habarta.typescript-generator + typescript-generator-core + 2.12-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-web + 2.1.1.RELEASE + + + org.springframework.boot + spring-boot-starter-logging + + + + + + cz.habarta.typescript-generator + typescript-generator-core + 2.12-SNAPSHOT + test-jar + test + + + junit + junit + 4.12 + test + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + check + verify + + check + + + + + + + + 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 new file mode 100644 index 000000000..96ac8f5c2 --- /dev/null +++ b/typescript-generator-spring/src/main/java/cz/habarta/typescript/generator/spring/SpringApplicationParser.java @@ -0,0 +1,252 @@ + +package cz.habarta.typescript.generator.spring; + +import cz.habarta.typescript.generator.Settings; +import cz.habarta.typescript.generator.TsType; +import cz.habarta.typescript.generator.TypeProcessor; +import cz.habarta.typescript.generator.TypeScriptGenerator; +import cz.habarta.typescript.generator.parser.JaxrsApplicationParser; +import cz.habarta.typescript.generator.parser.MethodParameterModel; +import cz.habarta.typescript.generator.parser.PathTemplate; +import cz.habarta.typescript.generator.parser.RestApplicationModel; +import cz.habarta.typescript.generator.parser.RestApplicationParser; +import cz.habarta.typescript.generator.parser.RestApplicationType; +import cz.habarta.typescript.generator.parser.RestMethodModel; +import cz.habarta.typescript.generator.parser.SourceType; +import cz.habarta.typescript.generator.util.Utils; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + + +public class SpringApplicationParser extends RestApplicationParser { + + // This factory class is instantiated using reflections! + public static class Factory extends RestApplicationParser.Factory { + + @Override + public TypeProcessor getSpecificTypeProcessor() { + return (javaType, context) -> { + final Class rawClass = Utils.getRawClassOrNull(javaType); + if (rawClass != null) { + for (Map.Entry, TsType> entry : getStandardEntityClassesMapping().entrySet()) { + final Class cls = entry.getKey(); + final TsType type = entry.getValue(); + if (cls.isAssignableFrom(rawClass) && type != null) { + return new TypeProcessor.Result(type); + } + } + if (getDefaultExcludedClassNames().contains(rawClass.getName())) { + return new TypeProcessor.Result(TsType.Any); + } + } + return null; + }; + } + + @Override + public RestApplicationParser create(Settings settings, TypeProcessor commonTypeProcessor) { + return new SpringApplicationParser(settings, commonTypeProcessor); + } + + }; + + public SpringApplicationParser(Settings settings, TypeProcessor commonTypeProcessor) { + super(settings, commonTypeProcessor, new RestApplicationModel(RestApplicationType.Spring)); + } + + @Override + public JaxrsApplicationParser.Result tryParse(SourceType sourceType) { + if (!(sourceType.type instanceof Class)) { + return null; + } + final Class cls = (Class) sourceType.type; + + // application + final SpringBootApplication app = cls.getAnnotation(SpringBootApplication.class); + if (app != null) { + TypeScriptGenerator.getLogger().verbose("Scanning Spring application: " + cls.getName()); + if (settings.scanSpringApplication) { + final ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(settings.classLoader); + final SpringApplicationHelper springApplicationHelper = new SpringApplicationHelper(settings.classLoader, cls); + final List> restControllers = springApplicationHelper.findRestControllers(); + return new JaxrsApplicationParser.Result(restControllers.stream() + .map(controller -> new SourceType(controller, cls, "")) + .collect(Collectors.toList()) + ); + } finally { + Thread.currentThread().setContextClassLoader(originalContextClassLoader); + } + } else { + return null; + } + } + + // controller + final RestController controller = cls.getAnnotation(RestController.class); + if (controller != null) { + TypeScriptGenerator.getLogger().verbose("Parsing Spring RestController: " + cls.getName()); + final JaxrsApplicationParser.Result result = new JaxrsApplicationParser.Result(); + final RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(cls, RequestMapping.class); + final String path = requestMapping != null && requestMapping.path() != null ? requestMapping.path()[0] : null; + final JaxrsApplicationParser.ResourceContext context = new JaxrsApplicationParser.ResourceContext(cls, path); + parseController(result, context, cls); + return result; + } + + return null; + } + + private class SpringApplicationHelper extends SpringApplication { + + private final ClassLoader classLoader; + + public SpringApplicationHelper(ClassLoader classLoader, Class... primarySources) { + super(primarySources); + this.classLoader = classLoader; + } + + public List> findRestControllers() { + try (ConfigurableApplicationContext context = createApplicationContext()) { + load(context, getAllSources().toArray()); + context.refresh(); + final List> classes = Stream.of(context.getBeanDefinitionNames()) + .map(beanName -> context.getBeanFactory().getBeanDefinition(beanName).getBeanClassName()) + .filter(Objects::nonNull) + .filter(className -> isClassNameExcluded == null || !isClassNameExcluded.test(className)) + .map(className -> { + try { + return classLoader.loadClass(className); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + }) + .filter(instance -> instance.isAnnotationPresent(RestController.class)) + .collect(Collectors.toList()); + return classes; + } + } + + } + + private void parseController(JaxrsApplicationParser.Result result, JaxrsApplicationParser.ResourceContext context, Class controllerClass) { + // parse controller methods + final List methods = Utils.getAllMethods(controllerClass); + methods.sort(Utils.methodComparator()); + for (Method method : methods) { + parseControllerMethod(result, context, controllerClass, method); + } + } + + // https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods + private void parseControllerMethod(JaxrsApplicationParser.Result result, JaxrsApplicationParser.ResourceContext context, Class controllerClass, Method method) { + final RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class); + if (requestMapping != null) { + + // subContext + context = context.subPath(requestMapping.path().length == 0 ? "" : requestMapping.path()[0]); + final Map pathParamTypes = new LinkedHashMap<>(); + for (Parameter parameter : method.getParameters()) { + final PathVariable pathVariableAnnotation = parameter.getAnnotation(PathVariable.class); + if (pathVariableAnnotation != null) { + pathParamTypes.put(pathVariableAnnotation.value(), parameter.getParameterizedType()); + } + } + context = context.subPathParamTypes(pathParamTypes); + final RequestMethod httpMethod = requestMapping.method().length == 0 ? RequestMethod.GET : requestMapping.method()[0]; + + // path parameters + final PathTemplate pathTemplate = PathTemplate.parse(context.path); + final Map contextPathParamTypes = context.pathParamTypes; + final List pathParams = pathTemplate.getParts().stream() + .filter(PathTemplate.Parameter.class::isInstance) + .map(PathTemplate.Parameter.class::cast) + .map(parameter -> { + final Type type = contextPathParamTypes.get(parameter.getOriginalName()); + final Type paramType = type != null ? type : String.class; + foundType(result, paramType, controllerClass, method.getName()); + return new MethodParameterModel(parameter.getValidName(), paramType); + }) + .collect(Collectors.toList()); + + // query parameters + final List queryParams = new ArrayList<>(); + for (Parameter param : method.getParameters()) { + final RequestParam requestParamAnnotation = param.getAnnotation(RequestParam.class); + if (requestParamAnnotation != null) { + queryParams.add(new MethodParameterModel(requestParamAnnotation.value(), param.getParameterizedType())); + foundType(result, param.getParameterizedType(), controllerClass, method.getName()); + } + } + + // entity parameter + final MethodParameterModel entityParameter = getEntityParameter(method); + if (entityParameter != null) { + foundType(result, entityParameter.getType(), controllerClass, method.getName()); + } + + // return Type + final Class returnType = method.getReturnType(); + final Type genericReturnType = method.getGenericReturnType(); + final Type modelReturnType; + if (genericReturnType instanceof ParameterizedType && returnType == ResponseEntity.class) { + final ParameterizedType parameterizedReturnType = (ParameterizedType) genericReturnType; + modelReturnType = parameterizedReturnType.getActualTypeArguments()[0]; + foundType(result, modelReturnType, controllerClass, method.getName()); + } else { + modelReturnType = genericReturnType; + foundType(result, modelReturnType, controllerClass, method.getName()); + } + + model.getMethods().add(new RestMethodModel(controllerClass, method.getName(), modelReturnType, + controllerClass, httpMethod.name(), context.path, pathParams, queryParams, entityParameter, null)); + } + } + + private static MethodParameterModel getEntityParameter(Method method) { + for (Parameter parameter : method.getParameters()) { + final RequestBody requestBodyAnnotation = parameter.getAnnotation(RequestBody.class); + if (requestBodyAnnotation != null) { + return new MethodParameterModel(parameter.getName(), parameter.getParameterizedType()); + } + } + return null; + } + + private static Map, TsType> getStandardEntityClassesMapping() { + if (standardEntityClassesMapping == null) { + final Map, TsType> map = new LinkedHashMap<>(); + standardEntityClassesMapping = map; + } + return standardEntityClassesMapping; + } + + private static Map, TsType> standardEntityClassesMapping; + + private static List getDefaultExcludedClassNames() { + return Arrays.asList( + ); + } +} 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 new file mode 100644 index 000000000..6f3d18dc6 --- /dev/null +++ b/typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTest.java @@ -0,0 +1,137 @@ + +package cz.habarta.typescript.generator.spring; + +import cz.habarta.typescript.generator.Input; +import cz.habarta.typescript.generator.Settings; +import cz.habarta.typescript.generator.TestUtils; +import cz.habarta.typescript.generator.TypeScriptFileType; +import cz.habarta.typescript.generator.TypeScriptGenerator; +import cz.habarta.typescript.generator.util.Utils; +import java.lang.reflect.Method; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + + +public class SpringTest { + + @Test + public void testAnnotationUtils() { + final Method greetingMethod = getMethod(SpringTestApplication.GreetingController.class, "greeting"); + final RequestMapping mapping = AnnotatedElementUtils.findMergedAnnotation(greetingMethod, RequestMapping.class); + Assert.assertNotNull(mapping); + Assert.assertEquals(0, mapping.method().length); + Assert.assertEquals(1, mapping.path().length); + Assert.assertEquals("/greeting", mapping.path()[0]); + } + + private static Method getMethod(Class cls, String methodName) { + final Method greetingMethod = Utils.getAllMethods(cls).stream() + .filter(method -> method.getName().equals(methodName)) + .findFirst() + .get(); + return greetingMethod; + } + + @Test + public void testApplicationScan() { + final Settings settings = TestUtils.settings(); + settings.generateSpringApplicationInterface = true; + settings.scanSpringApplication = true; + settings.classLoader = Thread.currentThread().getContextClassLoader(); + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(SpringTestApplication.class)); + Assert.assertTrue(output.contains("interface RestApplication")); + Assert.assertTrue(output.contains("greeting(queryParams?: { name?: string; }): RestResponse")); + Assert.assertTrue(output.contains("interface Greeting")); + } + + @Test + public void testPathParameters() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateSpringApplicationClient = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Controller1.class)); + Assert.assertTrue(output.contains("findPet(ownerId: number, petId: number): RestResponse")); + Assert.assertTrue(output.contains("uriEncoding`owners/${ownerId}/pets/${petId}`")); + Assert.assertTrue(output.contains("interface Pet")); + } + + @Test + public void testQueryParameters() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateSpringApplicationClient = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Controller2.class)); + Assert.assertTrue(output.contains("echo(queryParams?: { message?: string; }): RestResponse")); + } + + @Test + public void testEntityParameter() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateSpringApplicationClient = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Controller3.class)); + Assert.assertTrue(output.contains("setEntity(data: Data1): RestResponse")); + Assert.assertTrue(output.contains("interface Data1")); + } + + @Test + public void testReturnType() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateSpringApplicationClient = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Controller4.class)); + Assert.assertTrue(output.contains("getEntity(): RestResponse")); + Assert.assertTrue(output.contains("interface Data2")); + } + + @RestController + @RequestMapping("/owners/{ownerId}") + public static class Controller1 { + @GetMapping("/pets/{petId}") + public Pet findPet(@PathVariable("ownerId") Long ownerId, @PathVariable("petId") Long petId) { + return null; + } + } + + @RestController + public static class Controller2 { + @RequestMapping("/echo") + public String echo(@RequestParam("message") String message) { + return message; + } + } + + @RestController + public static class Controller3 { + @RequestMapping(path = "/data1", method = RequestMethod.PUT) + public void setEntity(@RequestBody Data1 data) { + } + } + + @RestController + public static class Controller4 { + @RequestMapping(path = "/data2", method = RequestMethod.GET) + public ResponseEntity getEntity() { + return null; + } + } + + public static class Pet { + } + + public static class Data1 { + } + + public static class Data2 { + } + +} diff --git a/typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTestApplication.java b/typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTestApplication.java new file mode 100644 index 000000000..7152e6461 --- /dev/null +++ b/typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTestApplication.java @@ -0,0 +1,51 @@ + +package cz.habarta.typescript.generator.spring; + +import java.util.concurrent.atomic.AtomicLong; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + + +@SpringBootApplication +public class SpringTestApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringTestApplication.class, args); + } + + @RestController + public static class GreetingController { + + private static final String template = "Hello, %s!"; + private final AtomicLong counter = new AtomicLong(); + + @RequestMapping("/greeting") + public Greeting greeting(@RequestParam(value="name", defaultValue="World") String name) { + return new Greeting(counter.incrementAndGet(), String.format(template, name)); + } + + } + + public static class Greeting { + + private final long id; + private final String content; + + public Greeting(long id, String content) { + this.id = id; + this.content = content; + } + + public long getId() { + return id; + } + + public String getContent() { + return content; + } + } + +}