Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package com.fasterxml.jackson.databind.jsontype.impl;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DatabindContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

/**
* {@link com.fasterxml.jackson.databind.jsontype.TypeIdResolver} implementation
* that converts between (JSON) Strings and simple Java class names via {@link Class#getSimpleName()}.
* <p>
* Note that this implementation is identical to {@link TypeNameIdResolver} except that instead of
* {@link TypeNameIdResolver#_defaultTypeId(Class)}, this implementation uses {@link Class#getSimpleName()}.
*
* @since 2.16
*/
public class SimpleClassNameIdResolver
extends TypeIdResolverBase
{
protected final MapperConfig<?> _config;

/**
* Mappings from class name to type id, used for serialization.
*<p>
* Since lazily constructed will require synchronization (either internal
* by type, or external)
*/
protected final ConcurrentHashMap<String, String> _typeToId;
Copy link
Member

@cowtowncoder cowtowncoder Aug 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to use concurrent data structure when it instances are immutable.

EDIT: I missed the part where we do actually modify the Map. So this is actually needed, just like TypeNameIdResolver.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not immutable. It has a mutable Map.

I think what is meant is whether a single SimpleClassNameIdResolver instance can be used by 2 threads at the same time. If it can be used by multiple threads, then this should be a concurrent map.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pjfanning Yes you are correct, I first only looked at construction but then noticed mutation later on. So my comment was based on only reading part of the code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻👍🏻


/**
* Mappings from type id to JavaType, used for deserialization.
*<p>
* Eagerly constructed, not modified, can use regular unsynchronized {@link Map}.
*/
protected final Map<String, JavaType> _idToType;

/**
* @since 2.11
*/
protected final boolean _caseInsensitive;

protected SimpleClassNameIdResolver(MapperConfig<?> config, JavaType baseType,
ConcurrentHashMap<String, String> typeToId,
HashMap<String, JavaType> idToType)
{
super(baseType, config.getTypeFactory());
_config = config;
_typeToId = typeToId;
_idToType = idToType;
_caseInsensitive = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES);
}

public static SimpleClassNameIdResolver construct(MapperConfig<?> config, JavaType baseType,
Collection<NamedType> subtypes, boolean forSer, boolean forDeser)
{
// sanity check
if (forSer == forDeser) throw new IllegalArgumentException();

final ConcurrentHashMap<String, String> typeToId;
final HashMap<String, JavaType> idToType;

if (forSer) {
// Only need Class-to-id for serialization; but synchronized since may be
// lazily built (if adding type-id-mappings dynamically)
typeToId = new ConcurrentHashMap<>();
idToType = null;
} else {
idToType = new HashMap<>();
// 14-Apr-2016, tatu: Apparently needed for special case of `defaultImpl`;
// see [databind#1198] for details: but essentially we only need room
// for a single value.
typeToId = new ConcurrentHashMap<>(4);
}
final boolean caseInsensitive = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_VALUES);

if (subtypes != null) {
for (NamedType t : subtypes) {
// no name? Need to figure out default; for now, let's just
// use non-qualified class name
Class<?> cls = t.getType();
String id = t.hasName() ? t.getName() : cls.getSimpleName(); // not {@code _defaultTypeId(cls);}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we use cls.getSimpleName(); not _defaultTypeId(cls); like in TypeNameIdResolver

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it still makes sense to extract _defaultTypeId() method here, too, same as in TypeNameIdResolver.java.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, will put back _defaultTypeId() and instead modify change its implementation a little bit. Here is the link 👉🏼 https://github.com/FasterXML/jackson-databind/pull/4065/files#r1283295309

if (forSer) {
typeToId.put(cls.getName(), id);
}
if (forDeser) {
// [databind#1983]: for case-insensitive lookups must canonicalize:
if (caseInsensitive) {
id = id.toLowerCase();
}
// One more problem; sometimes we have same name for multiple types;
// if so, use most specific
JavaType prev = idToType.get(id); // lgtm [java/dereferenced-value-may-be-null]
if (prev != null) { // Can only override if more specific
if (cls.isAssignableFrom(prev.getRawClass())) { // nope, more generic (or same)
continue;
}
}
idToType.put(id, config.constructType(cls));
}
}
}
return new SimpleClassNameIdResolver(config, baseType, typeToId, idToType);
}

