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 4453dde42..4ffcca01a 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 @@ -90,6 +90,7 @@ public class Settings { private Predicate mapClassesAsClassesFilter = null; public boolean generateConstructors = false; public boolean disableTaggedUnions = false; + public boolean generateReadonlyAndWriteonlyJSDocTags = false; public boolean ignoreSwaggerAnnotations = false; public boolean generateJaxrsApplicationInterface = false; public boolean generateJaxrsApplicationClient = false; 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 537bf2a35..df2e615f1 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 @@ -47,6 +47,7 @@ import cz.habarta.typescript.generator.parser.MethodParameterModel; import cz.habarta.typescript.generator.parser.Model; import cz.habarta.typescript.generator.parser.PathTemplate; +import cz.habarta.typescript.generator.parser.PropertyAccess; import cz.habarta.typescript.generator.parser.PropertyModel; import cz.habarta.typescript.generator.parser.RestApplicationModel; import cz.habarta.typescript.generator.parser.RestMethodModel; @@ -215,7 +216,7 @@ private static TsModel applyExtensionTransformers(SymbolTable symbolTable, TsMod public TsType javaToTypeScript(Type type) { final BeanModel beanModel = new BeanModel(Object.class, Object.class, null, null, null, Collections.emptyList(), - Collections.singletonList(new PropertyModel("property", type, false, null, null, null, null)), null); + Collections.singletonList(new PropertyModel("property", type, false, null, null, null, null, null)), null); final Model model = new Model(Collections.singletonList(beanModel), Collections.emptyList(), null); final TsModel tsModel = javaToTypeScript(model); return tsModel.getBeans().get(0).getProperties().get(0).getTsType(); @@ -386,7 +387,18 @@ private TsPropertyModel processProperty(SymbolTable symbolTable, BeanModel bean, final TsType type = typeFromJava(symbolTable, property.getType(), property.getContext(), property.getName(), bean.getOrigin()); final TsType tsType = property.isOptional() ? type.optional() : type; final TsModifierFlags modifiers = TsModifierFlags.None.setReadonly(settings.declarePropertiesAsReadOnly); - return new TsPropertyModel(prefix + property.getName() + suffix, tsType, modifiers, /*ownProperty*/ false, property.getComments()); + final List comments = settings.generateReadonlyAndWriteonlyJSDocTags + ? Utils.concat(property.getComments(), getPropertyAccessComments(property.getAccess())) + : property.getComments(); + return new TsPropertyModel(prefix + property.getName() + suffix, tsType, modifiers, /*ownProperty*/ false, comments); + } + + private static List getPropertyAccessComments(PropertyAccess access) { + final String accessTag = + access == PropertyAccess.ReadOnly ? "@readonly" : + access == PropertyAccess.WriteOnly ? "@writeonly" : + null; + return accessTag != null ? Collections.singletonList(accessTag) : null; } private TsEnumModel processEnum(SymbolTable symbolTable, EnumModel enumModel) { diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/GsonParser.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/GsonParser.java index 4afba76db..259026a4c 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/GsonParser.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/GsonParser.java @@ -92,7 +92,7 @@ private BeanModel parseBean(SourceType> sourceClass) { if (serializedName != null) { name = serializedName.value(); } - properties.add(new PropertyModel(name, field.getGenericType(), false, field, null, null, null)); + properties.add(new PropertyModel(name, field.getGenericType(), false, null, field, null, null, null)); addBeanToQueue(new SourceType<>(field.getGenericType(), sourceClass.type, name)); } cls = cls.getSuperclass(); 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 b46b04f45..899c3f901 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 @@ -89,14 +89,14 @@ private BeanModel parseBean(SourceType> sourceClass) { continue; } final boolean optional = isPropertyOptional(propertyMember); - properties.add(processTypeAndCreateProperty(beanPropertyWriter.getName(), propertyType, null, optional, sourceClass.type, member, null, null)); + properties.add(processTypeAndCreateProperty(beanPropertyWriter.getName(), propertyType, null, optional, null, sourceClass.type, member, null, null)); } } final JsonTypeInfo jsonTypeInfo = sourceClass.type.getAnnotation(JsonTypeInfo.class); if (jsonTypeInfo != null && jsonTypeInfo.include() == JsonTypeInfo.As.PROPERTY) { if (!containsProperty(properties, jsonTypeInfo.property())) { - properties.add(new PropertyModel(jsonTypeInfo.property(), String.class, false, null, null, null, null)); + properties.add(new PropertyModel(jsonTypeInfo.property(), String.class, false, null, null, null, null, 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 cd91fdccc..a04b7f2cb 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 @@ -16,10 +16,11 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.Module; @@ -29,6 +30,10 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.cfg.MutableConfigOverride; +import com.fasterxml.jackson.databind.deser.BeanDeserializer; +import com.fasterxml.jackson.databind.deser.BeanDeserializerFactory; +import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext; +import com.fasterxml.jackson.databind.deser.impl.BeanPropertyMap; import com.fasterxml.jackson.databind.introspect.AnnotatedClass; import com.fasterxml.jackson.databind.introspect.AnnotatedClassResolver; import com.fasterxml.jackson.databind.jsontype.NamedType; @@ -48,6 +53,7 @@ import cz.habarta.typescript.generator.compiler.EnumKind; import cz.habarta.typescript.generator.compiler.EnumMemberModel; import cz.habarta.typescript.generator.type.JUnionType; +import cz.habarta.typescript.generator.util.Pair; import cz.habarta.typescript.generator.util.PropertyMember; import cz.habarta.typescript.generator.util.Utils; import java.lang.annotation.Annotation; @@ -59,6 +65,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -166,7 +173,7 @@ public TypeProcessor.Result processType(Type javaType, TypeProcessor.Context con final Jackson2TypeContext jackson2TypeContext = (Jackson2TypeContext) context.getTypeContext(); final Jackson2ConfigurationResolved config = jackson2TypeContext.parser.settings.jackson2Configuration; // JsonSerialize - final JsonSerialize jsonSerialize = jackson2TypeContext.beanPropertyWriter.getAnnotation(JsonSerialize.class); + final JsonSerialize jsonSerialize = jackson2TypeContext.beanProperty.getAnnotation(JsonSerialize.class); if (jsonSerialize != null && config != null && config.serializerTypeMappings != null) { @SuppressWarnings("unchecked") final Class> using = (Class>) @@ -177,7 +184,7 @@ public TypeProcessor.Result processType(Type javaType, TypeProcessor.Context con } } // JsonDeserialize - final JsonDeserialize jsonDeserialize = jackson2TypeContext.beanPropertyWriter.getAnnotation(JsonDeserialize.class); + final JsonDeserialize jsonDeserialize = jackson2TypeContext.beanProperty.getAnnotation(JsonDeserialize.class); if (jsonDeserialize != null && config != null && config.deserializerTypeMappings != null) { @SuppressWarnings("unchecked") final Class> using = (Class>) @@ -189,7 +196,7 @@ public TypeProcessor.Result processType(Type javaType, TypeProcessor.Context con } // disableObjectIdentityFeature if (!jackson2TypeContext.disableObjectIdentityFeature) { - final Type resultType = jackson2TypeContext.parser.processIdentity(javaType, jackson2TypeContext.beanPropertyWriter); + final Type resultType = jackson2TypeContext.parser.processIdentity(javaType, jackson2TypeContext.beanProperty); if (resultType != null) { return context.withTypeContext(null).processType(resultType); } @@ -218,12 +225,12 @@ public TypeProcessor.Result processType(Type javaType, TypeProcessor.Context con private static class Jackson2TypeContext { public final Jackson2Parser parser; - public final BeanPropertyWriter beanPropertyWriter; + public final BeanProperty beanProperty; public final boolean disableObjectIdentityFeature; - public Jackson2TypeContext(Jackson2Parser parser, BeanPropertyWriter beanPropertyWriter, boolean disableObjectIdentityFeature) { + public Jackson2TypeContext(Jackson2Parser parser, BeanProperty beanProperty, boolean disableObjectIdentityFeature) { this.parser = parser; - this.beanPropertyWriter = beanPropertyWriter; + this.beanProperty = beanProperty; this.disableObjectIdentityFeature = disableObjectIdentityFeature; } } @@ -241,32 +248,34 @@ protected DeclarationModel parseClass(SourceType> sourceClass) { private BeanModel parseBean(SourceType> sourceClass, List classComments) { final List properties = new ArrayList<>(); - final BeanHelper beanHelper = getBeanHelper(sourceClass.type); - if (beanHelper != null) { - for (final BeanPropertyWriter beanPropertyWriter : beanHelper.getProperties()) { - final Member member = beanPropertyWriter.getMember().getMember(); - final PropertyMember propertyMember = wrapMember(settings.getTypeParser(), member, beanPropertyWriter::getAnnotation, beanPropertyWriter.getName(), sourceClass.type); + final BeanHelpers beanHelpers = getBeanHelpers(sourceClass.type); + if (beanHelpers != null) { + for (final Pair pair : beanHelpers.getPropertiesAndAccess()) { + final BeanProperty beanProperty = pair.getValue1(); + final PropertyAccess access = pair.getValue2(); + final Member member = beanProperty.getMember().getMember(); + final PropertyMember propertyMember = wrapMember(settings.getTypeParser(), member, beanProperty::getAnnotation, beanProperty.getName(), sourceClass.type); Type propertyType = propertyMember.getType(); - final List propertyComments = getComments(beanPropertyWriter.getAnnotation(JsonPropertyDescription.class)); + final List propertyComments = getComments(beanProperty.getAnnotation(JsonPropertyDescription.class)); final Jackson2TypeContext jackson2TypeContext = new Jackson2TypeContext( this, - beanPropertyWriter, + beanProperty, settings.jackson2Configuration != null && settings.jackson2Configuration.disableObjectIdentityFeature); - if (!isAnnotatedPropertyIncluded(beanPropertyWriter::getAnnotation, sourceClass.type.getName() + "." + beanPropertyWriter.getName())) { + if (!isAnnotatedPropertyIncluded(beanProperty::getAnnotation, sourceClass.type.getName() + "." + beanProperty.getName())) { continue; } final boolean optional = settings.optionalProperties == OptionalProperties.useLibraryDefinition - ? !beanPropertyWriter.isRequired() + ? !beanProperty.isRequired() : isPropertyOptional(propertyMember); // @JsonUnwrapped PropertyModel.PullProperties pullProperties = null; - final JsonUnwrapped annotation = beanPropertyWriter.getAnnotation(JsonUnwrapped.class); + final JsonUnwrapped annotation = beanProperty.getAnnotation(JsonUnwrapped.class); if (annotation != null && annotation.enabled()) { pullProperties = new PropertyModel.PullProperties(annotation.prefix(), annotation.suffix()); } - properties.add(processTypeAndCreateProperty(beanPropertyWriter.getName(), propertyType, jackson2TypeContext, optional, sourceClass.type, member, pullProperties, propertyComments)); + properties.add(processTypeAndCreateProperty(beanProperty.getName(), propertyType, jackson2TypeContext, optional, access, sourceClass.type, member, pullProperties, propertyComments)); } } if (sourceClass.type.isEnum()) { @@ -322,21 +331,21 @@ private BeanModel parseBean(SourceType> sourceClass, List class } // @JsonIdentityInfo and @JsonIdentityReference - private Type processIdentity(Type propertyType, BeanPropertyWriter propertyWriter) { + private Type processIdentity(Type propertyType, BeanProperty beanProperty) { final Class clsT = Utils.getRawClassOrNull(propertyType); - final Class clsW = propertyWriter.getType().getRawClass(); + final Class clsW = beanProperty.getType().getRawClass(); final Class cls = clsT != null ? clsT : clsW; if (cls != null) { final JsonIdentityInfo identityInfoC = cls.getAnnotation(JsonIdentityInfo.class); - final JsonIdentityInfo identityInfoP = propertyWriter.getAnnotation(JsonIdentityInfo.class); + final JsonIdentityInfo identityInfoP = beanProperty.getAnnotation(JsonIdentityInfo.class); final JsonIdentityInfo identityInfo = identityInfoP != null ? identityInfoP : identityInfoC; if (identityInfo == null) { return null; } final JsonIdentityReference identityReferenceC = cls.getAnnotation(JsonIdentityReference.class); - final JsonIdentityReference identityReferenceP = propertyWriter.getAnnotation(JsonIdentityReference.class); + final JsonIdentityReference identityReferenceP = beanProperty.getAnnotation(JsonIdentityReference.class); final JsonIdentityReference identityReference = identityReferenceP != null ? identityReferenceP : identityReferenceC; final boolean alwaysAsId = identityReference != null && identityReference.alwaysAsId(); @@ -344,18 +353,18 @@ private Type processIdentity(Type propertyType, BeanPropertyWriter propertyWrite if (identityInfo.generator() == ObjectIdGenerators.None.class) { return null; } else if (identityInfo.generator() == ObjectIdGenerators.PropertyGenerator.class) { - final BeanHelper beanHelper = getBeanHelper(cls); - if (beanHelper == null) { + final BeanHelpers beanHelpers = getBeanHelpers(cls); + if (beanHelpers == null) { return null; } - final BeanPropertyWriter[] properties = beanHelper.getProperties(); - final Optional idProperty = Stream.of(properties) + final List properties = beanHelpers.getProperties(); + final Optional idPropertyOptional = properties.stream() .filter(p -> p.getName().equals(identityInfo.property())) .findFirst(); - if (idProperty.isPresent()) { - final BeanPropertyWriter idPropertyWriter = idProperty.get(); - final Member idMember = idPropertyWriter.getMember().getMember(); - final PropertyMember idPropertyMember = wrapMember(settings.getTypeParser(), idMember, idPropertyWriter::getAnnotation, idPropertyWriter.getName(), cls); + if (idPropertyOptional.isPresent()) { + final BeanProperty idProperty = idPropertyOptional.get(); + final Member idMember = idProperty.getMember().getMember(); + final PropertyMember idPropertyMember = wrapMember(settings.getTypeParser(), idMember, idProperty::getAnnotation, idProperty.getName(), cls); idType = idPropertyMember.getType(); } else { return null; @@ -471,42 +480,184 @@ private static T getAnnotationRecursive(Class cls, Cla return null; } - private BeanHelper getBeanHelper(Class beanClass) { + private BeanHelpers getBeanHelpers(Class beanClass) { if (beanClass == null) { return null; } if (beanClass == Enum.class) { return null; } + final JavaType javaType = objectMapper.constructType(beanClass); + final BeanSerializerHelper beanSerializerHelper = createBeanSerializerHelper(javaType); + if (beanSerializerHelper != null) { + final BeanDeserializerHelper beanDeserializerHelper = createBeanDeserializerHelper(javaType); + return new BeanHelpers(beanClass, beanSerializerHelper, beanDeserializerHelper); + } + return null; + } + + private BeanSerializerHelper createBeanSerializerHelper(JavaType javaType) { try { - final DefaultSerializerProvider.Impl serializerProvider1 = (DefaultSerializerProvider.Impl) objectMapper.getSerializerProvider(); - final DefaultSerializerProvider.Impl serializerProvider2 = serializerProvider1.createInstance(objectMapper.getSerializationConfig(), objectMapper.getSerializerFactory()); - final JavaType simpleType = objectMapper.constructType(beanClass); - final JsonSerializer jsonSerializer = BeanSerializerFactory.instance.createSerializer(serializerProvider2, simpleType); - if (jsonSerializer == null) { + final DefaultSerializerProvider.Impl serializerProvider = new DefaultSerializerProvider.Impl() + .createInstance(objectMapper.getSerializationConfig(), objectMapper.getSerializerFactory()); + final JsonSerializer jsonSerializer = BeanSerializerFactory.instance.createSerializer(serializerProvider, javaType); + if (jsonSerializer != null && jsonSerializer instanceof BeanSerializer) { + return new BeanSerializerHelper((BeanSerializer) jsonSerializer); + } else { return null; } - if (jsonSerializer instanceof BeanSerializer) { - return new BeanHelper((BeanSerializer) jsonSerializer); + } catch (Exception e) { + return null; + } + } + + private BeanDeserializerHelper createBeanDeserializerHelper(JavaType javaType) { + try { + final DeserializationContext deserializationContext = new DefaultDeserializationContext.Impl(objectMapper.getDeserializationContext().getFactory()) + .createInstance(objectMapper.getDeserializationConfig(), null, null); + final BeanDescription beanDescription = deserializationContext.getConfig().introspect(javaType); + final JsonDeserializer jsonDeserializer = BeanDeserializerFactory.instance.createBeanDeserializer(deserializationContext, javaType, beanDescription); + if (jsonDeserializer != null && jsonDeserializer instanceof BeanDeserializer) { + return new BeanDeserializerHelper((BeanDeserializer) jsonDeserializer); } else { return null; } - } catch (JsonMappingException e) { - throw new RuntimeException(e); + } catch (Exception e) { + return null; } } - private static class BeanHelper extends BeanSerializer { + // for tests + protected List getBeanProperties(Class beanClass) { + return getBeanHelpers(beanClass).getProperties(); + } + + private static class BeanHelpers { + public final Class beanClass; + public final BeanSerializerHelper serializer; + public final BeanDeserializerHelper deserializer; + + public BeanHelpers(Class beanClass, BeanSerializerHelper serializer, BeanDeserializerHelper deserializer) { + this.beanClass = beanClass; + this.serializer = serializer; + this.deserializer = deserializer; + } + + public List getProperties() { + return getPropertiesAndAccess().stream() + .map(Pair::getValue1) + .collect(Collectors.toList()); + } + + public List> getPropertiesAndAccess() { + return getPropertiesPairs().stream() + .map(pair -> pair.getValue1() != null + ? Pair.of(pair.getValue1(), pair.getValue2() != null ? PropertyAccess.ReadWrite : PropertyAccess.ReadOnly) + : Pair.of(pair.getValue2(), PropertyAccess.WriteOnly) + ) + .collect(Collectors.toList()); + } + + private List> getPropertiesPairs() { + final List serializableProperties = getSerializableProperties(); + final List deserializableProperties = getDeserializableProperties(); + final List> properties = Stream + .concat( + serializableProperties.stream() + .map(property -> Pair.of(property, getBeanProperty(deserializableProperties, property.getName()))), + deserializableProperties.stream() + .filter(property -> getBeanProperty(serializableProperties, property.getName()) == null) + .map(property -> Pair.of((BeanProperty) null, property)) + ) + .collect(Collectors.toCollection(ArrayList::new)); + + // sort + final Comparator> bySerializationOrder = (pair1, pair2) -> + pair1.getValue1() != null && pair2.getValue1() != null + ? Integer.compare( + serializableProperties.indexOf(pair1.getValue1()), + serializableProperties.indexOf(pair2.getValue1())) + : 0; + final Comparator> byIndex = Comparator.comparing( + pair -> getIndex(pair), + Comparator.nullsLast(Comparator.naturalOrder())); + final List fields = Utils.getAllFields(beanClass); + final Comparator> byFieldIndex = Comparator.comparing( + pair -> getFieldIndex(fields, pair), + Comparator.nullsLast(Comparator.naturalOrder())); + properties.sort(bySerializationOrder + .thenComparing(byIndex) + .thenComparing(byFieldIndex)); + return properties; + } + + private static BeanProperty getBeanProperty(List properties, String name) { + return properties.stream() + .filter(dp -> Objects.equals(dp.getName(), name)) + .findFirst() + .orElse(null); + } + + private static Integer getIndex(Pair pair) { + final Integer index1 = getIndex(pair.getValue1()); + return index1 != null ? index1 : getIndex(pair.getValue2()); + } + + private static Integer getIndex(BeanProperty property) { + if (property == null) { + return null; + } + return property.getMetadata().getIndex(); + } + + private static Integer getFieldIndex(List fields, Pair pair) { + final Integer fieldIndex1 = getFieldIndex(fields, pair.getValue1()); + return fieldIndex1 != null ? fieldIndex1 : getFieldIndex(fields, pair.getValue2()); + } + + private static Integer getFieldIndex(List fields, BeanProperty property) { + if (property == null) { + return null; + } + final int index = fields.indexOf(property.getMember().getMember()); + return index != -1 ? index : null; + } + + private List getSerializableProperties() { + return serializer != null + ? Arrays.asList(serializer.getProps()) + : Collections.emptyList(); + } + + private List getDeserializableProperties() { + return deserializer != null + ? Arrays.asList(deserializer.getBeanProperties().getPropertiesInInsertionOrder()) + : Collections.emptyList(); + } + } + + private static class BeanSerializerHelper extends BeanSerializer { private static final long serialVersionUID = 1; - public BeanHelper(BeanSerializer src) { + public BeanSerializerHelper(BeanSerializer src) { super(src); } - public BeanPropertyWriter[] getProperties() { + public BeanPropertyWriter[] getProps() { return _props; } + } + + private static class BeanDeserializerHelper extends BeanDeserializer { + private static final long serialVersionUID = 1; + + public BeanDeserializerHelper(BeanDeserializer src) { + super(src); + } + public BeanPropertyMap getBeanProperties() { + return _beanProperties; + } } private DeclarationModel parseEnumOrObjectEnum(SourceType> sourceClass, List classComments) { 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 f5206f3d4..baa8bdc2a 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 @@ -255,7 +255,7 @@ private static BeanModel getQueryParameters(Class paramBean) { for (Field field : fields) { final QueryParam annotation = field.getAnnotation(QueryParam.class); if (annotation != null) { - properties.add(new PropertyModel(annotation.value(), field.getGenericType(), /*optional*/true, field, null, null, null)); + properties.add(new PropertyModel(annotation.value(), field.getGenericType(), /*optional*/true, null, field, null, null, null)); } } try { @@ -265,7 +265,7 @@ private static BeanModel getQueryParameters(Class paramBean) { if (writeMethod != null) { final QueryParam annotation = writeMethod.getAnnotation(QueryParam.class); if (annotation != null) { - properties.add(new PropertyModel(annotation.value(), propertyDescriptor.getPropertyType(), /*optional*/true, writeMethod, null, null, null)); + properties.add(new PropertyModel(annotation.value(), propertyDescriptor.getPropertyType(), /*optional*/true, null, writeMethod, null, null, null)); } } } diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JsonbParser.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JsonbParser.java index 742b19c3f..99336e11e 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JsonbParser.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/JsonbParser.java @@ -174,7 +174,7 @@ private Stream visitConstructor(final Constructor constructor) (isOptional(type) || OptionalInt.class == type || OptionalLong.class == type || OptionalDouble.class == type || property.map(JsonbProperty::nillable).orElse(false)), - constructor.getDeclaringClass(), new ParameterMember(parameter), + null, constructor.getDeclaringClass(), new ParameterMember(parameter), null, null); }); } @@ -197,7 +197,7 @@ private List visitClass(final Class clazz) { settings.getTypeParser().getMethodReturnType(Method.class.cast(member)), null, settings.optionalProperties == OptionalProperties.useLibraryDefinition || JsonbParser.this.isPropertyOptional(propertyMember), - clazz, member, null, null); + null, clazz, member, null, null); }) .sorted(Comparator.comparing(PropertyModel::getName)) .collect(Collectors.toList()); 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 bc5cd918b..fc3286898 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 @@ -130,10 +130,15 @@ protected static PropertyMember wrapMember(TypeParser typeParser, Member propert } if (propertyMember instanceof Method) { final Method method = (Method) propertyMember; - return new PropertyMember(method, typeParser.getMethodReturnType(method), method.getAnnotatedReturnType(), annotationGetter); + switch (method.getParameterCount()) { + case 0: + return new PropertyMember(method, typeParser.getMethodReturnType(method), method.getAnnotatedReturnType(), annotationGetter); + case 1: + return new PropertyMember(method, typeParser.getMethodParameterTypes(method).get(0), method.getAnnotatedParameterTypes()[0], annotationGetter); + } } throw new RuntimeException(String.format( - "Unexpected member type '%s' in property '%s' in class '%s'", + "Unexpected member '%s' in property '%s' in class '%s'", propertyMember != null ? propertyMember.getClass().getName() : null, propertyName, sourceClass.getName())); @@ -185,13 +190,13 @@ protected void addBeanToQueue(SourceType sourceType) { typeQueue.add(sourceType); } - protected PropertyModel processTypeAndCreateProperty(String name, Type type, Object typeContext, boolean optional, Class usedInClass, Member originalMember, PropertyModel.PullProperties pullProperties, List comments) { + protected PropertyModel processTypeAndCreateProperty(String name, Type type, Object typeContext, boolean optional, PropertyAccess access, Class usedInClass, Member originalMember, PropertyModel.PullProperties pullProperties, List comments) { final Type resolvedType = GenericsResolver.resolveType(usedInClass, type, originalMember.getDeclaringClass()); final List> classes = commonTypeProcessor.discoverClassesUsedInType(resolvedType, typeContext, settings); for (Class cls : classes) { typeQueue.add(new SourceType<>(cls, usedInClass, name)); } - return new PropertyModel(name, resolvedType, optional, originalMember, pullProperties, typeContext, comments); + return new PropertyModel(name, resolvedType, optional, access, originalMember, pullProperties, typeContext, comments); } public static boolean containsProperty(List properties, String propertyName) { diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/PropertyAccess.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/PropertyAccess.java new file mode 100644 index 000000000..6fbedf545 --- /dev/null +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/PropertyAccess.java @@ -0,0 +1,11 @@ + +package cz.habarta.typescript.generator.parser; + + +public enum PropertyAccess { + + ReadOnly, + WriteOnly, + ReadWrite, + +} diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/PropertyModel.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/PropertyModel.java index ab1f597dd..f805b2861 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/PropertyModel.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/parser/PropertyModel.java @@ -12,6 +12,7 @@ public class PropertyModel { private final String name; private final Type type; private final boolean optional; + private final PropertyAccess access; private final Member originalMember; private final PullProperties pullProperties; private final Object context; @@ -27,10 +28,11 @@ public PullProperties(String prefix, String suffix) { } } - public PropertyModel(String name, Type type, boolean optional, Member originalMember, PullProperties pullProperties, Object context, List comments) { + public PropertyModel(String name, Type type, boolean optional, PropertyAccess access, Member originalMember, PullProperties pullProperties, Object context, List comments) { this.name = Objects.requireNonNull(name); this.type = Objects.requireNonNull(type); this.optional = optional; + this.access = access; this.originalMember = originalMember; this.pullProperties = pullProperties; this.context = context; @@ -49,12 +51,16 @@ public boolean isOptional() { return optional; } + public PropertyAccess getAccess() { + return access; + } + public Member getOriginalMember() { return originalMember; } public PropertyModel originalMember(Member originalMember) { - return new PropertyModel(name, type, optional, originalMember, pullProperties, context, comments); + return new PropertyModel(name, type, optional, access, originalMember, pullProperties, context, comments); } public PullProperties getPullProperties() { @@ -70,15 +76,15 @@ public List getComments() { } public PropertyModel withComments(List comments) { - return new PropertyModel(name, type, optional, originalMember, pullProperties, context, comments); + return new PropertyModel(name, type, optional, access, originalMember, pullProperties, context, comments); } public PropertyModel withType(Type type) { - return new PropertyModel(name, type, optional, originalMember, pullProperties, context, comments); + return new PropertyModel(name, type, optional, access, originalMember, pullProperties, context, comments); } public PropertyModel withOptional(boolean optional) { - return new PropertyModel(name, type, optional, originalMember, pullProperties, context, comments); + return new PropertyModel(name, type, optional, access, originalMember, pullProperties, context, comments); } @Override 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 cf9bfed7b..a92b5426f 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 @@ -14,7 +14,6 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; @@ -32,7 +31,6 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; diff --git a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ReadOnlyWriteOnlyTest.java b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ReadOnlyWriteOnlyTest.java new file mode 100644 index 000000000..bb54a4674 --- /dev/null +++ b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ReadOnlyWriteOnlyTest.java @@ -0,0 +1,99 @@ + +package cz.habarta.typescript.generator; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import org.junit.Assert; +import org.junit.Test; + + +public class ReadOnlyWriteOnlyTest { + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ReadOnlyWriteOnlyUser { + + public String name; + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String id1; + + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + public String password1; + + private String _id2; // Jackson would use `id2` field as writable property + + public String getId2() { + return _id2; + } + + private String password2; + + public void setPassword2(String password2) { + this.password2 = password2; + } + + @Override + public String toString() { + return new Gson().toJson(this); + } + + } + + @Test + public void testJacksonSerialization() throws JsonProcessingException { + final ReadOnlyWriteOnlyUser user = new ReadOnlyWriteOnlyUser(); + user.name = "name"; + user.id1 = "id1"; + user._id2 = "id2"; + user.password1 = "password1"; + user.password2 = "password2"; + final String json = new ObjectMapper().writeValueAsString(user); + Assert.assertTrue(json.contains("id1")); + Assert.assertTrue(json.contains("id2")); + Assert.assertTrue(!json.contains("password1")); + Assert.assertTrue(!json.contains("password2")); + } + + @Test + public void testJacksonDeserialization() throws JsonProcessingException { + final String json = "{'name':'name','id1':'id1','id2':'id2','password1':'password1','password2':'password2'}" + .replace("'", "\""); + final ReadOnlyWriteOnlyUser user = new ObjectMapper().readValue(json, ReadOnlyWriteOnlyUser.class); + Assert.assertNull(user.id1); + Assert.assertNull(user._id2); + Assert.assertNotNull(user.password1); + Assert.assertNotNull(user.password2); + } + + @Test + public void test() { + final Settings settings = TestUtils.settings(); + settings.generateReadonlyAndWriteonlyJSDocTags = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(ReadOnlyWriteOnlyUser.class)); + final String expected = "\n" + + "interface ReadOnlyWriteOnlyUser {\n" + + " name: string;\n" + + " /**\n" + + " * @readonly\n" + + " */\n" + + " id1: string;\n" + + " /**\n" + + " * @writeonly\n" + + " */\n" + + " password1: string;\n" + + " /**\n" + + " * @readonly\n" + + " */\n" + + " id2: string;\n" + + " /**\n" + + " * @writeonly\n" + + " */\n" + + " password2: string;\n" + + "}\n"; + Assert.assertEquals(expected, output); + } + +} diff --git a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ext/OnePossiblePropertyValueAssigningExtensionTest.java b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ext/OnePossiblePropertyValueAssigningExtensionTest.java index d7f747b2f..9b8974ddc 100644 --- a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ext/OnePossiblePropertyValueAssigningExtensionTest.java +++ b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/ext/OnePossiblePropertyValueAssigningExtensionTest.java @@ -4,8 +4,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import cz.habarta.typescript.generator.ClassMapping; import cz.habarta.typescript.generator.Input; -import cz.habarta.typescript.generator.JsonLibrary; 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.TypeScriptOutputKind; @@ -65,15 +65,12 @@ public void testGeneration() { } private static Settings createBaseSettings(OnePossiblePropertyValueAssigningExtension extension) { - Settings settings = new Settings(); + Settings settings = TestUtils.settings(); settings.sortDeclarations = true; settings.extensions.add(extension); - settings.jsonLibrary = JsonLibrary.jackson2; settings.outputFileType = TypeScriptFileType.implementationFile; settings.outputKind = TypeScriptOutputKind.module; settings.mapClasses = ClassMapping.asClasses; - settings.noFileComment = true; - settings.noEslintDisable = true; return settings; } diff --git a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/parser/Jackson2ParserPropertiesTest.java b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/parser/Jackson2ParserPropertiesTest.java new file mode 100644 index 000000000..243e23894 --- /dev/null +++ b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/parser/Jackson2ParserPropertiesTest.java @@ -0,0 +1,97 @@ + +package cz.habarta.typescript.generator.parser; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.BeanProperty; +import cz.habarta.typescript.generator.Settings; +import cz.habarta.typescript.generator.TestUtils; +import cz.habarta.typescript.generator.TypeScriptGenerator; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.Assert; +import org.junit.Test; + + +public class Jackson2ParserPropertiesTest { + + @JsonPropertyOrder({"password1", "id2"}) + public static class UserOrdered { + public String name; + public String id1; + public String id2; + public String password1; + public String password2; + } + + @JsonPropertyOrder(alphabetic = true) + public static class UserAlphabetic { + public String name; + public String id1; + public String id2; + public String password1; + public String password2; + } + + public static class UserIndexed { + @JsonProperty(index = 5) public String name; + @JsonProperty(index = 4) public String id1; + @JsonProperty(index = 3) public String id2; + @JsonProperty(index = 2) public String password1; + @JsonProperty(index = 1) public String password2; + } + + public static class User1 { + @JsonProperty(access = JsonProperty.Access.READ_WRITE) public String name; + @JsonProperty(access = JsonProperty.Access.READ_WRITE) public String id1; + @JsonProperty(access = JsonProperty.Access.READ_WRITE) public String id2; + @JsonProperty(access = JsonProperty.Access.READ_WRITE) public String password1; + @JsonProperty(access = JsonProperty.Access.READ_WRITE) public String password2; + } + + public static class User2 { + @JsonProperty(access = JsonProperty.Access.READ_WRITE) public String name; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String id1; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String id2; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) public String password1; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) public String password2; + } + + public static class User3 { + @JsonProperty(access = JsonProperty.Access.READ_WRITE) public String name; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) public String password1; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) public String password2; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String id1; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String id2; + } + + public static class User4 { + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) public String password1; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String id1; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) public String password2; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String id2; + @JsonProperty(access = JsonProperty.Access.READ_WRITE) public String name; + } + + @Test + public void testPropertyOrder() { + Assert.assertEquals(Arrays.asList("password1", "id2", "name", "id1", "password2"), getProperties(UserOrdered.class)); + Assert.assertEquals(Arrays.asList("id1", "id2", "name", "password1", "password2"), getProperties(UserAlphabetic.class)); + Assert.assertEquals(Arrays.asList("password2", "password1", "id2", "id1", "name"), getProperties(UserIndexed.class)); + Assert.assertEquals(Arrays.asList("name", "id1", "id2", "password1", "password2"), getProperties(User1.class)); + Assert.assertEquals(Arrays.asList("name", "id1", "id2", "password1", "password2"), getProperties(User2.class)); + Assert.assertEquals(Arrays.asList("name", "password1", "password2", "id1", "id2"), getProperties(User3.class)); + Assert.assertEquals(Arrays.asList("password1", "id1", "password2", "id2", "name"), getProperties(User4.class)); + } + + private List getProperties(Class beanClass) { + final Settings settings = TestUtils.settings(); + final TypeScriptGenerator typeScriptGenerator = new TypeScriptGenerator(settings); + final Jackson2Parser jackson2Parser = (Jackson2Parser) typeScriptGenerator.getModelParser(); + final List properties = jackson2Parser.getBeanProperties(beanClass); + final List names = properties.stream().map(BeanProperty::getName).collect(Collectors.toList()); + return names; + } + +} diff --git a/typescript-generator-core/src/test/resources/ext/OnePossiblePropertyValueAssigningExtensionTest-all.ts b/typescript-generator-core/src/test/resources/ext/OnePossiblePropertyValueAssigningExtensionTest-all.ts index 51ccae325..5675eb942 100644 --- a/typescript-generator-core/src/test/resources/ext/OnePossiblePropertyValueAssigningExtensionTest-all.ts +++ b/typescript-generator-core/src/test/resources/ext/OnePossiblePropertyValueAssigningExtensionTest-all.ts @@ -1,4 +1,3 @@ -/* tslint:disable */ export class BaseClass { discriminator: "OnePossiblePropertyValueAssigningExtensionTest$SubClass" | "OnePossiblePropertyValueAssigningExtensionTest$OtherSubClass"; 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 eb6d27260..c74b0d3fd 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 @@ -84,6 +84,7 @@ public class GenerateTask extends DefaultTask { public List mapClassesAsClassesPatterns; public boolean generateConstructors; public boolean disableTaggedUnions; + public boolean generateReadonlyAndWriteonlyJSDocTags; public boolean ignoreSwaggerAnnotations; public boolean generateJaxrsApplicationInterface; public boolean generateJaxrsApplicationClient; @@ -165,6 +166,7 @@ private Settings createSettings(URLClassLoader classLoader) { settings.mapClassesAsClassesPatterns = mapClassesAsClassesPatterns; settings.generateConstructors = generateConstructors; settings.disableTaggedUnions = disableTaggedUnions; + settings.generateReadonlyAndWriteonlyJSDocTags = generateReadonlyAndWriteonlyJSDocTags; settings.ignoreSwaggerAnnotations = ignoreSwaggerAnnotations; settings.generateJaxrsApplicationInterface = generateJaxrsApplicationInterface; settings.generateJaxrsApplicationClient = generateJaxrsApplicationClient; 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 75f51d97b..f33065b28 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 @@ -474,6 +474,12 @@ public class GenerateMojo extends AbstractMojo { @Parameter private boolean disableTaggedUnions; + /** + * If true JSDoc tags @readonly and @writeonly will be generated on properties with read-only or write-only access. + */ + @Parameter + private boolean generateReadonlyAndWriteonlyJSDocTags; + /** * If true Swagger annotations will not be used. */ @@ -845,6 +851,7 @@ private Settings createSettings(URLClassLoader classLoader) { settings.mapClassesAsClassesPatterns = mapClassesAsClassesPatterns; settings.generateConstructors = generateConstructors; settings.disableTaggedUnions = disableTaggedUnions; + settings.generateReadonlyAndWriteonlyJSDocTags = generateReadonlyAndWriteonlyJSDocTags; settings.ignoreSwaggerAnnotations = ignoreSwaggerAnnotations; settings.generateJaxrsApplicationInterface = generateJaxrsApplicationInterface; settings.generateJaxrsApplicationClient = generateJaxrsApplicationClient;