From 5e7cfb45684317ab7f88439cdcfab990e80889bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20Habarta?= Date: Thu, 28 Sep 2017 09:03:54 +0200 Subject: [PATCH] Allow to use property optionality defined by Jackson2 library --- .../generator/OptionalProperties.java | 7 ++ .../typescript/generator/Settings.java | 24 ++++++- .../typescript/generator/emitter/Emitter.java | 2 +- .../generator/parser/Jackson1Parser.java | 11 ++-- .../generator/parser/Jackson2Parser.java | 21 +++--- .../generator/parser/ModelParser.java | 28 ++++++++ .../generator/Jackson2ParserTest.java | 65 +++++++++++++++++++ .../generator/gradle/GenerateTask.java | 10 ++- .../generator/maven/GenerateMojo.java | 35 +++++++++- 9 files changed, 181 insertions(+), 22 deletions(-) create mode 100644 typescript-generator-core/src/main/java/cz/habarta/typescript/generator/OptionalProperties.java diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/OptionalProperties.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/OptionalProperties.java new file mode 100644 index 000000000..d2bfb8f26 --- /dev/null +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/OptionalProperties.java @@ -0,0 +1,7 @@ + +package cz.habarta.typescript.generator; + + +public enum OptionalProperties { + useSpecifiedAnnotations, useLibraryDefinition, all +} 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 720d9be4a..ad37ba1eb 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 @@ -1,6 +1,7 @@ package cz.habarta.typescript.generator; +import com.fasterxml.jackson.databind.Module; import cz.habarta.typescript.generator.emitter.Emitter; import cz.habarta.typescript.generator.emitter.EmitterExtension; import cz.habarta.typescript.generator.emitter.EmitterExtensionFeatures; @@ -30,7 +31,8 @@ public class Settings { public String umdNamespace = null; public JsonLibrary jsonLibrary = null; private Predicate excludeFilter = null; - public boolean declarePropertiesAsOptional = false; + @Deprecated public boolean declarePropertiesAsOptional = false; + public OptionalProperties optionalProperties; // default is OptionalProperties.useSpecifiedAnnotations public boolean declarePropertiesAsReadOnly = false; public String removeTypeNamePrefix = null; public String removeTypeNameSuffix = null; @@ -69,7 +71,9 @@ public class Settings { public Map npmPackageDependencies = new LinkedHashMap<>(); public String typescriptVersion = "^2.4"; public boolean displaySerializerWarning = true; - public boolean disableJackson2ModuleDiscovery = false; + @Deprecated public boolean disableJackson2ModuleDiscovery = false; + public boolean jackson2ModuleDiscovery = false; + public List> jackson2Modules = new ArrayList<>(); public ClassLoader classLoader = null; private boolean defaultStringEnumsOverriddenByExtension = false; @@ -123,6 +127,12 @@ public void loadOptionalAnnotations(ClassLoader classLoader, List option } } + public void loadJackson2Modules(ClassLoader classLoader, List jackson2Modules) { + if (jackson2Modules != null) { + this.jackson2Modules = loadClasses(classLoader, jackson2Modules, Module.class); + } + } + public static Map convertToMap(List mappings) { final Map result = new LinkedHashMap<>(); if (mappings != null) { @@ -235,6 +245,16 @@ public void validate() { throw new RuntimeException("'npmName' and 'npmVersion' is only applicable when generating NPM 'package.json'."); } } + + if (declarePropertiesAsOptional) { + System.out.println("Warning: Parameter 'declarePropertiesAsOptional' is deprecated. Use 'optionalProperties' parameter."); + if (optionalProperties == null) { + optionalProperties = OptionalProperties.all; + } + } + if (disableJackson2ModuleDiscovery) { + System.out.println("Warning: Parameter 'disableJackson2ModuleDiscovery' was removed. See 'jackson2ModuleDiscovery' and 'jackson2Modules' parameters."); + } } private static void reportConfigurationChange(String extensionName, String parameterName, String parameterValue) { diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/Emitter.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/Emitter.java index ad3776a8f..1f451d9ce 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/Emitter.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/Emitter.java @@ -187,7 +187,7 @@ private void emitProperty(TsPropertyModel property) { emitComments(property.getComments()); final TsType tsType = property.getTsType(); final String readonly = property.readonly ? "readonly " : ""; - final String questionMark = settings.declarePropertiesAsOptional || (tsType instanceof TsType.OptionalType) ? "?" : ""; + final String questionMark = tsType instanceof TsType.OptionalType ? "?" : ""; writeIndentedLine(readonly + quoteIfNeeded(property.getName(), settings) + questionMark + ": " + tsType.format(settings) + ";"); } 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 d62e49905..41143938d 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 @@ -3,6 +3,7 @@ import cz.habarta.typescript.generator.*; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Member; import java.lang.reflect.Type; import java.util.*; @@ -48,6 +49,8 @@ private BeanModel parseBean(SourceType> sourceClass) { final BeanHelper beanHelper = getBeanHelper(sourceClass.type); if (beanHelper != null) { for (BeanPropertyWriter beanPropertyWriter : beanHelper.getProperties()) { + final Member propertyMember = beanPropertyWriter.getMember().getMember(); + checkMember(propertyMember, beanPropertyWriter.getName(), sourceClass.type); Type propertyType = beanPropertyWriter.getGenericPropertyType(); if (propertyType == JsonNode.class) { propertyType = Object.class; @@ -65,13 +68,7 @@ private BeanModel parseBean(SourceType> sourceClass) { continue; } } - boolean optional = false; - for (Class optionalAnnotation : settings.optionalAnnotations) { - if (beanPropertyWriter.getAnnotation(optionalAnnotation) != null) { - optional = true; - break; - } - } + final boolean optional = isAnnotatedPropertyOptional((AnnotatedElement) propertyMember); final Member originalMember = beanPropertyWriter.getMember().getMember(); properties.add(processTypeAndCreateProperty(beanPropertyWriter.getName(), propertyType, optional, sourceClass.type, originalMember, null)); } 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 e6278b822..e865ea7ed 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 @@ -20,6 +20,7 @@ import java.beans.PropertyDescriptor; import java.lang.annotation.Annotation; import java.lang.reflect.AccessibleObject; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; @@ -38,9 +39,16 @@ public Jackson2Parser(Settings settings, TypeProcessor typeProcessor) { public Jackson2Parser(Settings settings, TypeProcessor typeProcessor, boolean useJaxbAnnotations) { super(settings, typeProcessor); - if (!settings.disableJackson2ModuleDiscovery) { + 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); @@ -63,6 +71,7 @@ private BeanModel parseBean(SourceType> sourceClass) { if (beanHelper != null) { for (BeanPropertyWriter beanPropertyWriter : beanHelper.getProperties()) { final Member propertyMember = beanPropertyWriter.getMember().getMember(); + checkMember(propertyMember, beanPropertyWriter.getName(), sourceClass.type); Type propertyType = getGenericType(propertyMember); if (propertyType == JsonNode.class) { propertyType = Object.class; @@ -80,13 +89,9 @@ private BeanModel parseBean(SourceType> sourceClass) { continue; } } - boolean optional = false; - for (Class optionalAnnotation : settings.optionalAnnotations) { - if (beanPropertyWriter.getAnnotation(optionalAnnotation) != null) { - optional = true; - break; - } - } + final boolean optional = settings.optionalProperties == OptionalProperties.useLibraryDefinition + ? !beanPropertyWriter.isRequired() + : isAnnotatedPropertyOptional((AnnotatedElement) propertyMember); // @JsonUnwrapped PropertyModel.PullProperties pullProperties = null; final Member originalMember = beanPropertyWriter.getMember().getMember(); 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 06ab469d5..396a65148 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 @@ -5,7 +5,11 @@ import cz.habarta.typescript.generator.compiler.EnumKind; import cz.habarta.typescript.generator.compiler.SymbolTable; import cz.habarta.typescript.generator.*; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; import java.lang.reflect.Member; +import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.*; @@ -79,6 +83,30 @@ private Model parseQueue() { protected abstract DeclarationModel parseClass(SourceType> sourceClass); + protected static void checkMember(Member propertyMember, String propertyName, Class sourceClass) { + if (!(propertyMember instanceof Field) && !(propertyMember instanceof Method)) { + throw new RuntimeException(String.format( + "Unexpected member type '%s' in property '%s' in class '%s'", + propertyMember != null ? propertyMember.getClass().getName() : null, + propertyName, + sourceClass.getName())); + } + } + + protected boolean isAnnotatedPropertyOptional(AnnotatedElement annotatedProperty) { + if (settings.optionalProperties == OptionalProperties.all) { + return true; + } + if (settings.optionalProperties == null || settings.optionalProperties == OptionalProperties.useSpecifiedAnnotations) { + for (Class optionalAnnotation : settings.optionalAnnotations) { + if (annotatedProperty.getAnnotation(optionalAnnotation) != null) { + return true; + } + } + } + return false; + } + protected static DeclarationModel parseEnum(SourceType> sourceClass) { final List values = new ArrayList<>(); if (sourceClass.type.isEnum()) { diff --git a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/Jackson2ParserTest.java b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/Jackson2ParserTest.java index 267148738..f45fdb279 100644 --- a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/Jackson2ParserTest.java +++ b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/Jackson2ParserTest.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.*; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import javax.xml.bind.annotation.XmlElement; import org.junit.Assert; import org.junit.Test; @@ -109,4 +110,68 @@ public static void main(String[] args) throws JsonProcessingException { System.out.println(new ObjectMapper().writeValueAsString(new SubTypeDiscriminatedByName4())); } + @Test + public void testOptionalJsonProperty() { + final Settings settings = TestUtils.settings(); + settings.optionalProperties = OptionalProperties.useLibraryDefinition; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(ClassWithOptionals.class)); + Assert.assertTrue(output.contains("oname1?: string")); +// Assert.assertTrue(output.contains("oname2?: string")); // uncomment on Java 8 + Assert.assertTrue(output.contains("jname1?: string")); + Assert.assertTrue(output.contains("jname2?: string")); + Assert.assertTrue(output.contains("jname3: string")); + Assert.assertTrue(output.contains("jname4: string")); + Assert.assertTrue(output.contains("xname1?: string")); + Assert.assertTrue(output.contains("xname2?: string")); + Assert.assertTrue(output.contains("xname3?: string")); + Assert.assertTrue(output.contains("xname4?: string")); + } + + @Test + public void testOptionalXmlElement() { + final Settings settings = TestUtils.settings(); + settings.jsonLibrary = JsonLibrary.jaxb; + settings.optionalProperties = OptionalProperties.useLibraryDefinition; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(ClassWithOptionals.class)); + Assert.assertTrue(output.contains("oname1?: string")); +// Assert.assertTrue(output.contains("oname2?: string")); // uncomment on Java 8 + Assert.assertTrue(output.contains("jname1?: string")); + Assert.assertTrue(output.contains("jname2?: string")); + Assert.assertTrue(output.contains("jname3?: string")); + Assert.assertTrue(output.contains("jname4?: string")); + Assert.assertTrue(output.contains("xname1?: string")); + Assert.assertTrue(output.contains("xname2?: string")); + Assert.assertTrue(output.contains("xname3: string")); + Assert.assertTrue(output.contains("xname4: string")); + } + + public static class ClassWithOptionals { + public String oname1; +// public Optional oname2; // uncomment on Java 8 + + @JsonProperty + public String jname1; + @JsonProperty(required = false) + public String jname2; + @JsonProperty(required = true) + public String jname3; + private String jname4; + @JsonProperty(required = true) + public String getJname4() { + return jname4; + } + + @XmlElement + public String xname1; + @XmlElement(required = false) + public String xname2; + @XmlElement(required = true) + public String xname3; + private String xname4; + @XmlElement(required = true) + public String getXname4() { + return xname4; + } + } + } 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 d22227373..42b497598 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 @@ -27,7 +27,8 @@ public class GenerateTask extends DefaultTask { public List excludeClassPatterns; public List includePropertyAnnotations; public JsonLibrary jsonLibrary; - public boolean declarePropertiesAsOptional; + @Deprecated public boolean declarePropertiesAsOptional; + public OptionalProperties optionalProperties; public boolean declarePropertiesAsReadOnly; public String removeTypeNamePrefix; public String removeTypeNameSuffix; @@ -62,7 +63,9 @@ public class GenerateTask extends DefaultTask { public String npmVersion; public StringQuotes stringQuotes; public boolean displaySerializerWarning = true; - public boolean disableJackson2ModuleDiscovery; + @Deprecated public boolean disableJackson2ModuleDiscovery; + public boolean jackson2ModuleDiscovery; + public List jackson2Modules; public boolean debug; @TaskAction @@ -101,6 +104,7 @@ public void generate() throws Exception { settings.setExcludeFilter(excludeClasses, excludeClassPatterns); settings.jsonLibrary = jsonLibrary; settings.declarePropertiesAsOptional = declarePropertiesAsOptional; + settings.optionalProperties = optionalProperties; settings.declarePropertiesAsReadOnly = declarePropertiesAsReadOnly; settings.removeTypeNamePrefix = removeTypeNamePrefix; settings.removeTypeNameSuffix = removeTypeNameSuffix; @@ -137,6 +141,8 @@ public void generate() throws Exception { settings.setStringQuotes(stringQuotes); settings.displaySerializerWarning = displaySerializerWarning; settings.disableJackson2ModuleDiscovery = disableJackson2ModuleDiscovery; + settings.jackson2ModuleDiscovery = jackson2ModuleDiscovery; + settings.loadJackson2Modules(classLoader, jackson2Modules); settings.classLoader = classLoader; final File output = outputFile != null ? getProject().file(outputFile) 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 0d5c50b7d..3303c1d2e 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 @@ -131,11 +131,26 @@ public class GenerateMojo extends AbstractMojo { private JsonLibrary jsonLibrary; /** - * If true declared properties will be optional. + * Deprecated, use optionalProperties parameter. */ + @Deprecated @Parameter private boolean declarePropertiesAsOptional; + /** + * Specifies how properties are defined to be optional. + * Supported values are: + *
    + *
  • useSpecifiedAnnotations - annotations specified using optionalAnnotations parameter
  • + *
  • useLibraryDefinition - examples: @JsonProperty(required = false) when using jackson2 library + * or @XmlElement(required = false) when using jaxb library
  • + *
  • all - all properties are optional
  • + *
+ * Default value is useSpecifiedAnnotations. + */ + @Parameter + private OptionalProperties optionalProperties; + /** * If true declared properties will be readonly. */ @@ -404,11 +419,24 @@ public class GenerateMojo extends AbstractMojo { private boolean displaySerializerWarning; /** - * Turns off Jackson2 automatic module discovery. + * Deprecated, see jackson2ModuleDiscovery and jackson2Modules parameters. */ + @Deprecated @Parameter private boolean disableJackson2ModuleDiscovery; + /** + * Turns on Jackson2 automatic module discovery. + */ + @Parameter + private boolean jackson2ModuleDiscovery; + + /** + * Specifies Jackson2 modules to use. + */ + @Parameter + private List jackson2Modules; + /** * Turns on verbose output for debugging purposes. */ @@ -447,6 +475,7 @@ public void execute() { settings.setExcludeFilter(excludeClasses, excludeClassPatterns); settings.jsonLibrary = jsonLibrary; settings.declarePropertiesAsOptional = declarePropertiesAsOptional; + settings.optionalProperties = optionalProperties; settings.declarePropertiesAsReadOnly = declarePropertiesAsReadOnly; settings.removeTypeNamePrefix = removeTypeNamePrefix; settings.removeTypeNameSuffix = removeTypeNameSuffix; @@ -483,6 +512,8 @@ public void execute() { settings.setStringQuotes(stringQuotes); settings.displaySerializerWarning = displaySerializerWarning; settings.disableJackson2ModuleDiscovery = disableJackson2ModuleDiscovery; + settings.jackson2ModuleDiscovery = jackson2ModuleDiscovery; + settings.loadJackson2Modules(classLoader, jackson2Modules); settings.classLoader = classLoader; final File output = outputFile != null ? outputFile