com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector Maven / Gradle / Ivy
Show all versions of jackson-module-jakarta-xmlbind-annotations Show documentation
package com.fasterxml.jackson.module.jakarta.xmlbind;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import jakarta.xml.bind.JAXBElement;
import jakarta.xml.bind.annotation.*;
import jakarta.xml.bind.annotation.adapters.XmlAdapter;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapters;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.*;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
import com.fasterxml.jackson.databind.jsontype.impl.StdTypeResolverBuilder;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.ClassUtil;
import com.fasterxml.jackson.databind.util.Converter;
import com.fasterxml.jackson.module.jakarta.xmlbind.deser.DataHandlerDeserializer;
import com.fasterxml.jackson.module.jakarta.xmlbind.ser.DataHandlerSerializer;
/**
* Annotation introspector that uses Jakarta Xml Bind annotations
* (nee "JAXB" Annotations") where applicable for jackson-databind to use.
* As of Jackson 2.x, majority of XmlBind annotations are supported
* to some degree.
* Ones that are NOT yet supported are:
*
* - {@link XmlAnyAttribute} not supported; possible (if unlikely) to be used
* (as an alias for {@code @JsonAnySetter})
*
* - {@link XmlAnyElement} not supported; unlikely to ever be supported.
*
* - {@link jakarta.xml.bind.annotation.XmlAttachmentRef}: JSON does not support external attachments
*
- {@link XmlElementDecl}
*
- {@link XmlElementRefs} because Jackson doesn't have any support for 'named' collection items
*
- {@link jakarta.xml.bind.annotation.XmlInlineBinaryData} since the underlying concepts
* (like XOP) do not exist in JSON -- Jackson will always use inline base64 encoding as the method
*
- {@link jakarta.xml.bind.annotation.XmlList} because JSON does not have (or necessarily need)
* method of serializing list of values as space-separated Strings
*
- {@link jakarta.xml.bind.annotation.XmlMimeType}
*
- {@link jakarta.xml.bind.annotation.XmlMixed} since JSON has no concept of mixed content
*
- {@link XmlRegistry} not supported, unlikely to ever be.
*
- {@link XmlSchema} not supported, unlikely to ever be.
*
- {@link XmlSchemaType} not supported, unlikely to ever be.
*
- {@link XmlSchemaTypes} not supported, unlikely to ever be.
*
- {@link XmlSeeAlso} not supported.
*
*
* Note also the following limitations:
*
*
* - Any property annotated with {@link XmlValue} will have implicit property named 'value' on
* its JSON object; although it should be possible to override this name
*
*
*
*/
public class JakartaXmlBindAnnotationIntrospector
extends AnnotationIntrospector
implements AnnotationIntrospector.XmlExtensions, // since 2.13
Versioned
{
private static final long serialVersionUID = -1L;
protected final static String DEFAULT_NAME_FOR_XML_VALUE = "value";
protected final static boolean DEFAULT_IGNORE_XMLIDREF = false;
protected final static String MARKER_FOR_DEFAULT = "##default";
protected final static JsonFormat.Value FORMAT_STRING = new JsonFormat.Value().withShape(JsonFormat.Shape.STRING);
protected final static JsonFormat.Value FORMAT_INT = new JsonFormat.Value().withShape(JsonFormat.Shape.NUMBER_INT);
protected final String _jaxbPackageName;
protected final JsonSerializer> _dataHandlerSerializer;
protected final JsonDeserializer> _dataHandlerDeserializer;
protected final TypeFactory _typeFactory;
protected final boolean _ignoreXmlIDREF;
/**
* When using {@link XmlValue} annotation, a placeholder name is assigned
* to property (unless overridden by explicit name); this configuration
* value specified what that name is.
*/
protected String _xmlValueName = DEFAULT_NAME_FOR_XML_VALUE;
/**
* Inclusion value to return for properties annotated with
* {@link XmlElement} and {@link XmlElementWrapper}, in case {@code nillable}
* property is left as {@code false}. Default setting is
* {@code null}; this is typically changed to either
* {@link com.fasterxml.jackson.annotation.JsonInclude.Include#NON_NULL}
* or {@link com.fasterxml.jackson.annotation.JsonInclude.Include#NON_EMPTY}.
*
* @since 2.7
*/
protected JsonInclude.Include _nonNillableInclusion = null;
/**
* @deprecated Since 2.1, use constructor that takes TypeFactory.
*/
@Deprecated
public JakartaXmlBindAnnotationIntrospector() {
this(TypeFactory.defaultInstance());
}
public JakartaXmlBindAnnotationIntrospector(MapperConfig> config) {
this(config.getTypeFactory());
}
public JakartaXmlBindAnnotationIntrospector(TypeFactory typeFactory) {
this(typeFactory, DEFAULT_IGNORE_XMLIDREF);
}
/**
* @param typeFactory Type factory used for resolving type information
* @param ignoreXmlIDREF Whether {@link XmlIDREF} annotation should be processed
* JAXB style (meaning that references are always serialized using id), or
* not (first reference as full POJO, others as ids)
*/
public JakartaXmlBindAnnotationIntrospector(TypeFactory typeFactory, boolean ignoreXmlIDREF)
{
_typeFactory = (typeFactory == null)? TypeFactory.defaultInstance() : typeFactory;
_ignoreXmlIDREF = ignoreXmlIDREF;
_jaxbPackageName = XmlElement.class.getPackage().getName();
JsonSerializer> dataHandlerSerializer = null;
JsonDeserializer> dataHandlerDeserializer = null;
/// Data handlers included dynamically, to try to prevent issues on
// platforms with less than complete support for JAXB API
try {
dataHandlerSerializer = (JsonSerializer>) DataHandlerSerializer.class.newInstance();
dataHandlerDeserializer = (JsonDeserializer>) DataHandlerDeserializer.class.newInstance();
} catch (Throwable e) {
//dataHandlers not supported...
}
_dataHandlerSerializer = dataHandlerSerializer;
_dataHandlerDeserializer = dataHandlerDeserializer;
}
/**
* Method that will return version information stored in and read from jar
* that contains this class.
*/
@Override
public Version version() {
return PackageVersion.VERSION;
}
/*
/**********************************************************************
/* Configuration
/**********************************************************************
*/
/**
* Configuration method that can be used to change default name
* ("value") used for properties annotated with {@link XmlValue};
* note that setting it to null
will actually avoid
* name override, and name will instead be derived from underlying
* method name using standard bean name introspection.
*/
public void setNameUsedForXmlValue(String name) {
_xmlValueName = name;
}
/**
* Accessor for getting currently configured placeholder named
* used for property annotated with {@link XmlValue}.
*/
public String getNameUsedForXmlValue() {
return _xmlValueName;
}
/**
* Method to call to change inclusion criteria used for property annotated
* with {@link XmlElement} or {@link XmlElementWrapper}, with nillable
* set as false
.
*
* @since 2.7
*/
public JakartaXmlBindAnnotationIntrospector setNonNillableInclusion(JsonInclude.Include incl) {
_nonNillableInclusion = incl;
return this;
}
/**
* @since 2.7
*/
public JsonInclude.Include getNonNillableInclusion() {
return _nonNillableInclusion;
}
/*
/**********************************************************************
/* Extended API: since 2.13, AnnotationIntrospector.XmlExtensions
* (before 2.13: XmlAnnotationIntrospector)
/**********************************************************************
*/
@Override // AnnotationIntrospector.XmlExtensions
public String findNamespace(MapperConfig> config, Annotated ann)
{
String ns = null;
if (ann instanceof AnnotatedClass) {
// For classes, it must be @XmlRootElement. Also, we do
// want to use defaults from package, base class
XmlRootElement elem = findRootElementAnnotation((AnnotatedClass) ann);
if (elem != null) {
ns = elem.namespace();
}
} else {
// For others, XmlElement or XmlAttribute work (anything else?)
XmlElement elem = findAnnotation(XmlElement.class, ann, false, false, false);
if (elem != null) {
ns = elem.namespace();
}
if (ns == null || MARKER_FOR_DEFAULT.equals(ns)) {
XmlAttribute attr = findAnnotation(XmlAttribute.class, ann, false, false, false);
if (attr != null) {
ns = attr.namespace();
}
}
}
// JAXB uses marker for "not defined"
if (MARKER_FOR_DEFAULT.equals(ns)) {
ns = null;
}
return ns;
}
/**
* Here we assume fairly simple logic; if there is XmlAttribute
to be found,
* we consider it an attribute; if XmlElement
, not-an-attribute; and otherwise
* we will consider there to be no information.
* Caller is likely to default to considering things as elements.
*/
@Override // AnnotationIntrospector.XmlExtensions
public Boolean isOutputAsAttribute(MapperConfig> config, Annotated ann) {
XmlAttribute attr = findAnnotation(XmlAttribute.class, ann, false, false, false);
if (attr != null) {
return Boolean.TRUE;
}
XmlElement elem = findAnnotation(XmlElement.class, ann, false, false, false);
if (elem != null) {
return Boolean.FALSE;
}
return null;
}
@Override // AnnotationIntrospector.XmlExtensions
public Boolean isOutputAsText(MapperConfig> config, Annotated ann) {
XmlValue attr = findAnnotation(XmlValue.class, ann, false, false, false);
if (attr != null) {
return Boolean.TRUE;
}
return null;
}
@Override // AnnotationIntrospector.XmlExtensions
public Boolean isOutputAsCData(MapperConfig> config, Annotated ann) {
// JAXB has nothing for this one?
return null;
}
/*
/**********************************************************************
/* General annotations (for classes, properties)
/**********************************************************************
*/
@Override
public ObjectIdInfo findObjectIdInfo(Annotated ann)
{
// To work in the way that works with JAXB and Jackson,
// we need to do things in bit of round-about way, starting
// with AnnotatedClass, locating @XmlID property, if any.
if (!(ann instanceof AnnotatedClass)) {
return null;
}
AnnotatedClass ac = (AnnotatedClass) ann;
/* Ideally, should not have to infer settings for class from
* individual fields and/or methods; but for now this
* has to do ...
*/
PropertyName idPropName = null;
method_loop:
for (AnnotatedMethod m : ac.memberMethods()) {
XmlID idProp = m.getAnnotation(XmlID.class);
if (idProp == null) {
continue;
}
switch (m.getParameterCount()) {
case 0: // getter
idPropName = findJaxbPropertyName(m, m.getRawType(),
_okNameForGetter(m));
break method_loop;
case 1: // setter
idPropName = findJaxbPropertyName(m, m.getRawType(),
_okNameForMutator(m));
break method_loop;
}
}
if (idPropName == null) {
for (AnnotatedField f : ac.fields()) {
XmlID idProp = f.getAnnotation(XmlID.class);
if (idProp != null) {
idPropName = findJaxbPropertyName(f, f.getRawType(), f.getName());
break;
}
}
}
if (idPropName != null) {
/* Scoping... hmmh. Could XML requires somewhat global scope, n'est pas?
* The alternative would be to use declared type of this class.
*/
Class> scope = Object.class; // alternatively would use 'ac.getRawType()'
// and we will assume that there exists property thus named...
return new ObjectIdInfo(idPropName,
scope, ObjectIdGenerators.PropertyGenerator.class,
// should we customize Object Id resolver somehow?
SimpleObjectIdResolver.class);
}
return null;
}
@Override
public ObjectIdInfo findObjectReferenceInfo(Annotated ann, ObjectIdInfo base)
{
if (!_ignoreXmlIDREF) {
XmlIDREF idref = ann.getAnnotation(XmlIDREF.class);
/* JAXB makes XmlIDREF mean "always as id", as far as I know.
* May need to make it configurable in future, but for not that
* is fine...
*/
if (idref != null) {
if (base == null) {
base = ObjectIdInfo.empty();
}
base = base.withAlwaysAsId(true);
}
}
return base;
}
/*
/**********************************************************************
/* General class annotations
/**********************************************************************
*/
@Override
public PropertyName findRootName(AnnotatedClass ac)
{
XmlRootElement elem = findRootElementAnnotation(ac);
if (elem != null) {
return _combineNames(elem.name(), elem.namespace(), "");
}
return null;
}
// 28-Jul-2020, tatu: two parts; by-name ignorals have no counterpart in JAXB;
// although "ignore unknown" could map (by default this is what JAXB does).
// But without annotation changing that, not much we can map here
/*
@Override
public JsonIgnoreProperties.Value findPropertyIgnoralByName(MapperConfig> config, Annotated ann)
*/
@Override
public Boolean isIgnorableType(AnnotatedClass ac) {
// Does JAXB have any such indicators? No?
return null;
}
/*
/**********************************************************************
/* General member (field, method/constructor) annotations
/**********************************************************************
*/
@Override
public boolean hasIgnoreMarker(AnnotatedMember m) {
return m.getAnnotation(XmlTransient.class) != null;
}
//(ryan) JAXB has @XmlAnyAttribute and @XmlAnyElement annotations, but they're not applicable in this case
// because JAXB says those annotations are only applicable to methods with specific signatures
// that Jackson doesn't support (Jackson's any setter needs 2 arguments, name and value, whereas
// JAXB expects use of Map
// 28-May-2016, tatu: While `@XmlAnyAttribute` looks ALMOST like applicable (esp.
// assuming Jackson could use `Map` field, not just setter/getter), it is alas not.
// The reason is that key is expected to be `QNmae`, XML/JAXB specific name and
// something Jackson does not require or use
/*
@Override
public boolean hasAnySetterAnnotation(AnnotatedMethod am) { }
@Override
public boolean hasAnySetterAnnotation(AnnotatedMethod am)
*/
@Override
public Boolean hasRequiredMarker(AnnotatedMember m) {
// 17-Oct-2017, tatu: [modules-base#32]
// Before 2.9.3, was handling `true` correctly,
// but otherwise used confusing logic (probably in attempt to try to avoid
// reporting not-required for default value case
XmlAttribute attr = m.getAnnotation(XmlAttribute.class);
if (attr != null) {
return attr.required();
}
XmlElement elem = m.getAnnotation(XmlElement.class);
if (elem != null) {
return elem.required();
}
return null;
}
@Override
public PropertyName findWrapperName(Annotated ann)
{
XmlElementWrapper w = findAnnotation(XmlElementWrapper.class, ann, false, false, false);
if (w != null) {
// 18-Sep-2013, tatu: As per [jaxb-annotations#24], need to take special care with empty
// String, as that should indicate here "use underlying unmodified
// property name" (that is, one NOT overridden by @JsonProperty)
PropertyName name = _combineNames(w.name(), w.namespace(), "");
// clumsy, yes, but has to do:
if (!name.hasSimpleName()) {
if (ann instanceof AnnotatedMethod) {
AnnotatedMethod am = (AnnotatedMethod) ann;
String str;
if (am.getParameterCount() == 0) {
str = _okNameForGetter(am);
} else {
str = _okNameForMutator(am);
}
if (str != null) {
return name.withSimpleName(str);
}
}
return name.withSimpleName(ann.getName());
}
return name;
}
return null;
}
@Override
public String findImplicitPropertyName(AnnotatedMember m) {
XmlValue valueInfo = m.getAnnotation(XmlValue.class);
if (valueInfo != null) {
return _xmlValueName;
}
return null;
}
@Override
public JsonFormat.Value findFormat(Annotated m) {
// Use @XmlEnum value (Class) to indicate format, iff it makes sense
if (m instanceof AnnotatedClass) {
XmlEnum ann = m.getAnnotation(XmlEnum.class);
if (ann != null) {
Class> type = ann.value();
if (type == String.class || type.isEnum()) {
return FORMAT_STRING;
}
if (Number.class.isAssignableFrom(type)) {
return FORMAT_INT;
}
}
}
return null;
}
/*
/**********************************************************************
/* Property auto-detection
/**********************************************************************
*/
@Override
public VisibilityChecker> findAutoDetectVisibility(AnnotatedClass ac,
VisibilityChecker> checker)
{
XmlAccessType at = findAccessType(ac);
if (at == null) {
/* JAXB default is "PUBLIC_MEMBER"; however, here we should not
* override settings if there is no annotation -- that would mess
* up global baseline. Fortunately Jackson defaults are very close
* to JAXB 'PUBLIC_MEMBER' settings (considering that setters and
* getters must come in pairs)
*/
return checker;
}
// Note: JAXB does not do creator auto-detection, can (and should) ignore
switch (at) {
case FIELD: // all fields, independent of visibility; no methods
return checker.withFieldVisibility(Visibility.ANY)
.withSetterVisibility(Visibility.NONE)
.withGetterVisibility(Visibility.NONE)
.withIsGetterVisibility(Visibility.NONE)
;
case NONE: // no auto-detection
return checker.withFieldVisibility(Visibility.NONE)
.withSetterVisibility(Visibility.NONE)
.withGetterVisibility(Visibility.NONE)
.withIsGetterVisibility(Visibility.NONE)
;
case PROPERTY:
return checker.withFieldVisibility(Visibility.NONE)
.withSetterVisibility(Visibility.PUBLIC_ONLY)
.withGetterVisibility(Visibility.PUBLIC_ONLY)
.withIsGetterVisibility(Visibility.PUBLIC_ONLY)
;
case PUBLIC_MEMBER:
return checker.withFieldVisibility(Visibility.PUBLIC_ONLY)
.withSetterVisibility(Visibility.PUBLIC_ONLY)
.withGetterVisibility(Visibility.PUBLIC_ONLY)
.withIsGetterVisibility(Visibility.PUBLIC_ONLY)
;
}
return checker;
}
/**
* Method for locating JAXB {@link XmlAccessType} annotation value
* for given annotated entity, if it has one, or inherits one from
* its ancestors (in JAXB sense, package etc). Returns null if
* nothing has been explicitly defined.
*/
protected XmlAccessType findAccessType(Annotated ac)
{
XmlAccessorType at = findAnnotation(XmlAccessorType.class, ac, true, true, true);
return (at == null) ? null : at.value();
}
/*
/**********************************************************************
/* Class annotations for Polymorphic type handling
/**********************************************************************
*/
@Override
public TypeResolverBuilder> findTypeResolver(MapperConfig> config,
AnnotatedClass ac, JavaType baseType)
{
// no per-class type resolvers, right?
return null;
}
@Override
public TypeResolverBuilder> findPropertyTypeResolver(MapperConfig> config,
AnnotatedMember am, JavaType baseType)
{
// First: @XmlElements and @XmlElementRefs only applies type for immediate property, if it
// is NOT a structured type.
if (baseType.isContainerType()) return null;
return _typeResolverFromXmlElements(am);
}
@Override
public TypeResolverBuilder> findPropertyContentTypeResolver(MapperConfig> config,
AnnotatedMember am, JavaType containerType)
{
// First: let's ensure property is a container type: caller should have
// verified but just to be sure
if (containerType.getContentType() == null) {
throw new IllegalArgumentException("Must call method with a container or reference type (got "+containerType+")");
}
return _typeResolverFromXmlElements(am);
}
protected TypeResolverBuilder> _typeResolverFromXmlElements(AnnotatedMember am)
{
/* If simple type, @XmlElements and @XmlElementRefs are applicable.
* Note: @XmlElement and @XmlElementRef are NOT handled here, since they
* are handled specifically as non-polymorphic indication
* of the actual type
*/
XmlElements elems = findAnnotation(XmlElements.class, am, false, false, false);
XmlElementRefs elemRefs = findAnnotation(XmlElementRefs.class, am, false, false, false);
if (elems == null && elemRefs == null) {
return null;
}
TypeResolverBuilder> b = new StdTypeResolverBuilder();
// JAXB always uses type name as id
b = b.init(JsonTypeInfo.Id.NAME, null);
// and let's consider WRAPPER_OBJECT to be canonical inclusion method
b = b.inclusion(JsonTypeInfo.As.WRAPPER_OBJECT);
return b;
}
@Override
public List findSubtypes(Annotated a)
{
// No package/superclass defaulting (only used with fields, methods)
XmlElements elems = findAnnotation(XmlElements.class, a, false, false, false);
ArrayList result = null;
if (elems != null) {
result = new ArrayList();
for (XmlElement elem : elems.value()) {
String name = elem.name();
if (MARKER_FOR_DEFAULT.equals(name)) name = null;
result.add(new NamedType(elem.type(), name));
}
} else {
XmlElementRefs elemRefs = findAnnotation(XmlElementRefs.class, a, false, false, false);
if (elemRefs != null) {
result = new ArrayList();
for (XmlElementRef elemRef : elemRefs.value()) {
Class> refType = elemRef.type();
// only good for types other than JAXBElement (which is XML based)
if (!JAXBElement.class.isAssignableFrom(refType)) {
// first consider explicit name declaration
String name = elemRef.name();
if (name == null || MARKER_FOR_DEFAULT.equals(name)) {
XmlRootElement rootElement = (XmlRootElement) refType.getAnnotation(XmlRootElement.class);
if (rootElement != null) {
name = rootElement.name();
}
}
if (name == null || MARKER_FOR_DEFAULT.equals(name)) {
name = _decapitalize(refType.getSimpleName());
}
result.add(new NamedType(refType, name));
}
}
}
}
// Check @XmlSeeAlso as well.
/* 17-Aug-2012, tatu: But wait! For structured type, what we really is
* value (content) type!
* If code below does not make full (or any) sense, do not despair -- it
* is wrong. Yet it works. The call sequence before we get here is mangled,
* its logic twisted... but as Dire Straits put it: "That ain't working --
* that's The Way You Do It!"
*/
XmlSeeAlso ann = a.getAnnotation(XmlSeeAlso.class);
if (ann != null) {
if (result == null) {
result = new ArrayList();
}
for (Class> cls : ann.value()) {
result.add(new NamedType(cls));
}
}
return result;
}
@Override
public String findTypeName(AnnotatedClass ac) {
XmlType type = findAnnotation(XmlType.class, ac, false, false, false);
if (type != null) {
String name = type.name();
if (!MARKER_FOR_DEFAULT.equals(name)) return name;
}
return null;
}
/*
/**********************************************************************
/* Serialization: general annotations
/**********************************************************************
*/
@Override
public JsonSerializer> findSerializer(Annotated am)
{
final Class> type = _rawSerializationType(am);
/*
XmlAdapter