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:
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2016 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.integration.amqp.support;
|
||||
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.amqp.core.MessageProperties;
|
||||
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
|
||||
import org.springframework.amqp.rabbit.core.ChannelCallback;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter;
|
||||
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
|
||||
import org.springframework.integration.amqp.rule.BrokerRunning;
|
||||
import org.springframework.integration.json.ObjectToJsonTransformer;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import com.rabbitmq.client.AMQP.BasicProperties;
|
||||
import com.rabbitmq.client.Channel;
|
||||
|
||||
/**
|
||||
* @author Gary Russell
|
||||
* @since 4.3
|
||||
*
|
||||
*/
|
||||
public class JsonConverterCompatibilityTests {
|
||||
|
||||
private static final String JSON_TESTQ = "si.json.tests";
|
||||
|
||||
@Rule
|
||||
public BrokerRunning brokerRunning = BrokerRunning.isRunningWithEmptyQueues(JSON_TESTQ);
|
||||
|
||||
private RabbitTemplate rabbitTemplate;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.rabbitTemplate = new RabbitTemplate(new CachingConnectionFactory("localhost"));
|
||||
this.rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
this.brokerRunning.removeTestQueues();
|
||||
((CachingConnectionFactory) this.rabbitTemplate.getConnectionFactory()).destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInbound() {
|
||||
@SuppressWarnings("unchecked")
|
||||
final Message<String> out = (Message<String>) new ObjectToJsonTransformer()
|
||||
.transform(new GenericMessage<Foo>(new Foo()));
|
||||
MessageProperties messageProperties = new MessageProperties();
|
||||
DefaultAmqpHeaderMapper.outboundMapper().fromHeadersToRequest(out.getHeaders(), messageProperties);
|
||||
final BasicProperties props = new DefaultMessagePropertiesConverter().fromMessageProperties(messageProperties,
|
||||
"UTF-8");
|
||||
this.rabbitTemplate.execute(new ChannelCallback<Void>() {
|
||||
|
||||
@Override
|
||||
public Void doInRabbit(Channel channel) throws Exception {
|
||||
channel.basicPublish("", JSON_TESTQ, props, out.getPayload().getBytes());
|
||||
return null;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Object received = this.rabbitTemplate.receiveAndConvert(JSON_TESTQ);
|
||||
assertThat(received, instanceOf(Foo.class));
|
||||
}
|
||||
|
||||
public static class Foo {
|
||||
|
||||
private String bar = "bar";
|
||||
|
||||
public String getBar() {
|
||||
return this.bar;
|
||||
}
|
||||
|
||||
public void setBar(String bar) {
|
||||
this.bar = bar;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user