TypeIdCacheGrowthTest.java
package tools.jackson.databind.jsontype.impl;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import tools.jackson.databind.*;
import tools.jackson.databind.deser.DeserializationProblemHandler;
import tools.jackson.databind.deser.std.NullifyingDeserializer;
import tools.jackson.databind.exc.InvalidTypeIdException;
import tools.jackson.databind.jsontype.TypeIdResolver;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
// [databind#6203]
/**
* Tests to verify that the per-{@link
* tools.jackson.databind.jsontype.TypeDeserializer} type id to deserializer
* lookup cache does not grow without bound: of unrecognized type ids, only ones
* handled via {@code defaultImpl} are cached, and cache size is capped.
*<p>
* Note: in package {@code ...jsontype.impl} to have access to protected
* {@code _deserializers} / {@code _findDeserializer()} of {@link TypeDeserializerBase}.
*/
public class TypeIdCacheGrowthTest extends DatabindTestUtil
{
static class Base { }
static class Impl extends Base { public int x; }
static class DefaultImpl extends Base { }
// Resolves exactly one id ("impl"), everything else is unknown
static class SingleIdResolver extends TypeIdResolverBase
{
@Override
public JsonTypeInfo.Id getMechanism() { return JsonTypeInfo.Id.NAME; }
@Override
public String idFromValue(DatabindContext ctxt, Object value) {
return idFromValueAndType(ctxt, value, value.getClass());
}
@Override
public String idFromValueAndType(DatabindContext ctxt, Object value, Class<?> type) {
return (type == Impl.class) ? "impl" : "?";
}
@Override
public JavaType typeFromId(DatabindContext ctxt, String id) {
return "impl".equals(id) ? ctxt.constructType(Impl.class) : null;
}
}
private final static int UNKNOWN_ID_COUNT = 100;
private final ObjectMapper MAPPER = newJsonMapper();
// Unknown type ids resolved via `defaultImpl` ARE cached (to avoid re-resolving
// them every time), since `defaultImpl` does not depend on reader configuration;
// growth is bounded by `MAX_CACHED_TYPE_IDS`
@Test
public void unknownTypeIdsWithDefaultImplCached() throws Exception
{
DeserializationContext ctxt = _context(MAPPER);
AsPropertyTypeDeserializer typeDeser = _typeDeserializer(MAPPER.constructType(DefaultImpl.class));
ValueDeserializer<Object> defaultDeser = null;
for (int i = 0; i < UNKNOWN_ID_COUNT; ++i) {
ValueDeserializer<Object> deser = typeDeser._findDeserializer(ctxt, "unknown-"+i);
if (defaultDeser == null) {
defaultDeser = deser;
} else {
// ... and all unknown ids share the very same `defaultImpl` deserializer
assertSame(defaultDeser, deser);
}
}
assertEquals(UNKNOWN_ID_COUNT, typeDeser._deserializers.size());
// As are actually resolvable type ids
typeDeser._findDeserializer(ctxt, "impl");
typeDeser._findDeserializer(ctxt, "impl");
assertEquals(UNKNOWN_ID_COUNT + 1, typeDeser._deserializers.size());
}
// Unknown type ids handled by `NullifyingDeserializer` must not be cached, either
@Test
public void unknownTypeIdsWithoutFailOnInvalidSubtypeNotCached() throws Exception
{
DeserializationContext ctxt = _context(jsonMapperBuilder()
.disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE)
.build());
AsPropertyTypeDeserializer typeDeser = _typeDeserializer(null);
for (int i = 0; i < UNKNOWN_ID_COUNT; ++i) {
assertSame(NullifyingDeserializer.instance,
typeDeser._findDeserializer(ctxt, "unknown-"+i));
}
assertEquals(0, typeDeser._deserializers.size());
typeDeser._findDeserializer(ctxt, "impl");
assertEquals(1, typeDeser._deserializers.size());
}
// Unknown type ids resolved by a `DeserializationProblemHandler` must not be cached
@Test
public void unknownTypeIdsFromProblemHandlerNotCached() throws Exception
{
DeserializationContext ctxt = _context(jsonMapperBuilder()
.addHandler(new DeserializationProblemHandler() {
@Override
public JavaType handleUnknownTypeId(DeserializationContext c,
JavaType baseType, String subTypeId, TypeIdResolver idResolver,
String failureMsg) {
return c.constructType(Impl.class);
}
})
.build());
AsPropertyTypeDeserializer typeDeser = _typeDeserializer(null);
for (int i = 0; i < UNKNOWN_ID_COUNT; ++i) {
typeDeser._findDeserializer(ctxt, "unknown-"+i);
}
assertEquals(0, typeDeser._deserializers.size());
// ... nor leak to a reader that has no such handler
DeserializationContext plainCtxt = _context(MAPPER);
assertThrows(InvalidTypeIdException.class,
() -> typeDeser._findDeserializer(plainCtxt, "unknown-0"));
typeDeser._findDeserializer(ctxt, "impl");
assertEquals(1, typeDeser._deserializers.size());
}
// Even type ids that do resolve (like different spellings of the same type id)
// must not grow the cache without bound
@Test
public void resolvedTypeIdCacheIsBounded() throws Exception
{
DeserializationContext ctxt = _context(MAPPER);
JavaType baseType = MAPPER.constructType(Base.class);
SingleIdResolver idRes = new SingleIdResolver() {
@Override
public JavaType typeFromId(DatabindContext c, String id) {
return id.startsWith("impl") ? c.constructType(Impl.class) : null;
}
};
idRes.init(baseType);
AsPropertyTypeDeserializer typeDeser = new AsPropertyTypeDeserializer(baseType, idRes,
"type", false, null, JsonTypeInfo.As.PROPERTY, true);
final int max = TypeDeserializerBase.MAX_CACHED_TYPE_IDS;
for (int i = 0; i < max; ++i) {
typeDeser._findDeserializer(ctxt, "impl-"+i);
}
assertEquals(max, typeDeser._deserializers.size());
// One more: cache is full, so gets cleared before new entry is added
typeDeser._findDeserializer(ctxt, "impl-"+max);
assertEquals(1, typeDeser._deserializers.size());
}
// Overlong type ids must not be cached, as cache is only bounded by entry count
// (and type ids may be as long as maximum String length allowed)
@Test
public void overlongTypeIdsNotCached() throws Exception
{
DeserializationContext ctxt = _context(MAPPER);
AsPropertyTypeDeserializer typeDeser = _typeDeserializer(MAPPER.constructType(DefaultImpl.class));
final int maxLen = TypeDeserializerBase.MAX_CACHED_TYPE_ID_LENGTH;
StringBuilder sb = new StringBuilder("unknown-");
while (sb.length() <= maxLen) {
sb.append('x');
}
final String overlongId = sb.toString();
assertEquals(maxLen + 1, overlongId.length());
ValueDeserializer<Object> deser = typeDeser._findDeserializer(ctxt, overlongId);
assertSame(deser, typeDeser._findDeserializer(ctxt, overlongId));
assertEquals(0, typeDeser._deserializers.size());
// But one of exactly maximum length is still cached
typeDeser._findDeserializer(ctxt, overlongId.substring(0, maxLen));
assertEquals(1, typeDeser._deserializers.size());
}
private DeserializationContext _context(ObjectMapper mapper) {
return mapper._deserializationContext();
}
private AsPropertyTypeDeserializer _typeDeserializer(JavaType defaultImpl)
{
JavaType baseType = MAPPER.constructType(Base.class);
SingleIdResolver idRes = new SingleIdResolver();
idRes.init(baseType);
return new AsPropertyTypeDeserializer(baseType, idRes, "type", false, defaultImpl,
JsonTypeInfo.As.PROPERTY, true);
}
}