Skip to content

Commit 3579be0

Browse files
authored
Add support for new JsonTypeInfo.Id.SIMPLE_NAME polymorphic type id option (#4065)
1 parent 73eca59 commit 3579be0

File tree

3 files changed

+495
-0
lines changed

3 files changed

+495
-0
lines changed
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package com.fasterxml.jackson.databind.jsontype.impl;
2+
3+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
4+
import com.fasterxml.jackson.databind.BeanDescription;
5+
import com.fasterxml.jackson.databind.DatabindContext;
6+
import com.fasterxml.jackson.databind.JavaType;
7+
import com.fasterxml.jackson.databind.MapperFeature;
8+
import com.fasterxml.jackson.databind.cfg.MapperConfig;
9+
import com.fasterxml.jackson.databind.jsontype.NamedType;
10+
import java.util.Collection;
11+
import java.util.HashMap;
12+
import java.util.Map;
13+
import java.util.TreeSet;
14+
import java.util.concurrent.ConcurrentHashMap;
15+
16+
/**
17+
* {@link com.fasterxml.jackson.databind.jsontype.TypeIdResolver} implementation
18+
* that converts between (JSON) Strings and simple Java class names.
19+
*
20+
* @since 2.16
21+
*/
22+
public class SimpleNameIdResolver
23+
extends TypeIdResolverBase
24+
{
25+
protected final MapperConfig<?> _config;
26+
27+
/**
28+
* Mappings from class name to type id, used for serialization.
29+
*<p>
30+
* Since lazily constructed will require synchronization (either internal
31+
* by type, or external)
32+
*/
33+
protected final ConcurrentHashMap<String, String> _typeToId;
34+
35+
/**
36+
* Mappings from type id to JavaType, used for deserialization.
37+
*<p>
38+
* Eagerly constructed, not modified, can use regular unsynchronized {@link Map}.
39+
*/
40+
protected final Map<String, JavaType> _idToType;
41+
42+
protected final boolean _caseInsensitive;
43+
44+
protected SimpleNameIdResolver(MapperConfig<?> config, JavaType baseType,
45+
ConcurrentHashMap<String, String> typeToId,
46+
HashMap<String, JavaType> idToType)
47+
{
48+
super(baseType, config.getTypeFactory());
49+
_config = config;
50+
_typeToId = typeToId;
51+
_idToType = idToType;
52+
_caseInsensitive = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES);
53+
}
54+
55+
public static SimpleNameIdResolver construct(MapperConfig<?> config, JavaType baseType,
56+
Collection<NamedType> subtypes, boolean forSer, boolean forDeser)
57+
{
58+
// sanity check
59+
if (forSer == forDeser) throw new IllegalArgumentException();
60+
61+
final ConcurrentHashMap<String, String> typeToId;
62+
final HashMap<String, JavaType> idToType;
63+
64+
if (forSer) {
65+
// Only need Class-to-id for serialization; but synchronized since may be
66+
// lazily built (if adding type-id-mappings dynamically)
67+
typeToId = new ConcurrentHashMap<>();
68+
idToType = null;
69+
} else {
70+
idToType = new HashMap<>();
71+
// 14-Apr-2016, tatu: Apparently needed for special case of `defaultImpl`;
72+
// see [databind#1198] for details: but essentially we only need room
73+
// for a single value.
74+
typeToId = new ConcurrentHashMap<>(4);
75+
}
76+
final boolean caseInsensitive = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES);
77+
78+
if (subtypes != null) {
79+
for (NamedType t : subtypes) {
80+
// no name? Need to figure out default; for now, let's just
81+
// use non-qualified class name
82+
Class<?> cls = t.getType();
83+
String id = t.hasName() ? t.getName() : _defaultTypeId(cls);
84+
if (forSer) {
85+
typeToId.put(cls.getName(), id);
86+
}
87+
if (forDeser) {
88+
// [databind#1983]: for case-insensitive lookups must canonicalize:
89+
if (caseInsensitive) {
90+
id = id.toLowerCase();
91+
}
92+
// One more problem; sometimes we have same name for multiple types;
93+
// if so, use most specific
94+
JavaType prev = idToType.get(id); // lgtm [java/dereferenced-value-may-be-null]
95+
if (prev != null) { // Can only override if more specific
96+
if (cls.isAssignableFrom(prev.getRawClass())) { // nope, more generic (or same)
97+
continue;
98+
}
99+
}
100+
idToType.put(id, config.constructType(cls));
101+
}
102+
}
103+
}
104+
return new SimpleNameIdResolver(config, baseType, typeToId, idToType);
105+
}
106+
107+
@Override
108+
public JsonTypeInfo.Id getMechanism() { return JsonTypeInfo.Id.SIMPLE_NAME; }
109+
110+
@Override
111+
public String idFromValue(Object value) {
112+
return idFromClass(value.getClass());
113+
}
114+
115+
protected String idFromClass(Class<?> clazz)
116+
{
117+
if (clazz == null) {
118+
return null;
119+
}
120+
// NOTE: although we may need to let `TypeModifier` change actual type to use
121+
// for id, we can use original type as key for more efficient lookup:
122+
final String key = clazz.getName();
123+
String name = _typeToId.get(key);
124+
125+
if (name == null) {
126+
// 29-Nov-2019, tatu: As per test in `TestTypeModifierNameResolution` somehow
127+
// we need to do this odd piece here which seems unnecessary but isn't.
128+
Class<?> cls = _typeFactory.constructType(clazz).getRawClass();
129+
// 24-Feb-2011, tatu: As per [JACKSON-498], may need to dynamically look up name
130+
// can either throw an exception, or use default name...
131+
if (_config.isAnnotationProcessingEnabled()) {
132+
BeanDescription beanDesc = _config.introspectClassAnnotations(cls);
133+
name = _config.getAnnotationIntrospector().findTypeName(beanDesc.getClassInfo());
134+
}
135+
if (name == null) {
136+
// And if still not found, let's choose default?
137+
name = _defaultTypeId(cls);
138+
}
139+
_typeToId.put(key, name);
140+
}
141+
return name;
142+
}
143+
144+
@Override
145+
public String idFromValueAndType(Object value, Class<?> type) {
146+
// 18-Jan-2013, tatu: We may be called with null value occasionally
147+
// it seems; nothing much we can figure out that way.
148+
if (value == null) {
149+
return idFromClass(type);
150+
}
151+
return idFromValue(value);
152+
}
153+
154+
@Override
155+
public JavaType typeFromId(DatabindContext context, String id) {
156+
return _typeFromId(id);
157+
}
158+
159+
protected JavaType _typeFromId(String id) {
160+
// [databind#1983]: for case-insensitive lookups must canonicalize:
161+
if (_caseInsensitive) {
162+
id = id.toLowerCase();
163+
}
164+
// Now: if no type is found, should we try to locate it by
165+
// some other means? (specifically, if in same package as base type,
166+
// could just try Class.forName)
167+
// For now let's not add any such workarounds; can add if need be
168+
return _idToType.get(id);
169+
}
170+
171+
@Override
172+
public String getDescForKnownTypeIds() {
173+
// 05-May-2020, tatu: As per [databind#1919], only include ids for
174+
// non-abstract types
175+
final TreeSet<String> ids = new TreeSet<>();
176+
for (Map.Entry<String, JavaType> entry : _idToType.entrySet()) {
177+
if (entry.getValue().isConcrete()) {
178+
ids.add(entry.getKey());
179+
}
180+
}
181+
return ids.toString();
182+
}
183+
184+
@Override
185+
public String toString() {
186+
return String.format("[%s; id-to-type=%s]", getClass().getName(), _idToType);
187+
}
188+
189+
/*
190+
/*********************************************************
191+
/* Helper methods
192+
/*********************************************************
193+
*/
194+
195+
/**
196+
* If no name was explicitly given for a class, we will just
197+
* use simple class name
198+
*/
199+
protected static String _defaultTypeId(Class<?> cls)
200+
{
201+
String n = cls.getName();
202+
int ix = Math.max(n.lastIndexOf('.'), n.lastIndexOf('$'));
203+
return (ix < 0) ? n : n.substring(ix+1);
204+
}
205+
}

src/main/java/com/fasterxml/jackson/databind/jsontype/impl/StdTypeResolverBuilder.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,8 @@ protected TypeIdResolver idResolver(MapperConfig<?> config,
368368
return ClassNameIdResolver.construct(baseType, config, subtypeValidator);
369369
case MINIMAL_CLASS:
370370
return MinimalClassNameIdResolver.construct(baseType, config, subtypeValidator);
371+
case SIMPLE_NAME:
372+
return SimpleNameIdResolver.construct(config, baseType, subtypes, forSer, forDeser);
371373
case NAME:
372374
return TypeNameIdResolver.construct(config, baseType, subtypes, forSer, forDeser);
373375
case NONE: // hmmh. should never get this far with 'none'

0 commit comments

Comments
 (0)