INT-4045: Fix NPE in ObjectToJsonTransformer

JIRA: https://jira.spring.io/browse/INT-4045

Handle the situation when the first element of a container type (or map)
is null; set the generic type of the container to `Object`.

Also, clarify how the compatibility between the transformer and Spring AMQP
message converter is achieved.
This commit is contained in:
Gary Russell
2016-06-06 11:25:50 -04:00
parent c54d95b1aa
commit 2650aef173
6 changed files with 175 additions and 15 deletions

View File

@@ -19,17 +19,23 @@ package org.springframework.integration.json;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.integration.mapping.support.JsonHeaders;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.integration.support.json.JsonObjectMapperProvider;
import org.springframework.integration.support.json.JsonObjectMapper;
import org.springframework.integration.support.json.JsonObjectMapperProvider;
import org.springframework.integration.transformer.AbstractTransformer;
import org.springframework.messaging.Message;
/**
* Transformer implementation that converts a JSON string payload into an instance of the provided target Class.
* By default this transformer uses {@linkplain org.springframework.integration.support.json.JsonObjectMapperProvider} factory
* to get an instance of Jackson 1 or Jackson 2 JSON-processor {@linkplain JsonObjectMapper} implementation
* depending on the jackson-databind or jackson-mapper-asl libs on the classpath.
* Any other {@linkplain JsonObjectMapper} implementation can be provided.
* Transformer implementation that converts a JSON string payload into an instance of the
* provided target Class. By default this transformer uses
* {@linkplain org.springframework.integration.support.json.JsonObjectMapperProvider}
* factory to get an instance of Jackson 1 or Jackson 2 JSON-processor
* {@linkplain JsonObjectMapper} implementation depending on the jackson-databind or
* jackson-mapper-asl libs on the classpath. Any other {@linkplain JsonObjectMapper}
* implementation can be provided.
* <p>Since version 3.0, you can omit the target class and the target type can be
* determined by the {@link JsonHeaders} type entries - including the contents of a
* one-level container or map type.
* <p>The type headers can be classes or fully-qualified class names.
*
* @author Mark Fisher
* @author Artem Bilan

View File

@@ -27,11 +27,25 @@ import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.StringUtils;
/**
* Transformer implementation that converts a payload instance into a JSON string representation.
* By default this transformer uses {@linkplain org.springframework.integration.support.json.JsonObjectMapperProvider} factory
* to get an instance of a Jackson or Jackson 2 JSON-processor {@linkplain JsonObjectMapper} implementation
* depending on the jackson-databind or jackson-mapper-asl libs on the classpath.
* Any other {@linkplain JsonObjectMapper} implementation can be provided.
* Transformer implementation that converts a payload instance into a JSON string
* representation. By default this transformer uses
* {@linkplain org.springframework.integration.support.json.JsonObjectMapperProvider}
* factory to get an instance of a Jackson or Jackson 2 JSON-processor
* {@linkplain JsonObjectMapper} implementation depending on the jackson-databind or
* jackson-mapper-asl libs on the classpath. Any other {@linkplain JsonObjectMapper}
* implementation can be provided.
* <p>Since version 3.0, adds headers to represent the object types that were mapped
* from (including one level of container and Map content types). These headers
* are compatible with the Spring AMQP Json type mapper such that messages mapped
* or converted by either technology are compatible. One difference, however, is the
* Spring AMQP converter, when converting to JSON, sets the header types to the class
* name. This transformer sets the header types to the class itself.
* <p>The compatibility is achieved because, when mapping the Spring Integration
* message in the outbound endpoint (via the {@code DefaultAmqpHeaderMapper}), the
* classes are converted to String at that time.
* <p>Note: the first element of container/map types are used to determine the
* container/map content types. If the first element is null, the type is set to
* {@link Object}.
*
* @author Mark Fisher
* @author James Carr

View File

@@ -107,7 +107,7 @@ public class Jackson2JsonObjectMapper extends AbstractJacksonJsonObjectMapper<Js
@Override
public <T> T fromJson(JsonParser parser, Type valueType) throws Exception {
return this.objectMapper.readValue(parser, this.constructType(valueType));
return this.objectMapper.readValue(parser, constructType(valueType));
}
@Override

View File

@@ -66,11 +66,14 @@ public abstract class JsonObjectMapperAdapter<N, P> implements JsonObjectMapper<
public void populateJavaTypes(Map<String, Object> map, Object object) {
map.put(JsonHeaders.TYPE_ID, object.getClass());
if (object instanceof Collection && !((Collection<?>) object).isEmpty()) {
map.put(JsonHeaders.CONTENT_TYPE_ID, ((Collection<?>) object).iterator().next().getClass());
Object firstElement = ((Collection<?>) object).iterator().next();
map.put(JsonHeaders.CONTENT_TYPE_ID, firstElement != null ? firstElement.getClass() : Object.class);
}
if (object instanceof Map && !((Map<?, ?>) object).isEmpty()) {
map.put(JsonHeaders.CONTENT_TYPE_ID, ((Map<?, ?>) object).values().iterator().next().getClass());
map.put(JsonHeaders.KEY_TYPE_ID, ((Map<?, ?>) object).keySet().iterator().next().getClass());
Object firstValue = ((Map<?, ?>) object).values().iterator().next();
map.put(JsonHeaders.CONTENT_TYPE_ID, firstValue != null ? firstValue.getClass() : Object.class);
Object firstKey = ((Map<?, ?>) object).keySet().iterator().next();
map.put(JsonHeaders.KEY_TYPE_ID, firstKey != null ? firstKey.getClass() : Object.class);
}
}

View File

@@ -16,12 +16,16 @@
package org.springframework.integration.json;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -32,6 +36,7 @@ import org.springframework.context.expression.MapAccessor;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.mapping.support.JsonHeaders;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.support.json.BoonJsonObjectMapper;
import org.springframework.integration.support.json.Jackson2JsonObjectMapper;
@@ -150,6 +155,34 @@ public class ObjectToJsonTransformerTests {
assertTrue(addressResult.contains("street:\"Main Street\""));
}
@Test
public void collectionOrMap() {
ObjectToJsonTransformer transformer = new ObjectToJsonTransformer();
List<String> list = Collections.singletonList("foo");
Message<?> out = transformer.transform(new GenericMessage<>(list));
assertThat(out.getHeaders().get(JsonHeaders.TYPE_ID).toString(), containsString("SingletonList"));
assertThat(out.getHeaders().get(JsonHeaders.CONTENT_TYPE_ID), equalTo(String.class));
Map<String, Long> map = Collections.singletonMap("foo", 1L);
out = transformer.transform(new GenericMessage<>(map));
assertThat(out.getHeaders().get(JsonHeaders.TYPE_ID).toString(), containsString("SingletonMap"));
assertThat(out.getHeaders().get(JsonHeaders.CONTENT_TYPE_ID), equalTo(Long.class));
assertThat(out.getHeaders().get(JsonHeaders.KEY_TYPE_ID), equalTo(String.class));
}
@Test
public void collectionOrMapWithNullFirstElement() {
ObjectToJsonTransformer transformer = new ObjectToJsonTransformer();
List<String> list = Collections.singletonList(null);
Message<?> out = transformer.transform(new GenericMessage<>(list));
assertThat(out.getHeaders().get(JsonHeaders.TYPE_ID).toString(), containsString("SingletonList"));
assertThat(out.getHeaders().get(JsonHeaders.CONTENT_TYPE_ID), equalTo(Object.class));
Map<String, String> map = Collections.singletonMap("foo", null);
out = transformer.transform(new GenericMessage<>(map));
assertThat(out.getHeaders().get(JsonHeaders.TYPE_ID).toString(), containsString("SingletonMap"));
assertThat(out.getHeaders().get(JsonHeaders.CONTENT_TYPE_ID), equalTo(Object.class));
assertThat(out.getHeaders().get(JsonHeaders.KEY_TYPE_ID), equalTo(String.class));
}
@Test
public void testBoonJsonObjectMapper() throws Exception {
ObjectToJsonTransformer transformer = new ObjectToJsonTransformer(new BoonJsonObjectMapper());