@Override
public JsonTypeInfo.Id getMechanism() { return JsonTypeInfo.Id.SIMPLE_CLASS_NAME; }

@Override
public String idFromValue(Object value) {
return idFromClass(value.getClass());
}

protected String idFromClass(Class<?> clazz)
{
if (clazz == null) {
return null;
}
// NOTE: although we may need to let `TypeModifier` change actual type to use
// for id, we can use original type as key for more efficient lookup:
final String key = clazz.getName();
String name = _typeToId.get(key);

if (name == null) {
// 29-Nov-2019, tatu: As per test in `TestTypeModifierNameResolution` somehow
// we need to do this odd piece here which seems unnecessary but isn't.
Class<?> cls = _typeFactory.constructType(clazz).getRawClass();
// 24-Feb-2011, tatu: As per [JACKSON-498], may need to dynamically look up name
// can either throw an exception, or use default name...
if (_config.isAnnotationProcessingEnabled()) {
BeanDescription beanDesc = _config.introspectClassAnnotations(cls);
name = _config.getAnnotationIntrospector().findTypeName(beanDesc.getClassInfo());
}
if (name == null) {
// And if still not found, let's choose default?
name = cls.getSimpleName();
}
_typeToId.put(key, name);
}
return name;
}

@Override
public String idFromValueAndType(Object value, Class<?> type) {
// 18-Jan-2013, tatu: We may be called with null value occasionally
// it seems; nothing much we can figure out that way.
if (value == null) {
return idFromClass(type);
}
return idFromValue(value);
}

@Override
public JavaType typeFromId(DatabindContext context, String id) {
return _typeFromId(id);
}

protected JavaType _typeFromId(String id) {
// [databind#1983]: for case-insensitive lookups must canonicalize:
if (_caseInsensitive) {
id = id.toLowerCase();
}
// Now: if no type is found, should we try to locate it by
// some other means? (specifically, if in same package as base type,
// could just try Class.forName)
// For now let's not add any such workarounds; can add if need be
return _idToType.get(id);
}

@Override
public String getDescForKnownTypeIds() {
// 05-May-2020, tatu: As per [databind#1919], only include ids for
// non-abstract types
final TreeSet<String> ids = new TreeSet<>();
for (Map.Entry<String, JavaType> entry : _idToType.entrySet()) {
if (entry.getValue().isConcrete()) {
ids.add(entry.getKey());
}
}
return ids.toString();
}

