Update Jackson2ExecutionContextStringSerializer
This commit is contained in:
@@ -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">
|
||||
* @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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user