Update Jackson2ExecutionContextStringSerializer

This commit is contained in:
Mahmoud Ben Hassine
2020-06-10 20:11:08 +02:00
parent 6052852db6
commit 6d7d2927dd
3 changed files with 319 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2019 the original author or authors.
* Copyright 2008-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,28 +18,80 @@ package org.springframework.batch.core.repository.dao;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.fasterxml.jackson.annotation.JacksonAnnotation;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DatabindContext;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.batch.core.JobParameter;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.repository.ExecutionContextSerializer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
/**
* Implementation that uses Jackson2 to provide (de)serialization.
* Implementation that uses Jackson2 to provide (de)serialization.
*
* By default, this implementation trusts a limited set of classes to be
* deserialized from the execution context. If a class is not trusted by default
* and is safe to deserialize, you can provide an explicit mapping using Jackson
* annotations, as shown in the following example:
*
* <pre class="code">
* &#064;JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
* public class MyTrustedType implements Serializable {
*
* }
* </pre>
*
* It is also possible to provide a custom {@link ObjectMapper} with a mixin for
* the trusted type:
*
* <pre class="code">
* ObjectMapper objectMapper = new ObjectMapper();
* objectMapper.addMixIn(MyTrustedType.class, Object.class);
* Jackson2ExecutionContextStringSerializer serializer = new Jackson2ExecutionContextStringSerializer();
* serializer.setObjectMapper(objectMapper);
* // register serializer in JobRepositoryFactoryBean
* </pre>
*
* If the (de)serialization is only done by a trusted source, you can also enable
* default typing:
*
* <pre class="code">
* PolymorphicTypeValidator polymorphicTypeValidator = .. // configure your trusted PolymorphicTypeValidator
* ObjectMapper objectMapper = new ObjectMapper();
* objectMapper.activateDefaultTyping(polymorphicTypeValidator);
* Jackson2ExecutionContextStringSerializer serializer = new Jackson2ExecutionContextStringSerializer();
* serializer.setObjectMapper(objectMapper);
* // register serializer in JobRepositoryFactoryBean
* </pre>
*
* @author Marten Deinum
* @author Mahmoud Ben Hassine
@@ -55,7 +107,8 @@ public class Jackson2ExecutionContextStringSerializer implements ExecutionContex
this.objectMapper = new ObjectMapper();
this.objectMapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, false);
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
this.objectMapper.enableDefaultTyping();
this.objectMapper.configure(MapperFeature.BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES, true);
this.objectMapper.setDefaultTyping(createTrustedDefaultTyping());
this.objectMapper.registerModule(new JobParametersModule());
}
@@ -141,4 +194,158 @@ public class Jackson2ExecutionContextStringSerializer implements ExecutionContex
}
/**
* Creates a TypeResolverBuilder that checks if a type is trusted.
* @return a TypeResolverBuilder that checks if a type is trusted.
*/
private static TypeResolverBuilder<? extends TypeResolverBuilder> createTrustedDefaultTyping() {
TypeResolverBuilder<? extends TypeResolverBuilder> result = new TrustedTypeResolverBuilder(ObjectMapper.DefaultTyping.NON_FINAL);
result = result.init(JsonTypeInfo.Id.CLASS, null);
result = result.inclusion(JsonTypeInfo.As.PROPERTY);
return result;
}
/**
* An implementation of {@link ObjectMapper.DefaultTypeResolverBuilder}
* that inserts an {@code allow all} {@link PolymorphicTypeValidator}
* and overrides the {@code TypeIdResolver}
* @author Rob Winch
*/
static class TrustedTypeResolverBuilder extends ObjectMapper.DefaultTypeResolverBuilder {
TrustedTypeResolverBuilder(ObjectMapper.DefaultTyping defaultTyping) {
super(
defaultTyping,
//we do explicit validation in the TypeIdResolver
BasicPolymorphicTypeValidator.builder()
.allowIfSubType(Object.class)
.build()
);
}
@Override
protected TypeIdResolver idResolver(MapperConfig<?> config,
JavaType baseType,
PolymorphicTypeValidator subtypeValidator,
Collection<NamedType> subtypes, boolean forSer, boolean forDeser) {
TypeIdResolver result = super.idResolver(config, baseType, subtypeValidator, subtypes, forSer, forDeser);
return new TrustedTypeIdResolver(result);
}
}
/**
* A {@link TypeIdResolver} that delegates to an existing implementation and throws an IllegalStateException if the
* class being looked up is not trusted, does not provide an explicit mixin, and is not annotated with Jackson
* mappings.
*/
static class TrustedTypeIdResolver implements TypeIdResolver {
private static final Set<String> TRUSTED_CLASS_NAMES = Collections.unmodifiableSet(new HashSet(Arrays.asList(
"java.util.ArrayList",
"java.util.LinkedList",
"java.util.Collections$EmptyList",
"java.util.Collections$EmptyMap",
"java.util.Collections$EmptySet",
"java.util.Collections$UnmodifiableRandomAccessList",
"java.util.Collections$UnmodifiableList",
"java.util.Collections$UnmodifiableMap",
"java.util.Collections$UnmodifiableSet",
"java.util.Collections$SingletonList",
"java.util.Collections$SingletonMap",
"java.util.Collections$SingletonSet",
"java.util.Date",
"java.time.Instant",
"java.time.Duration",
"java.time.LocalDate",
"java.time.LocalTime",
"java.time.LocalDateTime",
"java.net.URL",
"java.util.TreeMap",
"java.util.HashMap",
"java.util.LinkedHashMap",
"java.util.TreeSet",
"java.util.HashSet",
"java.util.LinkedHashSet",
"java.lang.Boolean",
"java.lang.Byte",
"java.lang.Short",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Double",
"java.lang.Float",
"java.math.BigDecimal",
"java.math.BigInteger",
"java.lang.String",
"java.lang.Character",
"java.lang.CharSequence",
"java.util.Properties",
"[Ljava.util.Properties;",
"org.springframework.batch.core.JobParameter",
"org.springframework.batch.core.JobParameters",
"org.springframework.batch.core.jsr.partition.JsrPartitionHandler$PartitionPlanState"
)));
private final TypeIdResolver delegate;
TrustedTypeIdResolver(TypeIdResolver delegate) {
this.delegate = delegate;
}
@Override
public void init(JavaType baseType) {
delegate.init(baseType);
}
@Override
public String idFromValue(Object value) {
return delegate.idFromValue(value);
}
@Override
public String idFromValueAndType(Object value, Class<?> suggestedType) {
return delegate.idFromValueAndType(value, suggestedType);
}
@Override
public String idFromBaseType() {
return delegate.idFromBaseType();
}
@Override
public JavaType typeFromId(DatabindContext context, String id) throws IOException {
DeserializationConfig config = (DeserializationConfig) context.getConfig();
JavaType result = delegate.typeFromId(context, id);
String className = result.getRawClass().getName();
if (isTrusted(className)) {
return result;
}
boolean isExplicitMixin = config.findMixInClassFor(result.getRawClass()) != null;
if (isExplicitMixin) {
return result;
}
Class<?> rawClass = result.getRawClass();
JacksonAnnotation jacksonAnnotation = AnnotationUtils.findAnnotation(rawClass, JacksonAnnotation.class);
if (jacksonAnnotation != null) {
return result;
}
throw new IllegalArgumentException("The class with " + id + " and name of " + className + " is not trusted. " +
"If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or a custom ObjectMapper. " +
"If the serialization is only done by a trusted source, you can also enable default typing.");
}
private boolean isTrusted(String id) {
return TRUSTED_CLASS_NAMES.contains(id);
}
@Override
public String getDescForKnownTypeIds() {
return delegate.getDescForKnownTypeIds();
}
@Override
public JsonTypeInfo.Id getMechanism() {
return delegate.getMechanism();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2018 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
*/
package org.springframework.batch.core.repository.dao;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.junit.Test;
import org.springframework.batch.core.JobParameter;
import org.springframework.batch.core.JobParameters;
@@ -175,6 +176,7 @@ public abstract class AbstractExecutionContextSerializerTests {
protected abstract ExecutionContextSerializer getSerializer();
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public static class ComplexObject implements Serializable {
private static final long serialVersionUID = 1L;
private String name;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2008-2016 the original author or authors.
* Copyright 2008-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,11 +15,24 @@
*/
package org.springframework.batch.core.repository.dao;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.junit.Before;
import org.junit.Test;
import org.springframework.batch.core.repository.ExecutionContextSerializer;
import static org.junit.Assert.fail;
/**
* @author Marten Deinum
* @author Michael Minella
*/
public class Jackson2ExecutionContextStringSerializerTests extends AbstractExecutionContextSerializerTests {
@@ -32,8 +45,99 @@ public class Jackson2ExecutionContextStringSerializerTests extends AbstractExecu
serializer = serializerDeserializer;
}
@Test
public void mappedTypeTest() throws IOException {
Person person = new Person();
person.age = 28;
person.name = "Bob";
person.phone = new DomesticNumber();
person.phone.areaCode = 555;
person.phone.local = 1234567;
Jackson2ExecutionContextStringSerializer j = new Jackson2ExecutionContextStringSerializer();
Map<String, Object> context = new HashMap<>(1);
context.put("person", person);
ByteArrayOutputStream os = new ByteArrayOutputStream();
j.serialize(context, os);
InputStream in = new ByteArrayInputStream(os.toByteArray());
try {
j.deserialize(in);
}
catch (Exception e) {
fail(String.format("An exception was thrown but should not have been: %s", e.getMessage()));
}
}
@Override
protected ExecutionContextSerializer getSerializer() {
return this.serializer;
}
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public static class Person {
public String name;
public int age;
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public PhoneNumber phone;
}
public static abstract class PhoneNumber {
public int areaCode, local;
}
public static class InternationalNumber extends PhoneNumber {
public int countryCode;
}
public static class DomesticNumber extends PhoneNumber{}
@Test
public void unmappedTypeTest() throws IOException {
UnmappedPerson person = new UnmappedPerson();
person.age = 28;
person.name = "Bob";
person.phone = new UnmappedDomesticNumber();
person.phone.areaCode = 555;
person.phone.local = 1234567;
Jackson2ExecutionContextStringSerializer j = new Jackson2ExecutionContextStringSerializer();
Map<String, Object> context = new HashMap<>(1);
context.put("person", person);
ByteArrayOutputStream os = new ByteArrayOutputStream();
j.serialize(context, os);
InputStream in = new ByteArrayInputStream(os.toByteArray());
try {
j.deserialize(in);
fail("An exception should have been thrown but wasn't");
}
catch (Exception e) {
return;
}
}
public static class UnmappedPerson {
public String name;
public int age;
public UnmappedPhoneNumber phone;
}
public static abstract class UnmappedPhoneNumber {
public int areaCode, local;
}
public static class UnmappedInternationalNumber extends UnmappedPhoneNumber {
public int countryCode;
}
public static class UnmappedDomesticNumber extends UnmappedPhoneNumber{}
}