From 4dd95c41ea08141cb441a7e22e9db3c24783d943 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 11 Oct 2013 17:18:01 +0300 Subject: [PATCH] INT-2809: Add/Extract JSON JavaType Headers * Add `AmqpHeaders` headers * Add `JavaType` headers mapping to `DefaultAmqpHeaderMapper` * Add ability to Json Transformers to add/extract `JavaType` headers * Make `JsonToObjectTransformer#targetClass` as non-required and do fallback to extract type from `JavaType` headers * Add `JavaType` extraction in the `JsonObjectMapper` implementors JIRA: https://jira.springsource.org/browse/INT-2809 INT-2809: Polishing and refactoring * Introduce `JsonHeaders`, `AbstractJacksonJsonObjectMapper` * Move `TestPerson` and `TestAddress` to package level * Now `JsonToObjectTransformer` supports not only `String` payload * Remove json headers after transformation in the `JsonToObjectTransformer` INT-2809 Minor Polishing INT-2809 Docs and What's New --- .../amqp/support/DefaultAmqpHeaderMapper.java | 27 +++ .../amqp/inbound/InboundEndpointTests.java | 168 ++++++++++++++++++ .../xml/JsonToObjectTransformerParser.java | 6 +- .../integration/json/JsonHeaders.java | 43 +++++ .../json/JsonToObjectTransformer.java | 45 +++-- .../json/ObjectToJsonTransformer.java | 3 + .../mapping/AbstractHeaderMapper.java | 11 +- .../AbstractJacksonJsonMessageParser.java | 4 +- .../json/AbstractJacksonJsonObjectMapper.java | 84 +++++++++ .../json/Jackson2JsonObjectMapper.java | 84 +++++++-- .../support/json/JacksonJsonObjectMapper.java | 84 +++++++-- .../support/json/JsonObjectMapper.java | 7 +- .../support/json/JsonObjectMapperAdapter.java | 18 +- ...ToObjectTransformerParserTests-context.xml | 6 +- .../JsonToObjectTransformerParserTests.java | 77 +------- .../json/JsonToObjectTransformerTests.java | 92 ++-------- .../JsonTransformersSymmetricalTests.java | 47 +++++ .../ObjectToJsonTransformerParserTests.java | 81 --------- .../json/ObjectToJsonTransformerTests.java | 62 ------- .../integration/json/TestAddress.java | 80 +++++++++ .../integration/json/TestPerson.java | 105 +++++++++++ src/reference/docbook/transformer.xml | 106 +++++------ src/reference/docbook/whats-new.xml | 18 +- 23 files changed, 845 insertions(+), 413 deletions(-) create mode 100644 spring-integration-amqp/src/test/java/org/springframework/integration/amqp/inbound/InboundEndpointTests.java create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/json/JsonHeaders.java create mode 100644 spring-integration-core/src/main/java/org/springframework/integration/support/json/AbstractJacksonJsonObjectMapper.java create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/json/JsonTransformersSymmetricalTests.java create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/json/TestAddress.java create mode 100644 spring-integration-core/src/test/java/org/springframework/integration/json/TestPerson.java diff --git a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.java b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.java index 781bdd1d0e..ad6dc4c846 100644 --- a/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.java +++ b/spring-integration-amqp/src/main/java/org/springframework/integration/amqp/support/DefaultAmqpHeaderMapper.java @@ -26,6 +26,7 @@ import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.amqp.core.MessageProperties; import org.springframework.integration.MessageHeaders; import org.springframework.integration.amqp.AmqpHeaders; +import org.springframework.integration.json.JsonHeaders; import org.springframework.integration.mapping.AbstractHeaderMapper; import org.springframework.util.StringUtils; @@ -45,6 +46,8 @@ import org.springframework.util.StringUtils; * * @author Mark Fisher * @author Oleg Zhurakousky + * @author Gary Russell + * @author Artem Bilan * @since 2.1 */ public class DefaultAmqpHeaderMapper extends AbstractHeaderMapper implements AmqpHeaderMapper { @@ -70,6 +73,9 @@ public class DefaultAmqpHeaderMapper extends AbstractHeaderMapper headers = amqpMessageProperties.getHeaders(); headers.remove(AmqpHeaders.STACKED_CORRELATION_HEADER); headers.remove(AmqpHeaders.STACKED_REPLY_TO_HEADER); + return headers; } @@ -272,6 +287,18 @@ public class DefaultAmqpHeaderMapper extends AbstractHeaderMapper) { + value = ((Class) value).getName(); + } + amqpMessageProperties.setHeader(jsonHeader.replaceFirst(JsonHeaders.PREFIX, ""), value.toString()); + } + } + String replyCorrelation = getHeaderIfAvailable(headers, AmqpHeaders.SPRING_REPLY_CORRELATION, String.class); if (StringUtils.hasLength(replyCorrelation)) { amqpMessageProperties.setHeader("spring_reply_correlation", replyCorrelation); diff --git a/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/inbound/InboundEndpointTests.java b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/inbound/InboundEndpointTests.java new file mode 100644 index 0000000000..cfa0eb620a --- /dev/null +++ b/spring-integration-amqp/src/test/java/org/springframework/integration/amqp/inbound/InboundEndpointTests.java @@ -0,0 +1,168 @@ +/* + * Copyright 2013 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.inbound; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import org.springframework.amqp.core.MessageListener; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.connection.Connection; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; +import org.springframework.amqp.support.converter.JsonMessageConverter; +import org.springframework.amqp.support.converter.SimpleMessageConverter; +import org.springframework.integration.Message; +import org.springframework.integration.amqp.support.DefaultAmqpHeaderMapper; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.core.PollableChannel; +import org.springframework.integration.json.JsonToObjectTransformer; +import org.springframework.integration.json.ObjectToJsonTransformer; +import org.springframework.integration.message.GenericMessage; +import org.springframework.integration.transformer.Transformer; + +import com.rabbitmq.client.Channel; + +/** + * @author Artem Bilan + * @since 3.0 + */ +public class InboundEndpointTests { + + @Test + public void testInt2809JavaTypePropertiesToAmqp() { + Connection connection = mock(Connection.class); + doAnswer(new Answer() { + public Channel answer(InvocationOnMock invocation) throws Throwable { + return mock(Channel.class); + } + }).when(connection).createChannel(anyBoolean()); + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + when(connectionFactory.createConnection()).thenReturn(connection); + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + + AmqpInboundChannelAdapter adapter = new AmqpInboundChannelAdapter(container); + adapter.setMessageConverter(new JsonMessageConverter()); + + PollableChannel channel = new QueueChannel(); + + adapter.setOutputChannel(channel); + adapter.afterPropertiesSet(); + + Object payload = new Foo("bar1"); + + Transformer objectToJsonTransformer = new ObjectToJsonTransformer(); + Message jsonMessage = objectToJsonTransformer.transform(new GenericMessage(payload)); + + MessageProperties amqpMessageProperties = new MessageProperties(); + org.springframework.amqp.core.Message amqpMessage = + new SimpleMessageConverter().toMessage(jsonMessage.getPayload(), amqpMessageProperties); + new DefaultAmqpHeaderMapper().fromHeadersToRequest(jsonMessage.getHeaders(), amqpMessageProperties); + + MessageListener listener = (MessageListener) container.getMessageListener(); + listener.onMessage(amqpMessage); + + Message result = channel.receive(1000); + assertEquals(payload, result.getPayload()); + } + + @Test + public void testInt2809JavaTypePropertiesFromAmqp() { + Connection connection = mock(Connection.class); + doAnswer(new Answer() { + public Channel answer(InvocationOnMock invocation) throws Throwable { + return mock(Channel.class); + } + }).when(connection).createChannel(anyBoolean()); + ConnectionFactory connectionFactory = mock(ConnectionFactory.class); + when(connectionFactory.createConnection()).thenReturn(connection); + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + + AmqpInboundChannelAdapter adapter = new AmqpInboundChannelAdapter(container); + + PollableChannel channel = new QueueChannel(); + + adapter.setOutputChannel(channel); + adapter.afterPropertiesSet(); + + Object payload = new Foo("bar1"); + + MessageProperties amqpMessageProperties = new MessageProperties(); + org.springframework.amqp.core.Message amqpMessage = new JsonMessageConverter().toMessage(payload, amqpMessageProperties); + + MessageListener listener = (MessageListener) container.getMessageListener(); + listener.onMessage(amqpMessage); + + Message receive = channel.receive(1000); + + Message result = new JsonToObjectTransformer().transform(receive); + + assertEquals(payload, result.getPayload()); + } + + + + public static class Foo { + + private String bar; + + public Foo() { + } + + public Foo(String bar) { + this.bar = bar; + } + + public String getBar() { + return bar; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Foo foo = (Foo) o; + + if (bar != null ? !bar.equals(foo.bar) : foo.bar != null) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return bar != null ? bar.hashCode() : 0; + } + + } + +} diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/JsonToObjectTransformerParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/JsonToObjectTransformerParser.java index 588432554d..bb8c856836 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/JsonToObjectTransformerParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/JsonToObjectTransformerParser.java @@ -16,12 +16,12 @@ package org.springframework.integration.config.xml; -import org.springframework.util.StringUtils; import org.w3c.dom.Element; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.integration.json.JsonToObjectTransformer; +import org.springframework.util.StringUtils; /** * @author Mark Fisher @@ -39,7 +39,9 @@ public class JsonToObjectTransformerParser extends AbstractTransformerParser { protected void parseTransformer(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { String type = element.getAttribute("type"); String objectMapper = element.getAttribute("object-mapper"); - builder.addConstructorArgValue(type); + if (StringUtils.hasText(type)) { + builder.addConstructorArgValue(type); + } if (StringUtils.hasText(objectMapper)) { builder.addConstructorArgReference(objectMapper); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/json/JsonHeaders.java b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonHeaders.java new file mode 100644 index 0000000000..5381bdf472 --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/json/JsonHeaders.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013 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.json; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +/** + * Pre-defined names and prefixes to be used for setting and/or retrieving JSON + * entries from/to Message Headers and other adapter, e.g. AMQP. + * + * @author Artem Bilan + * @since 3.0 + */ +public class JsonHeaders { + + public static final String PREFIX = "json"; + + public static final String TYPE_ID = PREFIX + "__TypeId__"; + + public static final String CONTENT_TYPE_ID = PREFIX + "__ContentTypeId__"; + + public static final String KEY_TYPE_ID = PREFIX + "__KeyTypeId__"; + + public static final Collection HEADERS = + Collections.unmodifiableList(Arrays.asList(TYPE_ID, CONTENT_TYPE_ID, KEY_TYPE_ID)); + +} 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 6bbf10ec2f..92d895ef5a 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 @@ -16,10 +16,13 @@ package org.springframework.integration.json; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.integration.Message; +import org.springframework.integration.support.MessageBuilder; import org.springframework.integration.support.json.JacksonJsonObjectMapper; import org.springframework.integration.support.json.JacksonJsonObjectMapperProvider; import org.springframework.integration.support.json.JsonObjectMapper; -import org.springframework.integration.transformer.AbstractPayloadTransformer; +import org.springframework.integration.transformer.AbstractTransformer; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -36,13 +39,17 @@ import org.springframework.util.ClassUtils; * @see JacksonJsonObjectMapperProvider * @since 2.0 */ -public class JsonToObjectTransformer extends AbstractPayloadTransformer { +public class JsonToObjectTransformer extends AbstractTransformer implements BeanClassLoaderAware { - private final Class targetClass; + private final Class targetClass; private final JsonObjectMapper jsonObjectMapper; - public JsonToObjectTransformer(Class targetClass) { + public JsonToObjectTransformer() { + this((Class) null); + } + + public JsonToObjectTransformer(Class targetClass) { this(targetClass, null); } @@ -52,8 +59,7 @@ public class JsonToObjectTransformer extends AbstractPayloadTransformer targetClass, Object objectMapper) throws ClassNotFoundException { - Assert.notNull(targetClass, "targetClass must not be null"); + public JsonToObjectTransformer(Class targetClass, Object objectMapper) throws ClassNotFoundException { this.targetClass = targetClass; if (objectMapper != null) { try { @@ -70,15 +76,34 @@ public class JsonToObjectTransformer extends AbstractPayloadTransformer targetClass, JsonObjectMapper jsonObjectMapper) { - Assert.notNull(targetClass, "targetClass must not be null"); + public JsonToObjectTransformer(JsonObjectMapper jsonObjectMapper) { + this(null, jsonObjectMapper); + } + + public JsonToObjectTransformer(Class targetClass, JsonObjectMapper jsonObjectMapper) { this.targetClass = targetClass; this.jsonObjectMapper = (jsonObjectMapper != null) ? jsonObjectMapper : JacksonJsonObjectMapperProvider.newInstance(); } @Override - protected T transformPayload(String payload) throws Exception { - return this.jsonObjectMapper.fromJson(payload, this.targetClass); + public void setBeanClassLoader(ClassLoader classLoader) { + if (this.jsonObjectMapper instanceof BeanClassLoaderAware) { + ((BeanClassLoaderAware) this.jsonObjectMapper).setBeanClassLoader(classLoader); + } + } + + @Override + protected Object doTransform(Message message) throws Exception { + if (this.targetClass != null) { + return this.jsonObjectMapper.fromJson(message.getPayload(), this.targetClass); + } + else { + Object result = this.jsonObjectMapper.fromJson(message.getPayload(), message.getHeaders()); + MessageBuilder messageBuilder = MessageBuilder.withPayload(result) + .copyHeaders(message.getHeaders()) + .removeHeaders(JsonHeaders.HEADERS.toArray(new String[3])); + return messageBuilder.build(); + } } } 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 402d64fb2a..1b3cce2e98 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 @@ -109,6 +109,9 @@ public class ObjectToJsonTransformer extends AbstractTransformer { else if (StringUtils.hasLength(this.contentType)) { headers.put(MessageHeaders.CONTENT_TYPE, this.contentType); } + + this.jsonObjectMapper.populateJavaTypes(headers, message.getPayload().getClass()); + messageBuilder.copyHeaders(headers); return messageBuilder.build(); } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java b/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java index de3cc0326d..954f7cb6d3 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/mapping/AbstractHeaderMapper.java @@ -26,7 +26,9 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.integration.MessageHeaders; +import org.springframework.integration.json.JsonHeaders; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -248,7 +250,7 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe if (!type.isAssignableFrom(value.getClass())) { if (logger.isWarnEnabled()) { logger.warn("skipping header '" + name + "' since it is not of expected type [" + type + "], it is [" + - value.getClass() + "]"); + value.getClass() + "]"); } return null; } @@ -271,9 +273,14 @@ public abstract class AbstractHeaderMapper implements RequestReplyHeaderMappe */ private String addPrefixIfNecessary(String prefix, String propertyName) { String headerName = propertyName; - if (StringUtils.hasText(prefix) && !headerName.startsWith(prefix) && !headerName.equals(MessageHeaders.CONTENT_TYPE)) { + if (StringUtils.hasText(prefix) && !headerName.startsWith(prefix) && + !headerName.equals(MessageHeaders.CONTENT_TYPE) && + (!JsonHeaders.HEADERS.contains(headerName) || !JsonHeaders.HEADERS.contains(JsonHeaders.PREFIX + headerName))) { headerName = prefix + propertyName; } + if (JsonHeaders.HEADERS.contains(JsonHeaders.PREFIX + headerName)) { + headerName = JsonHeaders.PREFIX + headerName; + } return headerName; } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/AbstractJacksonJsonMessageParser.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/AbstractJacksonJsonMessageParser.java index e291e95f4e..1b6d6bfe36 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/json/AbstractJacksonJsonMessageParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/AbstractJacksonJsonMessageParser.java @@ -16,6 +16,8 @@ package org.springframework.integration.support.json; +import java.lang.reflect.Type; + import org.springframework.integration.Message; import org.springframework.integration.support.MessageBuilder; @@ -68,7 +70,7 @@ abstract class AbstractJacksonJsonMessageParser

