From 2650aef173018edee8da90218bb469ed925ff1f6 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Mon, 6 Jun 2016 11:25:50 -0400 Subject: [PATCH] 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. --- .../JsonConverterCompatibilityTests.java | 104 ++++++++++++++++++ .../json/JsonToObjectTransformer.java | 18 ++- .../json/ObjectToJsonTransformer.java | 24 +++- .../json/Jackson2JsonObjectMapper.java | 2 +- .../support/json/JsonObjectMapperAdapter.java | 9 +- .../json/ObjectToJsonTransformerTests.java | 33 ++++++ 6 files changed, 175 insertions(+), 15 deletions(-) create mode 100644 spring-integration-amqp/src/test/java/org/springframework/integration/amqp/support/JsonConverterCompatibilityTests.java diff --git a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/support/JsonConverterCompatibilityTests.java b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/support/JsonConverterCompatibilityTests.java new file mode 100644 index 0000000000..d302be6fea --- /dev/null +++ b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/support/JsonConverterCompatibilityTests.java @@ -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 out = (Message) new ObjectToJsonTransformer() + .transform(new GenericMessage(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() { + + @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; + } + + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonToObjectTransformer.java b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonToObjectTransformer.java index d95be7d9c3..4667a9d377 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonToObjectTransformer.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonToObjectTransformer.java @@ -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. + *

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. + *

The type headers can be classes or fully-qualified class names. * * @author Mark Fisher * @author Artem Bilan diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/ObjectToJsonTransformer.java b/spring-integration-core/src/main/java/org/springframework/integration/json/ObjectToJsonTransformer.java index 2717a870e6..996686c6d9 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/json/ObjectToJsonTransformer.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/ObjectToJsonTransformer.java @@ -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. + *

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. + *

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. + *

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 diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/Jackson2JsonObjectMapper.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/Jackson2JsonObjectMapper.java index c9ab470933..ef7e591ab9 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/json/Jackson2JsonObjectMapper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/Jackson2JsonObjectMapper.java @@ -107,7 +107,7 @@ public class Jackson2JsonObjectMapper extends AbstractJacksonJsonObjectMapper T fromJson(JsonParser parser, Type valueType) throws Exception { - return this.objectMapper.readValue(parser, this.constructType(valueType)); + return this.objectMapper.readValue(parser, constructType(valueType)); } @Override diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/JsonObjectMapperAdapter.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/JsonObjectMapperAdapter.java index e85544e597..fe3f8e705b 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/json/JsonObjectMapperAdapter.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/JsonObjectMapperAdapter.java @@ -66,11 +66,14 @@ public abstract class JsonObjectMapperAdapter implements JsonObjectMapper< public void populateJavaTypes(Map 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); } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/ObjectToJsonTransformerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/ObjectToJsonTransformerTests.java index 057422422c..f05afef62a 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/ObjectToJsonTransformerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/ObjectToJsonTransformerTests.java @@ -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 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 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 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 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());