diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java index 4be529b3..e16c7414 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonTypeInfo.java @@ -339,4 +339,195 @@ public abstract static class None {} * @since 2.16 */ public OptBoolean requireTypeIdForSubtypes() default OptBoolean.DEFAULT; + + /* + /********************************************************************** + /* Value class used to enclose information, allow for + /* merging of layered configuration settings. + /********************************************************************** + */ + + public static class Value + implements JacksonAnnotationValue, + java.io.Serializable + { + private static final long serialVersionUID = 1L; + + // should not really be needed usually but make sure defalts to `NONE`; other + // values of less interest + protected final static Value EMPTY = new Value(Id.NONE, As.PROPERTY, null, null, false, null); + + protected final Id _idType; + protected final As _inclusionType; + protected final String _propertyName; + + protected final Class _defaultImpl; + protected final boolean _idVisible; + protected final Boolean _requireTypeIdForSubtypes; + + /* + /********************************************************************** + /* Construction + /********************************************************************** + */ + + protected Value(Id idType, As inclusionType, + String propertyName, Class defaultImpl, boolean idVisible, Boolean requireTypeIdForSubtypes) + { + _defaultImpl = defaultImpl; + _idType = idType; + _inclusionType = inclusionType; + _propertyName = propertyName; + _idVisible = idVisible; + _requireTypeIdForSubtypes = requireTypeIdForSubtypes; + } + + public static Value construct(Id idType, As inclusionType, + String propertyName, Class defaultImpl, boolean idVisible, Boolean requireTypeIdForSubtypes) + { + // couple of overrides we need to apply here. First: if no propertyName specified, + // use Id-specific property name + if ((propertyName == null) || propertyName.isEmpty()) { + if (idType != null) { + propertyName = idType.getDefaultPropertyName(); + } else { + propertyName = ""; + } + } + // Although we can not do much here for special handling of `Void`, we can convert + // annotation types as `null` (== no default implementation) + if ((defaultImpl == null) || defaultImpl.isAnnotation()) { + defaultImpl = null; + } + return new Value(idType, inclusionType, propertyName, defaultImpl, idVisible, requireTypeIdForSubtypes); + } + + public static Value from(JsonTypeInfo src) { + if (src == null) { + return null; + } + return construct(src.use(), src.include(), + src.property(), src.defaultImpl(), src.visible(), src.requireTypeIdForSubtypes().asBoolean()); + } + + /* + /********************************************************************** + /* Mutators + /********************************************************************** + */ + + public Value withDefaultImpl(Class impl) { + return (impl == _defaultImpl) ? this : + new Value(_idType, _inclusionType, _propertyName, impl, _idVisible, _requireTypeIdForSubtypes); + } + + public Value withIdType(Id idType) { + return (idType == _idType) ? this : + new Value(idType, _inclusionType, _propertyName, _defaultImpl, _idVisible, _requireTypeIdForSubtypes); + } + + public Value withInclusionType(As inclusionType) { + return (inclusionType == _inclusionType) ? this : + new Value(_idType, inclusionType, _propertyName, _defaultImpl, _idVisible, _requireTypeIdForSubtypes); + } + + public Value withPropertyName(String propName) { + return (propName == _propertyName) ? this : + new Value(_idType, _inclusionType, propName, _defaultImpl, _idVisible, _requireTypeIdForSubtypes); + } + + public Value withIdVisible(boolean visible) { + return (visible == _idVisible) ? this : + new Value(_idType, _inclusionType, _propertyName, _defaultImpl, visible, _requireTypeIdForSubtypes); + } + + public Value withRequireTypeIdForSubtypes(Boolean requireTypeIdForSubtypes) { + return (_requireTypeIdForSubtypes == requireTypeIdForSubtypes) ? this : + new Value(_idType, _inclusionType, _propertyName, _defaultImpl, _idVisible, requireTypeIdForSubtypes); + } + + /* + /********************************************************************** + /* Simple accessors + /********************************************************************** + */ + + @Override + public Class valueFor() { + return JsonTypeInfo.class; + } + + public Class getDefaultImpl() { return _defaultImpl; } + public Id getIdType() { return _idType; } + public As getInclusionType() { return _inclusionType; } + public String getPropertyName() { return _propertyName; } + public boolean getIdVisible() { return _idVisible; } + public Boolean getRequireTypeIdForSubtypes() { return _requireTypeIdForSubtypes; } + + /** + * Static helper method for simple(r) checking of whether there's a Value instance + * that indicates that polymorphic handling is (to be) enabled. + */ + public static boolean isEnabled(JsonTypeInfo.Value v) { + return (v != null) && + (v._idType != null) && (v._idType != Id.NONE); + } + + /* + /********************************************************************** + /* Standard methods + /********************************************************************** + */ + + @Override + public String toString() { + return String.format("JsonTypeInfo.Value(idType=%s,includeAs=%s,propertyName=%s,defaultImpl=%s,idVisible=%s" + + ",requireTypeIdForSubtypes=%s)", + _idType, _inclusionType, _propertyName, + ((_defaultImpl == null) ? "NULL" : _defaultImpl.getName()), + _idVisible, _requireTypeIdForSubtypes); + } + + @Override + public int hashCode() { + int hashCode = 1; + hashCode = 31 * hashCode + (_idType != null ? _idType.hashCode() : 0); + hashCode = 31 * hashCode + (_inclusionType != null ? _inclusionType.hashCode() : 0); + hashCode = 31 * hashCode + (_propertyName != null ? _propertyName.hashCode() : 0); + hashCode = 31 * hashCode + (_defaultImpl != null ? _defaultImpl.hashCode() : 0); + hashCode = 31 * hashCode + (_requireTypeIdForSubtypes ? 11 : -17); + hashCode = 31 * hashCode + (_idVisible ? 11 : -17); + return hashCode; + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + if (o == null) return false; + return (o.getClass() == getClass()) + && _equals(this, (Value) o); + } + + private static boolean _equals(Value a, Value b) + { + return (a._idType == b._idType) + && (a._inclusionType == b._inclusionType) + && (a._defaultImpl == b._defaultImpl) + && (a._idVisible == b._idVisible) + && _equal(a._propertyName, b._propertyName) + && _equal(a._requireTypeIdForSubtypes, b._requireTypeIdForSubtypes) + ; + } + + private static boolean _equal(T value1, T value2) + { + if (value1 == null) { + return (value2 == null); + } + if (value2 == null) { + return false; + } + return value1.equals(value2); + } + } } diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java new file mode 100644 index 00000000..82b76ccc --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/annotation/JsonTypeInfoTest.java @@ -0,0 +1,103 @@ +package com.fasterxml.jackson.annotation; + +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; + +public class JsonTypeInfoTest extends TestBase +{ + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, visible=true, + defaultImpl = JsonTypeInfo.class, requireTypeIdForSubtypes = OptBoolean.TRUE) + private final static class Anno1 { } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.EXTERNAL_PROPERTY, + property = "ext", + defaultImpl = Void.class, requireTypeIdForSubtypes = OptBoolean.FALSE) + private final static class Anno2 { } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.EXTERNAL_PROPERTY, + property = "ext", + defaultImpl = Void.class) + private final static class Anno3 { } + + public void testEmpty() { + // 07-Mar-2017, tatu: Important to distinguish "none" from 'empty' value... + assertNull(JsonTypeInfo.Value.from(null)); + } + + public void testFromAnnotation() throws Exception + { + JsonTypeInfo.Value v1 = JsonTypeInfo.Value.from(Anno1.class.getAnnotation(JsonTypeInfo.class)); + assertEquals(JsonTypeInfo.Id.CLASS, v1.getIdType()); + // default from annotation definition + assertEquals(JsonTypeInfo.As.PROPERTY, v1.getInclusionType()); + // default from annotation definition + assertEquals("@class", v1.getPropertyName()); + assertTrue(v1.getIdVisible()); + assertNull(v1.getDefaultImpl()); + assertTrue(v1.getRequireTypeIdForSubtypes()); + + JsonTypeInfo.Value v2 = JsonTypeInfo.Value.from(Anno2.class.getAnnotation(JsonTypeInfo.class)); + assertEquals(JsonTypeInfo.Id.NAME, v2.getIdType()); + assertEquals(JsonTypeInfo.As.EXTERNAL_PROPERTY, v2.getInclusionType()); + assertEquals("ext", v2.getPropertyName()); + assertFalse(v2.getIdVisible()); + assertEquals(Void.class, v2.getDefaultImpl()); + assertFalse(v2.getRequireTypeIdForSubtypes()); + + assertTrue(v1.equals(v1)); + assertTrue(v2.equals(v2)); + + assertFalse(v1.equals(v2)); + assertFalse(v2.equals(v1)); + + assertEquals("JsonTypeInfo.Value(idType=CLASS,includeAs=PROPERTY,propertyName=@class,defaultImpl=NULL,idVisible=true,requireTypeIdForSubtypes=true)", v1.toString()); + assertEquals("JsonTypeInfo.Value(idType=NAME,includeAs=EXTERNAL_PROPERTY,propertyName=ext,defaultImpl=java.lang.Void,idVisible=false,requireTypeIdForSubtypes=false)", v2.toString()); + } + + public void testMutators() throws Exception + { + JsonTypeInfo.Value v = JsonTypeInfo.Value.from(Anno1.class.getAnnotation(JsonTypeInfo.class)); + assertEquals(JsonTypeInfo.Id.CLASS, v.getIdType()); + + assertSame(v, v.withIdType(JsonTypeInfo.Id.CLASS)); + JsonTypeInfo.Value v2 = v.withIdType(JsonTypeInfo.Id.MINIMAL_CLASS); + assertEquals(JsonTypeInfo.Id.MINIMAL_CLASS, v2.getIdType()); + + assertEquals(JsonTypeInfo.As.PROPERTY, v.getInclusionType()); + assertSame(v, v.withInclusionType(JsonTypeInfo.As.PROPERTY)); + v2 = v.withInclusionType(JsonTypeInfo.As.EXTERNAL_PROPERTY); + assertEquals(JsonTypeInfo.As.EXTERNAL_PROPERTY, v2.getInclusionType()); + + assertSame(v, v.withDefaultImpl(null)); + v2 = v.withDefaultImpl(String.class); + assertEquals(String.class, v2.getDefaultImpl()); + + assertSame(v, v.withIdVisible(true)); + assertFalse(v.withIdVisible(false).getIdVisible()); + + assertEquals("foobar", v.withPropertyName("foobar").getPropertyName()); + } + + public void testWithRequireTypeIdForSubtypes() { + JsonTypeInfo.Value empty = JsonTypeInfo.Value.EMPTY; + assertNull(empty.getRequireTypeIdForSubtypes()); + + JsonTypeInfo.Value requireTypeIdTrue = empty.withRequireTypeIdForSubtypes(Boolean.TRUE); + assertEquals(Boolean.TRUE, requireTypeIdTrue.getRequireTypeIdForSubtypes()); + + JsonTypeInfo.Value requireTypeIdFalse = empty.withRequireTypeIdForSubtypes(Boolean.FALSE); + assertEquals(Boolean.FALSE, requireTypeIdFalse.getRequireTypeIdForSubtypes()); + + JsonTypeInfo.Value requireTypeIdDefault = empty.withRequireTypeIdForSubtypes(null); + assertNull(requireTypeIdDefault.getRequireTypeIdForSubtypes()); + } + + public void testDefaultValueForRequireTypeIdForSubtypes() { + // default value + JsonTypeInfo.Value v3 = JsonTypeInfo.Value.from(Anno3.class.getAnnotation(JsonTypeInfo.class)); + assertNull(v3.getRequireTypeIdForSubtypes()); + + // toString() + assertEquals("JsonTypeInfo.Value(idType=NAME,includeAs=EXTERNAL_PROPERTY,propertyName=ext," + + "defaultImpl=java.lang.Void,idVisible=false,requireTypeIdForSubtypes=null)", v3.toString()); + } +}