@Override
public String toString() {
return String.format("[%s; id-to-type=%s]", getClass().getName(), _idToType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@ protected TypeIdResolver idResolver(MapperConfig<?> config,
return ClassNameIdResolver.construct(baseType, config, subtypeValidator);
case MINIMAL_CLASS:
return MinimalClassNameIdResolver.construct(baseType, config, subtypeValidator);
case SIMPLE_CLASS_NAME:
return SimpleClassNameIdResolver.construct(config, baseType, subtypes, forSer, forDeser);
case NAME:
return TypeNameIdResolver.construct(config, baseType, subtypes, forSer, forDeser);
case NONE: // hmmh. should never get this far with 'none'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package com.fasterxml.jackson.databind.jsontype;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.BaseMapTest;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
* Test for <a href="https://github.com/FasterXML/jackson-databind/issues/4061">
* [databind#4061] Add JsonTypeInfo.Id.SIMPLE_NAME using Class::getSimpleName</a>
*/

public class JsonTypeInfoSimpleClassName4061Test extends BaseMapTest {

@JsonTypeInfo(
use = JsonTypeInfo.Id.SIMPLE_CLASS_NAME)
@JsonSubTypes({
@JsonSubTypes.Type(value = InnerSub4061A.class),
@JsonSubTypes.Type(value = InnerSub4061B.class)
})
static class InnerSuper4061 { }

static class InnerSub4061A extends InnerSuper4061 { }

static class InnerSub4061B extends InnerSuper4061 { }

@JsonTypeInfo(
use = JsonTypeInfo.Id.MINIMAL_CLASS)
@JsonSubTypes({
@JsonSubTypes.Type(value = MinimalInnerSub4061A.class),
@JsonSubTypes.Type(value = MinimalInnerSub4061B.class)
})
static class MinimalInnerSuper4061 { }

static class MinimalInnerSub4061A extends MinimalInnerSuper4061 { }

static class MinimalInnerSub4061B extends MinimalInnerSuper4061 { }

@JsonTypeInfo(
use = JsonTypeInfo.Id.SIMPLE_CLASS_NAME)
@JsonSubTypes({
@JsonSubTypes.Type(value = MixedSub4061A.class),
@JsonSubTypes.Type(value = MixedSub4061B.class)
})
static class MixedSuper4061 { }

@JsonTypeInfo(
use = JsonTypeInfo.Id.MINIMAL_CLASS)
@JsonSubTypes({
@JsonSubTypes.Type(value = MixedMinimalSub4061A.class),
@JsonSubTypes.Type(value = MixedMinimalSub4061B.class)
})
static class MixedMinimalSuper4061 { }

private final ObjectMapper MAPPER = newJsonMapper();

// inner class that has contains dollar sign
public void testInnerClass() throws Exception
{
// ser
assertEquals(
a2q("{'@simpl':'InnerSub4061A'}"),
MAPPER.writeValueAsString(new InnerSub4061A()));

// deser <- breaks!
InnerSuper4061 bean = MAPPER.readValue(a2q("{'@simpl':'InnerSub4061B'}"), InnerSuper4061.class);
assertType(bean, InnerSuper4061.class);
}

// inner class that has contains dollar sign
public void testMinimalInnerClass() throws Exception
{
// ser
assertEquals(
a2q("{'@c':'.JsonTypeInfoSimpleClassName4061Test$MinimalInnerSub4061A'}"),
MAPPER.writeValueAsString(new MinimalInnerSub4061A()));

// deser <- breaks!
MinimalInnerSuper4061 bean = MAPPER.readValue(a2q("{'@c':'.JsonTypeInfoSimpleClassName4061Test$MinimalInnerSub4061A'}"), MinimalInnerSuper4061.class);
assertType(bean, MinimalInnerSuper4061.class);
}

// Basic : non-inner class, without dollar sign
public void testBasicClass() throws Exception
{
// ser
assertEquals(
a2q("{'@simpl':'BasicSub4061A'}"),
MAPPER.writeValueAsString(new BasicSub4061A()));

// deser
BasicSuper4061 bean = MAPPER.readValue(a2q("{'@simpl':'BasicSub4061B'}"), BasicSuper4061.class);
assertType(bean, BasicSuper4061.class);
assertType(bean, BasicSub4061B.class);
}

// Mixed : parent as inner, subtype as basic
public void testMixedClass() throws Exception
{
// ser
assertEquals(
a2q("{'@simpl':'MixedSub4061A'}"),
MAPPER.writeValueAsString(new MixedSub4061A()));

// deser
MixedSuper4061 bean = MAPPER.readValue(a2q("{'@simpl':'MixedSub4061B'}"), MixedSuper4061.class);
assertType(bean, MixedSuper4061.class);
assertType(bean, MixedSub4061B.class);
}
// Mixed : parent as inner, subtype as basic
public void testMixedMinimalClass() throws Exception
{
// ser
assertEquals(
a2q("{'@c':'.MixedMinimalSub4061A'}"),
MAPPER.writeValueAsString(new MixedMinimalSub4061A()));

// deser
MixedMinimalSuper4061 bean = MAPPER.readValue(a2q("{'@c':'.MixedMinimalSub4061B'}"), MixedMinimalSuper4061.class);
assertType(bean, MixedMinimalSuper4061.class);
assertType(bean, MixedMinimalSub4061B.class);
}
}

@JsonTypeInfo(
use = JsonTypeInfo.Id.SIMPLE_CLASS_NAME)
@JsonSubTypes({
@JsonSubTypes.Type(value = BasicSub4061A.class),
@JsonSubTypes.Type(value = BasicSub4061B.class)
})
class BasicSuper4061 { }

class BasicSub4061A extends BasicSuper4061 { }

class BasicSub4061B extends BasicSuper4061 { }

class MixedSub4061A extends JsonTypeInfoSimpleClassName4061Test.MixedSuper4061 { }

class MixedSub4061B extends JsonTypeInfoSimpleClassName4061Test.MixedSuper4061 { }

class MixedMinimalSub4061A extends JsonTypeInfoSimpleClassName4061Test.MixedMinimalSuper4061 { }

class MixedMinimalSub4061B extends JsonTypeInfoSimpleClassName4061Test.MixedMinimalSuper4061 { }