implements JsonInboundMessage Class headerType = this.messageMapper.getHeaderTypes().containsKey(headerName) ? this.messageMapper.getHeaderTypes().get(headerName) : Object.class; try { - return this.objectMapper.fromJson(parser, headerType); + return this.objectMapper.fromJson(parser, (Type) headerType); } catch (Exception e) { throw new IllegalArgumentException("Mapping header '" + headerName + "' of JSON message '" + diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/AbstractJacksonJsonObjectMapper.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/AbstractJacksonJsonObjectMapper.java new file mode 100644 index 0000000000..5f2ff9b87b --- /dev/null +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/AbstractJacksonJsonObjectMapper.java @@ -0,0 +1,84 @@ +/* + * Copyright 2013 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.support.json; + +import java.io.File; +import java.io.InputStream; +import java.io.Reader; +import java.lang.reflect.Type; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.util.ClassUtils; + +/** + * Base class for Jackson {@link JsonObjectMapper} implementations. + * + * @author Artem Bilan + * @since 3.0 + */ +public abstract class AbstractJacksonJsonObjectMapper implements JsonObjectMapper

, BeanClassLoaderAware { + + protected static final Collection> supportedJsonTypes = + Arrays.> asList(String.class, byte[].class, File.class, URL.class, InputStream.class, Reader.class); + + private volatile ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public T fromJson(Object json, Class valueType) throws Exception { + return this.fromJson(json, this.constructType(valueType)); + } + + @Override + public T fromJson(Object json, Map javaTypes) throws Exception { + J javaType = this.extractJavaType(javaTypes); + return this.fromJson(json, javaType); + } + + protected J createJavaType(Map javaTypes, String javaTypeKey) throws Exception { + Object classValue = javaTypes.get(javaTypeKey); + if (classValue == null) { + throw new IllegalArgumentException("Could not resolve '" + javaTypeKey + "' in 'javaTypes'."); + } + else { + Class aClass = null; + if (classValue instanceof Class) { + aClass = (Class) classValue; + } + else { + aClass = ClassUtils.forName(classValue.toString(), this.classLoader); + } + + return this.constructType(aClass); + } + } + + protected abstract T fromJson(Object json, J type) throws Exception; + + protected abstract J extractJavaType(Map javaTypes) throws Exception; + + protected abstract J constructType(Type type); + +} 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 14bcb1d32b..0d802e5bc4 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2013 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. @@ -16,15 +16,22 @@ package org.springframework.integration.support.json; +import java.io.File; +import java.io.InputStream; import java.io.Reader; import java.io.Writer; import java.lang.reflect.Type; +import java.net.URL; +import java.util.Collection; +import java.util.Map; + +import org.springframework.integration.json.JsonHeaders; +import org.springframework.util.Assert; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.util.Assert; - /** * Jackson 2 JSON-processor (@link https://github.com/FasterXML) {@linkplain JsonObjectMapper} implementation. * Delegates toJson and fromJson @@ -33,7 +40,7 @@ import org.springframework.util.Assert; * @author Artem Bilan * @since 3.0 */ -public class Jackson2JsonObjectMapper implements JsonObjectMapper { +public class Jackson2JsonObjectMapper extends AbstractJacksonJsonObjectMapper { private final ObjectMapper objectMapper; @@ -46,6 +53,7 @@ public class Jackson2JsonObjectMapper implements JsonObjectMapper { this.objectMapper = objectMapper; } + @Override public String toJson(Object value) throws Exception { return this.objectMapper.writeValueAsString(value); } @@ -55,18 +63,72 @@ public class Jackson2JsonObjectMapper implements JsonObjectMapper { this.objectMapper.writeValue(writer, value); } - public T fromJson(String json, Class valueType) throws Exception { - return this.objectMapper.readValue(json, valueType); - } - @Override - public T fromJson(Reader json, Class valueType) throws Exception { - return this.objectMapper.readValue(json, valueType); + protected T fromJson(Object json, JavaType type) throws Exception { + if (json instanceof String) { + return this.objectMapper.readValue((String) json, type); + } + else if(json instanceof byte[]) { + return this.objectMapper.readValue((byte[]) json, type); + } + else if (json instanceof File) { + return this.objectMapper.readValue((File) json, type); + } + else if (json instanceof URL) { + return this.objectMapper.readValue((URL) json, type); + } + else if (json instanceof InputStream) { + return this.objectMapper.readValue((InputStream) json, type); + } + else if (json instanceof Reader) { + return this.objectMapper.readValue((Reader) json, type); + } + else { + throw new IllegalArgumentException("'json' argument must be an instance of: " + supportedJsonTypes); + } } @Override public T fromJson(JsonParser parser, Type valueType) throws Exception { - return this.objectMapper.readValue(parser, this.objectMapper.constructType(valueType)); + return this.objectMapper.readValue(parser, this.constructType(valueType)); + } + + @Override + public void populateJavaTypes(Map map, Class sourceClass) { + JavaType javaType = this.objectMapper.constructType(sourceClass); + map.put(JsonHeaders.TYPE_ID, javaType.getRawClass()); + + if (javaType.isContainerType() && !javaType.isArrayType()) { + map.put(JsonHeaders.CONTENT_TYPE_ID, javaType.getContentType().getRawClass()); + } + + if (javaType.getKeyType() != null) { + map.put(JsonHeaders.KEY_TYPE_ID, javaType.getKeyType().getRawClass()); + } + } + + @Override + @SuppressWarnings({ "unchecked" }) + protected JavaType extractJavaType(Map javaTypes) throws Exception { + JavaType classType = this.createJavaType(javaTypes, JsonHeaders.TYPE_ID); + if (!classType.isContainerType() || classType.isArrayType()) { + return classType; + } + + JavaType contentClassType = this.createJavaType(javaTypes, JsonHeaders.CONTENT_TYPE_ID); + if (classType.getKeyType() == null) { + return this.objectMapper.getTypeFactory() + .constructCollectionType((Class>) classType.getRawClass(), contentClassType); + } + + JavaType keyClassType = this.createJavaType(javaTypes, JsonHeaders.KEY_TYPE_ID); + return this.objectMapper.getTypeFactory() + .constructMapType((Class>) classType.getRawClass(), keyClassType, contentClassType); + } + + @Override + protected JavaType constructType(Type type) { + return this.objectMapper.constructType(type); } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonObjectMapper.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonObjectMapper.java index 7b3ded0ac9..5297a2bb33 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonObjectMapper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/JacksonJsonObjectMapper.java @@ -16,13 +16,20 @@ package org.springframework.integration.support.json; +import java.io.File; +import java.io.InputStream; import java.io.Reader; import java.io.Writer; import java.lang.reflect.Type; +import java.net.URL; +import java.util.Collection; +import java.util.Map; import org.codehaus.jackson.JsonParser; import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.type.JavaType; +import org.springframework.integration.json.JsonHeaders; import org.springframework.util.Assert; /** @@ -33,7 +40,7 @@ import org.springframework.util.Assert; * @author Artem Bilan * @since 3.0 */ -public class JacksonJsonObjectMapper implements JsonObjectMapper { +public class JacksonJsonObjectMapper extends AbstractJacksonJsonObjectMapper { private final ObjectMapper objectMapper; @@ -46,6 +53,7 @@ public class JacksonJsonObjectMapper implements JsonObjectMapper { this.objectMapper = objectMapper; } + @Override public String toJson(Object value) throws Exception { return this.objectMapper.writeValueAsString(value); } @@ -55,18 +63,72 @@ public class JacksonJsonObjectMapper implements JsonObjectMapper { this.objectMapper.writeValue(writer, value); } - public T fromJson(String json, Class valueType) throws Exception { - return this.objectMapper.readValue(json, valueType); - } - - @Override - public T fromJson(Reader json, Class valueType) throws Exception { - return this.objectMapper.readValue(json, valueType); - } - @Override public T fromJson(JsonParser parser, Type valueType) throws Exception { - return this.objectMapper.readValue(parser, this.objectMapper.constructType(valueType)); + return this.objectMapper.readValue(parser, this.constructType(valueType)); + } + + @Override + protected T fromJson(Object json, JavaType type) throws Exception { + if (json instanceof String) { + return this.objectMapper.readValue((String) json, type); + } + else if(json instanceof byte[]) { + return this.objectMapper.readValue((byte[]) json, type); + } + else if (json instanceof File) { + return this.objectMapper.readValue((File) json, type); + } + else if (json instanceof URL) { + return this.objectMapper.readValue((URL) json, type); + } + else if (json instanceof InputStream) { + return this.objectMapper.readValue((InputStream) json, type); + } + else if (json instanceof Reader) { + return this.objectMapper.readValue((Reader) json, type); + } + else { + throw new IllegalArgumentException("'json' argument must be an instance of: " + supportedJsonTypes); + } + } + + @Override + public void populateJavaTypes(Map map, Class sourceClass) { + JavaType javaType = this.constructType(sourceClass); + map.put(JsonHeaders.TYPE_ID, javaType.getRawClass()); + + if (javaType.isContainerType() && !javaType.isArrayType()) { + map.put(JsonHeaders.CONTENT_TYPE_ID, javaType.getContentType().getRawClass()); + } + + if (javaType.getKeyType() != null) { + map.put(JsonHeaders.KEY_TYPE_ID, javaType.getKeyType().getRawClass()); + } + } + + @Override + protected JavaType constructType(Type type) { + return this.objectMapper.constructType(type); + } + + @Override + @SuppressWarnings({ "unchecked" }) + protected JavaType extractJavaType(Map javaTypes) throws Exception { + JavaType classType = this.createJavaType(javaTypes, JsonHeaders.TYPE_ID); + if (!classType.isContainerType() || classType.isArrayType()) { + return classType; + } + + JavaType contentClassType = this.createJavaType(javaTypes, JsonHeaders.CONTENT_TYPE_ID); + if (classType.getKeyType() == null) { + return this.objectMapper.getTypeFactory() + .constructCollectionType((Class>) classType.getRawClass(), contentClassType); + } + + JavaType keyClassType = this.createJavaType(javaTypes, JsonHeaders.KEY_TYPE_ID); + return this.objectMapper.getTypeFactory() + .constructMapType((Class>) classType.getRawClass(), keyClassType, contentClassType); } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/support/json/JsonObjectMapper.java b/spring-integration-core/src/main/java/org/springframework/integration/support/json/JsonObjectMapper.java index 682f7c0720..70b3c8c35e 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/support/json/JsonObjectMapper.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/support/json/JsonObjectMapper.java @@ -16,9 +16,9 @@ package org.springframework.integration.support.json; -import java.io.Reader; import java.io.Writer; import java.lang.reflect.Type; +import java.util.Map; /** * Strategy interface to convert an Object to/from the JSON representation. @@ -33,10 +33,11 @@ public interface JsonObjectMapper

{ void toJson(Object value, Writer writer) throws Exception; - T fromJson(String json, Class valueType) throws Exception; + T fromJson(Object json, Class valueType) throws Exception; - T fromJson(Reader json, Class valueType) throws Exception; + T fromJson(Object json, Map javaTypes) throws Exception; T fromJson(P parser, Type valueType) throws Exception; + void populateJavaTypes(Map map, Class sourceClass); } 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 8f7f1282f1..e5feaeec83 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 @@ -16,9 +16,9 @@ package org.springframework.integration.support.json; -import java.io.Reader; import java.io.Writer; import java.lang.reflect.Type; +import java.util.Map; /** * Simple {@linkplain JsonObjectMapper} adapter implementation, if there is no need @@ -39,12 +39,7 @@ public abstract class JsonObjectMapperAdapter

implements JsonObjectMapper

} @Override - public T fromJson(String json, Class valueType) throws Exception { - return null; - } - - @Override - public T fromJson(Reader json, Class valueType) throws Exception { + public T fromJson(Object json, Class valueType) throws Exception { return null; } @@ -53,4 +48,13 @@ public abstract class JsonObjectMapperAdapter

implements JsonObjectMapper

return null; } + @Override + public T fromJson(Object json, Map javaTypes) throws Exception { + return null; + } + + @Override + public void populateJavaTypes(Map map, Class sourceClass) { + } + } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests-context.xml index 7034732677..6df09b437e 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests-context.xml @@ -8,16 +8,16 @@ http://www.springframework.org/schema/integration/spring-integration.xsd"> + type="org.springframework.integration.json.TestPerson"/> diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests.java index e8bbc6c9b0..2359b05abc 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerParserTests.java @@ -134,79 +134,6 @@ public class JsonToObjectTransformerParserTests { } - static class TestPerson { - - private String firstName; - - private String lastName; - - private int age; - - private TestAddress address; - - - public String getFirstName() { - return this.firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getLastName() { - return this.lastName; - } - - public void setAge(int age) { - this.age = age; - } - - public int getAge() { - return this.age; - } - - public void setAddress(TestAddress address) { - this.address = address; - } - - public TestAddress getAddress() { - return this.address; - } - - @Override - public String toString() { - return "name=" + this.firstName + " " + this.lastName - + ", age=" + this.age + ", address=" + this.address; - } - } - - - static class TestAddress { - - private int number; - - private String street; - - - public void setNumber(int number) { - this.number = number; - } - - public void setStreet(String street) { - this.street = street; - } - - @Override - public String toString() { - return this.number + " " + this.street; - } - } - - static class CustomObjectMapper extends ObjectMapper { public CustomObjectMapper() { @@ -219,8 +146,8 @@ public class JsonToObjectTransformerParserTests { static class CustomJsonObjectMapper extends JsonObjectMapperAdapter { @Override - public Object fromJson(String json, Class valueType) throws Exception { - return new TestJsonContainer(json); + public Object fromJson(Object json, Class valueType) throws Exception { + return new TestJsonContainer((String) json); } } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerTests.java index 9ae4a6771a..aa583bc275 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonToObjectTransformerTests.java @@ -22,6 +22,8 @@ import org.codehaus.jackson.JsonParser.Feature; import org.codehaus.jackson.map.ObjectMapper; import org.junit.Test; +import org.springframework.integration.Message; +import org.springframework.integration.message.GenericMessage; import org.springframework.integration.support.json.JacksonJsonObjectMapper; /** @@ -33,9 +35,11 @@ public class JsonToObjectTransformerTests { @Test public void objectPayload() throws Exception { - JsonToObjectTransformer transformer = new JsonToObjectTransformer(TestPerson.class); + JsonToObjectTransformer transformer = new JsonToObjectTransformer(TestPerson.class); String jsonString = "{\"firstName\":\"John\",\"lastName\":\"Doe\",\"age\":42,\"address\":{\"number\":123,\"street\":\"Main Street\"}}"; - TestPerson person = transformer.transformPayload(jsonString); + Message message = transformer.transform(new GenericMessage(jsonString)); + @SuppressWarnings("unchecked") + TestPerson person = (TestPerson) message.getPayload(); assertEquals("John", person.getFirstName()); assertEquals("Doe", person.getLastName()); assertEquals(42, person.getAge()); @@ -47,10 +51,12 @@ public class JsonToObjectTransformerTests { ObjectMapper customMapper = new ObjectMapper(); customMapper.configure(Feature.ALLOW_UNQUOTED_FIELD_NAMES, Boolean.TRUE); customMapper.configure(Feature.ALLOW_SINGLE_QUOTES, Boolean.TRUE); - JsonToObjectTransformer transformer = - new JsonToObjectTransformer(TestPerson.class, new JacksonJsonObjectMapper(customMapper)); + JsonToObjectTransformer transformer = + new JsonToObjectTransformer(TestPerson.class, new JacksonJsonObjectMapper(customMapper)); String jsonString = "{firstName:'John', lastName:'Doe', age:42, address:{number:123, street:'Main Street'}}"; - TestPerson person = transformer.transformPayload(jsonString); + Message message = transformer.transform(new GenericMessage(jsonString)); + @SuppressWarnings("unchecked") + TestPerson person = (TestPerson) message.getPayload(); assertEquals("John", person.getFirstName()); assertEquals("Doe", person.getLastName()); assertEquals(42, person.getAge()); @@ -60,82 +66,8 @@ public class JsonToObjectTransformerTests { @SuppressWarnings("deprecation") @Test(expected = IllegalArgumentException.class) public void testInt2831IllegalArgument() throws Exception { - new JsonToObjectTransformer(String.class, new Object()); + new JsonToObjectTransformer(String.class, new Object()); } - @SuppressWarnings("unused") - private static class TestPerson { - - private String firstName; - - private String lastName; - - private int age; - - private TestAddress address; - - - public String getFirstName() { - return this.firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getLastName() { - return this.lastName; - } - - public void setAge(int age) { - this.age = age; - } - - public int getAge() { - return this.age; - } - - public void setAddress(TestAddress address) { - this.address = address; - } - - public TestAddress getAddress() { - return this.address; - } - - @Override - public String toString() { - return "name=" + this.firstName + " " + this.lastName - + ", age=" + this.age + ", address=" + this.address; - } - } - - - @SuppressWarnings("unused") - private static class TestAddress { - - private int number; - - private String street; - - - public void setNumber(int number) { - this.number = number; - } - - public void setStreet(String street) { - this.street = street; - } - - @Override - public String toString() { - return this.number + " " + this.street; - } - } - } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/JsonTransformersSymmetricalTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonTransformersSymmetricalTests.java new file mode 100644 index 0000000000..5f81466485 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/JsonTransformersSymmetricalTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013 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.json; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import org.springframework.integration.Message; +import org.springframework.integration.message.GenericMessage; + +/** + * @author Artem Bilan + * @since 3.0 + */ +public class JsonTransformersSymmetricalTests { + + @Test + public void testInt2809ObjectToJson_JsonToObject() { + + TestPerson person = new TestPerson("John", "Doe", 42); + person.setAddress(new TestAddress(123, "Main Street")); + + ObjectToJsonTransformer objectToJsonTransformer = new ObjectToJsonTransformer(); + Message jsonMessage = objectToJsonTransformer.transform(new GenericMessage(person)); + + JsonToObjectTransformer jsonToObjectTransformer = new JsonToObjectTransformer(); + Message result = jsonToObjectTransformer.transform(jsonMessage); + + assertEquals(person, result.getPayload()); + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/ObjectToJsonTransformerParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/json/ObjectToJsonTransformerParserTests.java index 31aa1f9ddd..a0c070eea5 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/json/ObjectToJsonTransformerParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/ObjectToJsonTransformerParserTests.java @@ -174,87 +174,6 @@ public class ObjectToJsonTransformerParserTests { } - static class TestPerson { - - private String firstName; - - private String lastName; - - private int age; - - private TestAddress address; - - - public String getFirstName() { - return this.firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getLastName() { - return this.lastName; - } - - public void setAge(int age) { - this.age = age; - } - - public int getAge() { - return this.age; - } - - public void setAddress(TestAddress address) { - this.address = address; - } - - public TestAddress getAddress() { - return this.address; - } - - @Override - public String toString() { - return "\"name\":\"" + this.firstName + " " + this.lastName - + "\", \"age\":" + this.age + ", \"address\":\"" + this.address + "\""; - } - } - - - static class TestAddress { - - private int number; - - private String street; - - - public int getNumber() { - return this.number; - } - - public void setNumber(int number) { - this.number = number; - } - - public String getStreet() { - return this.street; - } - - public void setStreet(String street) { - this.street = street; - } - - @Override - public String toString() { - return this.number + " " + this.street; - } - } - - static class CustomObjectMapper extends ObjectMapper { public CustomObjectMapper() { 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 8d4a777f5f..f512ca852a 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 @@ -147,66 +147,4 @@ public class ObjectToJsonTransformerTests { new ObjectToJsonTransformer(new Object()); } - @SuppressWarnings("unused") - private static class TestPerson { - - private final String firstName; - - private final String lastName; - - private final int age; - - private TestAddress address; - - - public TestPerson(String firstName, String lastName, int age) { - this.firstName = firstName; - this.lastName = lastName; - this.age = age; - } - - public String getFirstName() { - return firstName; - } - - public String getLastName() { - return lastName; - } - - public int getAge() { - return age; - } - - public TestAddress getAddress() { - return address; - } - - public void setAddress(TestAddress address) { - this.address = address; - } - } - - - @SuppressWarnings("unused") - private static class TestAddress { - - private final int number; - - private final String street; - - - public TestAddress(int number, String street) { - this.number = number; - this.street = street; - } - - public int getNumber() { - return number; - } - - public String getStreet() { - return street; - } - } - } diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/TestAddress.java b/spring-integration-core/src/test/java/org/springframework/integration/json/TestAddress.java new file mode 100644 index 0000000000..ac6039e1c3 --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/TestAddress.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013 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.json; + +/** + * @author Mark Fisher + * @since 2.0 +*/ +@SuppressWarnings("unused") +class TestAddress { + + private volatile int number; + + private volatile String street; + + TestAddress() { + } + + public TestAddress(int number, String street) { + this.number = number; + this.street = street; + } + + + public int getNumber() { + return this.number; + } + + public void setNumber(int number) { + this.number = number; + } + + public String getStreet() { + return this.street; + } + + public void setStreet(String street) { + this.street = street; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TestAddress that = (TestAddress) o; + + if (number != that.number) return false; + if (street != null ? !street.equals(that.street) : that.street != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = number; + result = 31 * result + (street != null ? street.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return this.number + " " + this.street; + } + +} diff --git a/spring-integration-core/src/test/java/org/springframework/integration/json/TestPerson.java b/spring-integration-core/src/test/java/org/springframework/integration/json/TestPerson.java new file mode 100644 index 0000000000..e77bf4d52a --- /dev/null +++ b/spring-integration-core/src/test/java/org/springframework/integration/json/TestPerson.java @@ -0,0 +1,105 @@ +/* + * Copyright 2013 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.json; + +/** + * @author Mark Fisher + * @since 2.0 +*/ +@SuppressWarnings("unused") +class TestPerson { + + private volatile String firstName; + + private volatile String lastName; + + private volatile int age; + + private volatile TestAddress address; + + TestPerson() { + } + + public TestPerson(String firstName, String lastName, int age) { + this.firstName = firstName; + this.lastName = lastName; + this.age = age; + } + + + public String getFirstName() { + return this.firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getLastName() { + return this.lastName; + } + + public void setAge(int age) { + this.age = age; + } + + public int getAge() { + return this.age; + } + + public void setAddress(TestAddress address) { + this.address = address; + } + + public TestAddress getAddress() { + return this.address; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TestPerson that = (TestPerson) o; + + if (age != that.age) return false; + if (address != null ? !address.equals(that.address) : that.address != null) return false; + if (firstName != null ? !firstName.equals(that.firstName) : that.firstName != null) return false; + if (lastName != null ? !lastName.equals(that.lastName) : that.lastName != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = firstName != null ? firstName.hashCode() : 0; + result = 31 * result + (lastName != null ? lastName.hashCode() : 0); + result = 31 * result + age; + result = 31 * result + (address != null ? address.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "name=" + this.firstName + " " + this.lastName + + ", age=" + this.age + ", address=" + this.address; + } +} diff --git a/src/reference/docbook/transformer.xml b/src/reference/docbook/transformer.xml index 8a959e3794..ee1a6d87e4 100644 --- a/src/reference/docbook/transformer.xml +++ b/src/reference/docbook/transformer.xml @@ -206,7 +206,7 @@ public class Kid { a BeanCreationException will be thrown.  - JSON Transformers + JSON Transformers Object to JSON and JSON to Object transformers are provided. @@ -279,7 +279,7 @@ public class Foo { - Beginning with version 2.2, the object-to-json-transformer sets the content-type + Beginning with version 2.2, the object-to-json-transformer sets the content-type header to application/json, by default, if the input message does not already have that header present. @@ -290,66 +290,50 @@ public class Foo { attribute to an empty string (""). This will result in a message with no content-type header, unless such a header was present on the input message. - - The behavior of adding the default header has a side affect - causing applications with the following - sequence to fail: - - - ->object-to-json-transformer->amqp-outbound-adapter----> - - - ---->amqp-inbound-adapter->json-to-object-transformer-> - - - This is because the default SimpleMessageConverter used by the inbound adapter doesn't - recognize this content type and the adapter emits a message with a byte[] payload instead of - String, which was the case with earlier versions. - - - If you are using this pattern, there are a number of ways to configure the environment so that JSON - conversion will be performed correctly. - - - One solution is to set the content type to a text type, so the inbound converter will convert the JSON to - String. This solution requires a change to just the outbound application. - - - ]]> - - - The second solution is to eliminate the json transformers altogether and use an - org.springframework.amqp.support.converter.JsonMessageConverter on both the - outbound and inbound adapters. This configures the adapters to perform the JSON conversion and - the transformers are not necessary. The converter on the outbound adapter adds - type information to the message properties; the inbound converter uses this type information for the conversion. - The converter is provided to the adapters using the message-converter attribute. - This solution requires a change to both the inbound and outbound applications. - - - The third solution is to eliminate the json-to-object-transformer in just - the inbound application and use an - org.springframework.amqp.support.converter.JsonMessageConverter on the - inbound adapter. The converter - is provided to the adapter using the message-converter attribute. - However, because there will be no type information in the message properties, - this also requires adding the defaultType to the converter, using the - same type as currently configured on the json-to-object-transformer. - This solution requires a change to just the inbound application. The configuration below shows - how to configure the message converter; it requires spring-amqp 1.1.3 or - above. - - - - - - - - - -]]> + + Beginning with version 3.0, the ObjectToJsonTransformer adds headers, + reflecting the source type, to the message. Similarly, the JsonToObjectTransformer can + use those type headers when converting the JSON to an object. These headers are mapped in the AMQP adapters so that + they are entirely compatible with the Spring-AMQP + JsonMessageConverter. + + + This enables the following flows to work without any special configuration... + + + ...->amqp-outbound-adapter----> + + + ---->amqp-inbound-adapter->json-to-object-transformer->... + + + Where the outbound adapter is configured with a JsonMessageConverter and the + inbound adapter uses the default SimpleMessageConverter. + + + ...->object-to-json-transformer->amqp-outbound-adapter----> + + + ---->amqp-inbound-adapter->... + + + Where the outbound adapter is configured with a SimpleMessageConverter and the + inbound adapter uses the default JsonMessageConverter. + + + ...->object-to-json-transformer->amqp-outbound-adapter----> + + + ---->amqp-inbound-adapter->json-to-object-transformer-> + + + Where both adapters are configured with a SimpleMessageConverter. + + + When using the headers to determine the type, you should not provide + a class attribute, because it takes precedence over the headers. + In addition to JSON Transformers, Spring Integration provides a built-in #jsonPath SpEL function for use in expressions. For more information see . diff --git a/src/reference/docbook/whats-new.xml b/src/reference/docbook/whats-new.xml index dc68dabc5f..8bd71c43ee 100644 --- a/src/reference/docbook/whats-new.xml +++ b/src/reference/docbook/whats-new.xml @@ -325,10 +325,20 @@
Jackson Support (JSON) - A new abstraction for JSON conversion has been introduced. Implementations for Jackson 1.x - and Jackson 2 are currently provided, with the version being determined by presence on - the classpath. Previously, only Jackson 1.x was supported. For more information, - see 'JSON Transformers' in . + + + A new abstraction for JSON conversion has been introduced. Implementations for Jackson 1.x + and Jackson 2 are currently provided, with the version being determined by presence on + the classpath. Previously, only Jackson 1.x was supported. + + + The ObjectToJsonTransformer and JsonToObjectTransformer + now emit/consume headers containing type information. + + + + + For more information, see 'JSON Transformers' in .