com.regnosys.rosetta.common.serialisation.xml.RosettaXMLAnnotationIntrospector Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rosetta-common Show documentation
Show all versions of rosetta-common Show documentation
Rune Common is a java library that is utilised by Rosetta Code Generators and models expressed in the Rosetta DSL.
The newest version!
package com.regnosys.rosetta.common.serialisation.xml;
/*-
* ==============
* Rune Common
* ==============
* Copyright (C) 2018 - 2024 REGnosys
* ==============
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ==============
*/
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyMetadata;
import com.fasterxml.jackson.databind.PropertyName;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.*;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.util.NameTransformer;
import com.fasterxml.jackson.databind.util.SimpleBeanPropertyDefinition;
import com.fasterxml.jackson.dataformat.xml.JacksonXmlAnnotationIntrospector;
import com.fasterxml.jackson.dataformat.xml.util.TypeUtil;
import com.google.common.collect.Streams;
import com.regnosys.rosetta.common.serialisation.ConstantAttributePropertyWriter;
import com.regnosys.rosetta.common.serialisation.mixin.EnumAsStringBuilderIntrospector;
import com.regnosys.rosetta.common.serialisation.mixin.RosettaEnumBuilderIntrospector;
import com.rosetta.model.lib.ModelSymbolId;
import com.rosetta.model.lib.annotations.RosettaAttribute;
import com.rosetta.model.lib.annotations.RosettaDataType;
import com.rosetta.model.lib.annotations.RosettaEnum;
import com.rosetta.model.lib.annotations.RosettaEnumValue;
import com.rosetta.util.DottedPath;
import com.rosetta.util.serialisation.AttributeXMLConfiguration;
import com.rosetta.util.serialisation.AttributeXMLRepresentation;
import com.rosetta.util.serialisation.RosettaXMLConfiguration;
import com.rosetta.util.serialisation.TypeXMLConfiguration;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class RosettaXMLAnnotationIntrospector extends JacksonXmlAnnotationIntrospector {
private static final long serialVersionUID = 1L;
// Our generated code uses 'build' for the build method, 'set' as setter prefix
private static final JsonPOJOBuilder.Value ROSETTA_BUILDER_CONFIG = new JsonPOJOBuilder.Value("build", "set");
private final RosettaXMLConfiguration rosettaXMLConfiguration;
private final RosettaEnumBuilderIntrospector rosettaEnumBuilderIntrospector;
private final EnumAsStringBuilderIntrospector enumAsStringBuilderIntrospector;
// Note: this is a hack! In some occasions, the methods below
// do not have access to the `MapperConfig>`, but they need it,
// so having access to the object mapper makes sure we can always
// access it.
// Related to https://github.com/FasterXML/jackson-databind/issues/4141.
private final ObjectMapper mapper;
public RosettaXMLAnnotationIntrospector(ObjectMapper mapper, final RosettaXMLConfiguration rosettaXMLConfiguration, final boolean supportNativeEnumValue) {
this(mapper, rosettaXMLConfiguration, new RosettaEnumBuilderIntrospector(supportNativeEnumValue), new EnumAsStringBuilderIntrospector());
}
public RosettaXMLAnnotationIntrospector(ObjectMapper mapper, RosettaXMLConfiguration rosettaXMLConfiguration, RosettaEnumBuilderIntrospector rosettaEnumBuilderIntrospector, final EnumAsStringBuilderIntrospector enumAsStringBuilderIntrospector) {
this.mapper = mapper;
this.rosettaXMLConfiguration = rosettaXMLConfiguration;
this.rosettaEnumBuilderIntrospector = rosettaEnumBuilderIntrospector;
this.enumAsStringBuilderIntrospector = enumAsStringBuilderIntrospector;
}
public SubstitutionMap findSubstitutionMap(MapperConfig> config, AnnotatedMember member, ClassLoader classLoader) {
AnnotatedClass ac = getAnnotatedClassOrContent(config, member);
RosettaDataType ann = ac.getAnnotation(RosettaDataType.class);
if (ann != null) {
ModelSymbolId id = createModelSymbolId(ac, ann.value());
List substitutions = rosettaXMLConfiguration.getSubstitutionsForType(id);
if (substitutions.isEmpty()) {
return null;
}
return new SubstitutionMap(
Streams.concat(substitutions.stream(), Stream.of(id))
.collect(Collectors.toMap(
s -> {
try {
return config.constructType(classLoader.loadClass(s.toString()));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
},
this::getElementName
))
);
}
return null;
}
private String getElementName(ModelSymbolId type) {
return rosettaXMLConfiguration.getConfigurationForType(type).flatMap(TypeXMLConfiguration::getXmlElementName).orElse(type.getName());
}
@Override
public NameTransformer findUnwrappingNameTransformer(AnnotatedMember member) {
return getAttributeXMLConfiguration(mapper.getDeserializationConfig(), member)
.flatMap(AttributeXMLConfiguration::getXmlRepresentation)
.filter(attributeXMLRepresentation -> attributeXMLRepresentation == AttributeXMLRepresentation.VIRTUAL)
.map(repr -> NameTransformer.NOP)
.orElseGet(() -> super.findUnwrappingNameTransformer(member));
}
@Override
public Class> findPOJOBuilder(AnnotatedClass ac) {
if (ac.hasAnnotation(RosettaDataType.class)) {
return ac.getAnnotation(RosettaDataType.class).builder();
}
return super.findPOJOBuilder(ac);
}
@Override
public JsonPOJOBuilder.Value findPOJOBuilderConfig(AnnotatedClass ac) {
if (ac.hasAnnotation(RosettaDataType.class)) {
return ROSETTA_BUILDER_CONFIG;
}
return super.findPOJOBuilderConfig(ac);
}
@Override
public PropertyName findRootName(AnnotatedClass ac) {
// If the element name is specified in the XML configuration, use that.
// Otherwise, if the RosettaDataType annotation is present, use the name from that.
// Otherwise, use the default.
return getTypeXMLConfigurations(mapper.getSerializationConfig(), ac).stream()
.filter(t -> t.getXmlElementName().isPresent())
.map(t -> t.getXmlElementName().get())
.findFirst()
.map(PropertyName::construct)
.orElseGet(() ->
Optional.ofNullable(super.findRootName(ac))
.orElseGet(() -> Optional.ofNullable(ac.getAnnotation(RosettaDataType.class))
.map(rosettaDataTypeAnn -> PropertyName.construct(rosettaDataTypeAnn.value()))
.orElse(null))
);
}
@Override
protected PropertyName _findXmlName(Annotated a) {
if (a.getRawType().equals(List.class) && getAttributeXMLConfiguration(mapper.getSerializationConfig(), a).flatMap(AttributeXMLConfiguration::getXmlRepresentation).map(repr -> repr == AttributeXMLRepresentation.VIRTUAL).orElse(false)) {
return PropertyName.NO_NAME;
}
if (this.shouldUseDefaultPropertyName(mapper.getSerializationConfig(), a)) {
// This is an edge case to conform to the same behaviour as the @JacksonXmlText annotation
// in case where the attribute should be rendered as an XML value.
return PropertyName.USE_DEFAULT;
}
// If the XML name is specified in the XML configuration, use that.
return this.getAttributeXMLConfiguration(mapper.getSerializationConfig(), a)
.flatMap(AttributeXMLConfiguration::getXmlName)
.map(PropertyName::construct)
.orElseGet(
() -> Optional.ofNullable(a.getAnnotation(RosettaAttribute.class))
.map(rosettaAttrAnn -> PropertyName.construct(rosettaAttrAnn.value()))
.orElseGet(() -> super._findXmlName(a))
);
}
private boolean shouldUseDefaultPropertyName(MapperConfig> config, Annotated a) {
return getAttributeXMLConfiguration(config, a)
.flatMap(AttributeXMLConfiguration::getXmlRepresentation)
.map(attributeXMLRepresentation -> attributeXMLRepresentation == AttributeXMLRepresentation.VALUE)
.orElse(false);
}
@Override
public void findAndAddVirtualProperties(MapperConfig> config, AnnotatedClass ac, List properties) {
getTypeXMLConfigurations(config, ac)
.forEach(typeXMLConfiguration -> {
typeXMLConfiguration.getXmlAttributes().ifPresent(xmlAttributes -> {
// For each XML attribute in the configuration, add a virtual XML attribute.
for (String xmlAttributeName : xmlAttributes.keySet()) {
String xmlAttributeValue = xmlAttributes.get(xmlAttributeName);
JavaType propType = config.constructType(String.class);
BeanPropertyWriter bpw = constructVirtualXMLAttribute(xmlAttributeName, xmlAttributeValue, config, ac, propType);
properties.add(bpw);
}
});
});
super.findAndAddVirtualProperties(config, ac, properties);
}
private BeanPropertyWriter constructVirtualXMLAttribute(final String xmlAttributeName, final String xmlAttributeValue, MapperConfig> config, AnnotatedClass ac, JavaType type) {
PropertyName propertyName = PropertyName.construct(xmlAttributeName);
AnnotatedMember member = new VirtualXMLAttribute(ac.getRawType(), xmlAttributeName, type);
SimpleBeanPropertyDefinition xmlPropertyDefinition = SimpleBeanPropertyDefinition.construct(config, member, propertyName, PropertyMetadata.STD_REQUIRED, JsonInclude.Include.NON_NULL);
return new ConstantAttributePropertyWriter(xmlAttributeName, xmlPropertyDefinition, ac.getAnnotations(), type, xmlAttributeValue);
}
@Override
public Boolean isOutputAsAttribute(MapperConfig> config, Annotated ann) {
if (ann instanceof VirtualXMLAttribute) {
// Edge case: manually constructed `VirtualXMLAttribute` instances should be rendered as attributes.
return true;
}
// If the XML representation for this member equals ATTRIBUTE, render it as an attribute.
return getAttributeXMLConfiguration(config, ann)
.flatMap(AttributeXMLConfiguration::getXmlRepresentation)
.map(attributeXMLRepresentation -> attributeXMLRepresentation == AttributeXMLRepresentation.ATTRIBUTE)
.orElseGet(() -> super.isOutputAsAttribute(config, ann));
}
@Override
public Boolean isOutputAsText(MapperConfig> config, Annotated ann) {
// If the XML representation for this member equals VALUE, render it as a value.
return getAttributeXMLConfiguration(config, ann)
.flatMap(AttributeXMLConfiguration::getXmlRepresentation)
.map(attributeXMLRepresentation -> attributeXMLRepresentation == AttributeXMLRepresentation.VALUE)
.orElseGet(() -> super.isOutputAsText(config, ann));
}
@Override
protected boolean _isIgnorable(Annotated a) {
boolean isIgnorable= super._isIgnorable(a);
if (isIgnorable) {
return true;
}
// Additionally, ignore any members that do not have the RosettaAttribute annotation
// except for constructors, which are necessary for deserialisation.
return !(a.hasAnnotation(RosettaAttribute.class) || a instanceof AnnotatedConstructor);
}
@Override
public JsonIgnoreProperties.Value findPropertyIgnoralByName(MapperConfig> config, Annotated a) {
// For the root element, ignore the xsi:schemaLocation attribute.
JsonIgnoreProperties.Value ignoreProps = super.findPropertyIgnoralByName(config, a);
return Optional.of(a)
.filter(ann -> ann instanceof AnnotatedClass)
.map(ann -> (AnnotatedClass) ann)
// TODO: substitution groups should not have this
.flatMap(ac -> this.getTypeXMLConfigurations(config, ac).stream().filter(t -> t.getXmlElementName().isPresent()).map(t -> t.getXmlElementName().get()).findFirst())
.map(rootElementName -> {
Set ignoredNames = new HashSet<>(ignoreProps.getIgnored());
return JsonIgnoreProperties.Value.construct(
ignoredNames,
ignoreProps.getIgnoreUnknown(),
ignoreProps.getAllowGetters(),
ignoreProps.getAllowSetters(),
ignoreProps.getMerge());
}).orElse(ignoreProps);
}
@Override
public PropertyName findWrapperName(Annotated ann) {
if (ann.hasAnnotation(RosettaAttribute.class) && hasCollectionType(ann)) {
// Disable wrapping of lists.
return PropertyName.NO_NAME;
}
return super.findWrapperName(ann);
}
private boolean hasCollectionType(Annotated ann) {
if (ann instanceof AnnotatedMethod) {
AnnotatedMethod method = (AnnotatedMethod) ann;
JavaType attributeType;
if (method.getParameterCount() == 1) {
attributeType = method.getParameterType(0);
} else {
attributeType = method.getType();
}
return TypeUtil.isIndexedType(attributeType);
}
return false;
}
@Override
public String[] findEnumValues(MapperConfig> config, AnnotatedClass enumType,
Enum>[] enumValues, String[] names) {
if (rosettaEnumBuilderIntrospector.isApplicable(enumType)) {
rosettaEnumBuilderIntrospector.findEnumValues(enumType, enumValues, names);
} else {
enumAsStringBuilderIntrospector.findEnumValues(enumType, enumValues, names);
}
getEnumXMLConfigurations(config, enumType).flatMap(TypeXMLConfiguration::getEnumValues).ifPresent(enumMap -> {
for (int i = 0; i < enumValues.length; i++) {
Enum> value = enumValues[i];
Field f;
try {
f = value.getDeclaringClass().getField(value.name());
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
RosettaEnumValue ann = f.getAnnotation(RosettaEnumValue.class);
if (ann != null) {
String nameOverride = enumMap.get(ann.value());
if (nameOverride != null) {
names[i] = nameOverride;
}
}
}
});
return names;
}
@Override
public void findEnumAliases(MapperConfig> config, AnnotatedClass enumType,
Enum>[] enumValues, String[][] aliasList) {
if (rosettaEnumBuilderIntrospector.isApplicable(enumType)) {
rosettaEnumBuilderIntrospector.findEnumAliases(enumType, enumValues, aliasList);
} else {
super.findEnumAliases(config, enumType, enumValues, aliasList);
}
}
private AnnotatedClass getEnclosingAnnotatedClass(MapperConfig> config, AnnotatedMember member) {
// TODO: see issue https://github.com/FasterXML/jackson-databind/issues/4141
return AnnotatedClassResolver.resolve(config, config.constructType(member.getDeclaringClass()), config);
}
private AnnotatedClass getAnnotatedClassOrContent(MapperConfig> config, AnnotatedMember m) {
JavaType t;
if (m instanceof AnnotatedMethod) {
AnnotatedMethod method = (AnnotatedMethod) m;
if (method.getParameterCount() == 1) {
// For setters
t = method.getParameterType(0);
} else {
t = method.getType();
}
} else {
t = m.getType();
}
if (t.getContentType() != null) {
t = t.getContentType();
}
return AnnotatedClassResolver.resolve(config, t, config);
}
private Optional getAttributeXMLConfiguration(MapperConfig> config, Annotated a) {
return Optional.of(a)
.filter(annotated -> annotated instanceof AnnotatedMember)
.map(annotated -> (AnnotatedMember) annotated)
.flatMap(annotatedMember ->
Optional.ofNullable(annotatedMember.getAnnotation(RosettaAttribute.class))
.flatMap(rosettaAttributeAnnotation ->
getTypeXMLConfigurations(config, getEnclosingAnnotatedClass(config, annotatedMember)).stream()
.filter(t -> t.getAttributes().isPresent())
.map(t -> t.getAttributes().get())
.filter(attrMap -> attrMap.containsKey(rosettaAttributeAnnotation.value()))
.map(attributeMap -> attributeMap.get(rosettaAttributeAnnotation.value()))
.findFirst())
);
}
private List getTypeXMLConfigurations(MapperConfig> config, AnnotatedClass ac) {
List result = new ArrayList<>();
Set visited = new HashSet<>();
RosettaDataType ann;
while ((ann = ac.getAnnotation(RosettaDataType.class)) != null) {
final ModelSymbolId modelSymbolId = createModelSymbolId(ac, ann.value());
if (visited.add(modelSymbolId)) {
rosettaXMLConfiguration.getConfigurationForType(modelSymbolId).ifPresent(result::add);
}
if (ac.getType().getSuperClass() == null) {
break;
}
ac = AnnotatedClassResolver.resolve(config, ac.getType().getSuperClass(), config);
}
return result;
}
private Optional getEnumXMLConfigurations(MapperConfig> config, AnnotatedClass ac) {
RosettaEnum ann = ac.getAnnotation(RosettaEnum.class);
if (ann != null) {
final ModelSymbolId modelSymbolId = createModelSymbolId(ac, ann.value());
return rosettaXMLConfiguration.getConfigurationForType(modelSymbolId);
}
return Optional.empty();
}
private ModelSymbolId createModelSymbolId(AnnotatedClass ac, String name) {
final String namespace = ac.getType().getRawClass().getPackage().getName();
return new ModelSymbolId(DottedPath.splitOnDots(namespace), name);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy