diff --git a/build.gradle b/build.gradle index d88ce6bd92..bb39469ca3 100644 --- a/build.gradle +++ b/build.gradle @@ -316,6 +316,7 @@ project('spring-integration-core') { testCompile ("org.aspectj:aspectjweaver:$aspectjVersion") testCompile "io.projectreactor:reactor-test:$reactorVersion" + testCompile ("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson2Version") } } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ObjectToMapTransformerParser.java b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ObjectToMapTransformerParser.java index bd2dc3d8ac..9058e06f3d 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ObjectToMapTransformerParser.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/config/xml/ObjectToMapTransformerParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2017 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. @@ -21,10 +21,13 @@ import org.w3c.dom.Element; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.integration.transformer.ObjectToMapTransformer; +import org.springframework.util.StringUtils; /** * @author Oleg Zhurakousky * @author Mauro Franceschini + * @author Artem Bilan + * * @since 2.0 */ public class ObjectToMapTransformerParser extends AbstractTransformerParser { @@ -36,6 +39,11 @@ public class ObjectToMapTransformerParser extends AbstractTransformerParser { @Override protected void parseTransformer(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + String objectMapper = element.getAttribute("object-mapper"); + if (StringUtils.hasText(objectMapper)) { + builder.addConstructorArgReference(objectMapper); + } IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "flatten", "shouldFlattenKeys"); } + } diff --git a/spring-integration-core/src/main/java/org/springframework/integration/dsl/Transformers.java b/spring-integration-core/src/main/java/org/springframework/integration/dsl/Transformers.java index 77ef35afa7..c136433eea 100644 --- a/spring-integration-core/src/main/java/org/springframework/integration/dsl/Transformers.java +++ b/spring-integration-core/src/main/java/org/springframework/integration/dsl/Transformers.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2017 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. @@ -71,6 +71,16 @@ public abstract class Transformers { return transformer; } + public static ObjectToMapTransformer toMap(JsonObjectMapper jsonObjectMapper) { + return new ObjectToMapTransformer(jsonObjectMapper); + } + + public static ObjectToMapTransformer toMap(JsonObjectMapper jsonObjectMapper, boolean shouldFlattenKeys) { + ObjectToMapTransformer transformer = new ObjectToMapTransformer(); + transformer.setShouldFlattenKeys(shouldFlattenKeys); + return transformer; + } + public static MapToObjectTransformer fromMap(Class targetClass) { return new MapToObjectTransformer(targetClass); } 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 ef7e591ab9..6aa6038d17 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 2013-2016 the original author or authors. + * Copyright 2013-2017 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. @@ -44,9 +44,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; * * * @author Artem Bilan + * @author Vikas Prasad + * * @since 3.0 */ public class Jackson2JsonObjectMapper extends AbstractJacksonJsonObjectMapper { @@ -57,6 +60,7 @@ public class Jackson2JsonObjectMapper extends AbstractJacksonJsonObjectMapper + * Transforms an object graph into a Map. It supports a conventional Map (map of maps) + * where complex attributes are represented as Map values as well as a flat Map + * where keys document the path to the value. By default it will transform to a flat Map. + * If you need to transform to a Map of Maps set the 'shouldFlattenKeys' property to 'false' + * via the {@link ObjectToMapTransformer#setShouldFlattenKeys(boolean)} method. + * It supports Collections, Maps and Arrays which means that for flat maps it will flatten + * an Object's properties. Below is an example showing how a flattened + * Object hierarchy is represented when 'shouldFlattenKeys' is TRUE. + *

+ * The transformation is based on to and then from JSON conversion. * * * public class Person { @@ -52,14 +57,36 @@ import org.springframework.util.StringUtils; * @author Oleg Zhurakousky * @author Artem Bilan * @author Gary Russell + * @author Vikas Prasad + * * @since 2.0 + * + * @see JsonObjectMapperProvider */ public class ObjectToMapTransformer extends AbstractPayloadTransformer> { - private final JsonObjectMapper jsonObjectMapper = JsonObjectMapperProvider.newInstance(); + private final JsonObjectMapper jsonObjectMapper; private volatile boolean shouldFlattenKeys = true; + /** + * Construct with the default {@link JsonObjectMapper} instance available via + * {@link JsonObjectMapperProvider#newInstance() factory}. + */ + public ObjectToMapTransformer() { + this(JsonObjectMapperProvider.newInstance()); + } + + /** + * Construct with the provided {@link JsonObjectMapper} instance. + * @param jsonObjectMapper the {@link JsonObjectMapper} to use. + * @since 5.0 + */ + public ObjectToMapTransformer(JsonObjectMapper jsonObjectMapper) { + Assert.notNull(jsonObjectMapper, "'jsonObjectMapper' must not be null"); + this.jsonObjectMapper = jsonObjectMapper; + } + public void setShouldFlattenKeys(boolean shouldFlattenKeys) { this.shouldFlattenKeys = shouldFlattenKeys; } @@ -88,7 +115,7 @@ public class ObjectToMapTransformer extends AbstractPayloadTransformer) element, resultMap); } else if (element != null && element.getClass().isArray()) { - Collection collection = CollectionUtils.arrayToList(element); + Collection collection = CollectionUtils.arrayToList(element); this.doProcessCollection(propertyPrefix, collection, resultMap); } else { @@ -97,7 +124,7 @@ public class ObjectToMapTransformer extends AbstractPayloadTransformer flattenMap(Map result) { - Map resultMap = new HashMap(); + Map resultMap = new HashMap<>(); this.doFlatten("", result, resultMap); return resultMap; } @@ -111,7 +138,7 @@ public class ObjectToMapTransformer extends AbstractPayloadTransformer list, Map resultMap) { + private void doProcessCollection(String propertyPrefix, Collection list, Map resultMap) { int counter = 0; for (Object element : list) { this.doProcessElement(propertyPrefix + "[" + counter + "]", element, resultMap); diff --git a/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration-5.0.xsd b/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration-5.0.xsd index 007fef328d..4d925981fc 100644 --- a/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration-5.0.xsd +++ b/spring-integration-core/src/main/resources/org/springframework/integration/config/spring-integration-5.0.xsd @@ -1832,7 +1832,7 @@ - + @@ -2441,22 +2441,42 @@ - + - - - - Specifies if the result Map of Maps should be transformed further to flat keys of - object's property paths. - Default is 'true'. - - - + + + + + + + Specifies if the result Map of Maps should be transformed further to flat keys of + object's property paths. + Default is 'true'. + + + + + + + Optional reference to a JsonObjectMapper instance. + By default, a JsonObjectMapper that uses a JsonObjectMapperProvider. + + + + + + + + + + + + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests-context.xml b/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests-context.xml index 7e20ad4a67..80aed044f1 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests-context.xml +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests-context.xml @@ -118,6 +118,10 @@ + + + + @@ -130,7 +134,9 @@ - + @@ -147,7 +153,7 @@ - + diff --git a/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests.java b/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests.java index 428bce035b..aad5f291ff 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/config/ChainParserTests.java @@ -16,6 +16,7 @@ package org.springframework.integration.config; +import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -59,8 +60,10 @@ import org.springframework.integration.handler.ReplyRequiredException; import org.springframework.integration.handler.ServiceActivatingHandler; import org.springframework.integration.message.MessageMatcher; import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.support.json.JsonObjectMapper; import org.springframework.integration.test.util.TestUtils; import org.springframework.integration.transformer.MessageTransformingHandler; +import org.springframework.integration.transformer.ObjectToMapTransformer; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; @@ -129,7 +132,8 @@ public class ChainParserTests { @Autowired private MessageChannel loggingChannelAdapterChannel; - @Autowired @Qualifier("logChain.handler") + @Autowired + @Qualifier("logChain.handler") private MessageHandlerChain logChain; @Autowired @@ -321,7 +325,7 @@ public class ChainParserTests { DirectFieldAccessor dfa = new DirectFieldAccessor(handler); dfa.setPropertyValue("messageLogger", logger); - this.loggingChannelAdapterChannel.send(MessageBuilder.withPayload(new byte[] {116, 101, 115, 116}).build()); + this.loggingChannelAdapterChannel.send(MessageBuilder.withPayload(new byte[] { 116, 101, 115, 116 }).build()); assertNotNull(log.get()); assertEquals("TEST", log.get()); } @@ -392,13 +396,23 @@ public class ChainParserTests { GatewayProxyFactoryBean.class); assertEquals("strings", TestUtils.getPropertyValue(gatewayProxyFactoryBean, "defaultRequestChannelName")); assertEquals("numbers", TestUtils.getPropertyValue(gatewayProxyFactoryBean, "defaultReplyChannelName")); - assertEquals(new Long(1000), TestUtils + assertEquals(1000L, TestUtils .getPropertyValue(gatewayProxyFactoryBean, "defaultRequestTimeout", Expression.class).getValue()); - assertEquals(new Long(100), TestUtils + assertEquals(100L, TestUtils .getPropertyValue(gatewayProxyFactoryBean, "defaultReplyTimeout", Expression.class).getValue()); assertTrue(this.beanFactory.containsBean("subComponentsIdSupport1$child.objectToStringTransformerWithinChain.handler")); assertTrue(this.beanFactory.containsBean("subComponentsIdSupport1$child.objectToMapTransformerWithinChain.handler")); + + Object transformerHandler = this.beanFactory.getBean("subComponentsIdSupport1$child.objectToMapTransformerWithinChain.handler"); + + Object transformer = TestUtils.getPropertyValue(transformerHandler, "transformer"); + + assertThat(transformer, instanceOf(ObjectToMapTransformer.class)); + assertFalse(TestUtils.getPropertyValue(transformer, "shouldFlattenKeys", Boolean.class)); + assertSame(this.beanFactory.getBean(JsonObjectMapper.class), + TestUtils.getPropertyValue(transformer, "jsonObjectMapper")); + assertTrue(this.beanFactory.containsBean("subComponentsIdSupport1$child.mapToObjectTransformerWithinChain.handler")); assertTrue(this.beanFactory.containsBean("subComponentsIdSupport1$child.controlBusWithinChain.handler")); assertTrue(this.beanFactory.containsBean("subComponentsIdSupport1$child.routerWithinChain.handler")); diff --git a/spring-integration-core/src/test/java/org/springframework/integration/transformer/ObjectToMapTransformerTests.java b/spring-integration-core/src/test/java/org/springframework/integration/transformer/ObjectToMapTransformerTests.java index 190bb1a317..6805f8abe3 100644 --- a/spring-integration-core/src/test/java/org/springframework/integration/transformer/ObjectToMapTransformerTests.java +++ b/spring-integration-core/src/test/java/org/springframework/integration/transformer/ObjectToMapTransformerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2017 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,17 +16,21 @@ package org.springframework.integration.transformer; +import static org.hamcrest.Matchers.instanceOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; import java.io.IOException; import java.math.BigDecimal; +import java.time.Instant; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import org.junit.Test; @@ -36,12 +40,19 @@ import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.support.json.Jackson2JsonObjectMapper; import org.springframework.messaging.Message; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + /** * * @author Oleg Zhurakousky * @author Gunnar Hillert + * @author Vikas Prasad + * @author Artem Bilan + * * @since 2.0 */ public class ObjectToMapTransformerTests { @@ -154,6 +165,41 @@ public class ObjectToMapTransformerTests { transformer.transform(message); } + @Test + public void testJacksonJSR310Support_PassInstantField_ReturnsMapWithOnlyOneEntryForInstantField() throws Exception { + Person person = new Person(); + person.deathDate = Instant.now(); + + Employee employee = new Employee(); + employee.setPerson(person); + + Map transformedMap = new ObjectToMapTransformer().transformPayload(employee); + + // If JSR310 support is enabled by calling findAndRegisterModules() on the Jackson mapper, + // Instant field should not be broken. Thus the count should exactly be 1 here. + assertEquals(1L, transformedMap.values().stream().filter(Objects::nonNull).count()); + } + + @Test + public void testCustomMapperSupport_DisableTimestampFlag_SerializesDateAsString() throws Exception { + Employee employee = buildEmployee(); + + ObjectMapper customMapper = new ObjectMapper(); + customMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + + Map transformedMap = + new ObjectToMapTransformer(new Jackson2JsonObjectMapper(customMapper)) + .transformPayload(employee); + + assertThat(transformedMap.get("listOfDates[0][0]"), instanceOf(String.class)); + + assertThat(transformedMap.get("listOfDates[0][1]"), instanceOf(String.class)); + + assertThat(transformedMap.get("listOfDates[1][0]"), instanceOf(String.class)); + + assertThat(transformedMap.get("listOfDates[1][1]"), instanceOf(String.class)); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) public Employee buildEmployee() { Address companyAddress = new Address(); @@ -162,8 +208,8 @@ public class ObjectToMapTransformerTests { companyAddress.setZip("12345"); Map coordinates = new HashMap(); - coordinates.put("latitude", new Long[]{(long) 1, (long) 5, (long) 13}); - coordinates.put("longitude", new Long[]{(long) 156}); + coordinates.put("latitude", new Long[] { (long) 1, (long) 5, (long) 13 }); + coordinates.put("longitude", new Long[] { (long) 156 }); companyAddress.setCoordinates(coordinates); List datesA = new ArrayList(); @@ -231,58 +277,90 @@ public class ObjectToMapTransformerTests { } public static class Employee { + private List departments; + private List> listOfDates; + private String companyName; + private Person person; + private Address companyAddress; + private Map> testMapInMapData; + public List> getListOfDates() { return listOfDates; } + public void setListOfDates(List> listOfDates) { this.listOfDates = listOfDates; } + public Map> getTestMapInMapData() { return testMapInMapData; } + public void setTestMapInMapData( Map> testMapInMapData) { this.testMapInMapData = testMapInMapData; } + public String getCompanyName() { return companyName; } + public void setCompanyName(String companyName) { this.companyName = companyName; } + public Person getPerson() { return person; } + public void setPerson(Person person) { this.person = person; } + public Address getCompanyAddress() { return companyAddress; } + public void setCompanyAddress(Address companyAddress) { this.companyAddress = companyAddress; } + public List getDepartments() { return departments; } + public void setDepartments(List departments) { this.departments = departments; } + } public static class Person { + private String fname; + private String lname; + private String[] akaNames; + private List> remarks; + private Child child; + private BigDecimal age; + + private Date birthDate; + + public Instant deathDate; + + private Address address; + public BigDecimal getAge() { return age; } @@ -299,85 +377,112 @@ public class ObjectToMapTransformerTests { this.birthDate = birthDate; } - private Date birthDate; public Child getChild() { return child; } + public void setChild(Child child) { this.child = child; } + public List> getRemarks() { return remarks; } + public void setRemarks(List> remarks) { this.remarks = remarks; } - private Address address; + public String[] getAkaNames() { return akaNames; } + public void setAkaNames(String... akaNames) { this.akaNames = akaNames; } + public String getFname() { return fname; } + public void setFname(String fname) { this.fname = fname; } + public String getLname() { return lname; } + public void setLname(String lname) { this.lname = lname; } + public Address getAddress() { return address; } + public void setAddress(Address address) { this.address = address; } + } public static class Address { + private String street; + private String city; + private String zip; + private Map> mapWithListData; + private Map coordinates; + public Map> getMapWithListData() { return mapWithListData; } + public void setMapWithListData(Map> mapWithListData) { this.mapWithListData = mapWithListData; } + public String getStreet() { return street; } + public void setStreet(String street) { this.street = street; } + public String getCity() { return city; } + public void setCity(String city) { this.city = city; } + public String getZip() { return zip; } + public void setZip(String zip) { this.zip = zip; } + public Map getCoordinates() { return coordinates; } + public void setCoordinates(Map coordinates) { this.coordinates = coordinates; } + } public static class Child { + private Person parent; public Person getParent() { @@ -387,5 +492,7 @@ public class ObjectToMapTransformerTests { public void setParent(Person parent) { this.parent = parent; } + } + } diff --git a/src/reference/asciidoc/transformer.adoc b/src/reference/asciidoc/transformer.adoc index 804fb7af68..d4e718ef01 100644 --- a/src/reference/asciidoc/transformer.adoc +++ b/src/reference/asciidoc/transformer.adoc @@ -121,7 +121,7 @@ These will use standard Java serialization by default, but you can provide an im ====== Object-to-Map and Map-to-Object Transformers -Spring Integration also provides _Object-to-Map_ and _Map-to-Object_ transformers which utilize the Spring Expression Language (SpEL) to serialize and de-serialize the object graphs. +Spring Integration also provides _Object-to-Map_ and _Map-to-Object_ transformers which utilize the JSON to serialize and de-serialize the object graphs. The object hierarchy is introspected to the most primitive types (String, int, etc.). The path to this type is described via SpEL, which becomes the _key_ in the transformed Map. The primitive type becomes the value. @@ -144,7 +144,7 @@ public class Child{ \...will be transformed to a Map which looks like this: `{person.name=George, person.child.name=Jenna, person.child.nickNames[0]=Bimbo ... etc}` -The SpEL-based Map allows you to describe the object structure without sharing the actual types allowing you to restore/rebuild the object graph into a differently typed Object graph as long as you maintain the structure. +The JSON-based Map allows you to describe the object structure without sharing the actual types allowing you to restore/rebuild the object graph into a differently typed Object graph as long as you maintain the structure. For example: The above structure could be easily restored back to the following Object graph via the Map-to-Object transformer: [source,java] @@ -215,7 +215,10 @@ or NOTE: NOTE: 'ref' and 'type' attributes are mutually exclusive. You can only use one. -Also, if using the 'ref' attribute, you must point to a 'prototype' scoped bean, otherwise a BeanCreationException will be thrown.  +Also, if using the 'ref' attribute, you must point to a 'prototype' scoped bean, otherwise a `BeanCreationException` will be thrown.  + +Starting with _version 5.0_, the `ObjectToMapTransformer` can be supplied with the customized `JsonObjectMapper`, for example in use-cases when we need special formats for dates or nulls for empty collections. +See <> for more information about `JsonObjectMapper` implementations. [[stream-transformer]] ====== Stream Transformer diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 731a441a33..5f5d5c8835 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -87,7 +87,11 @@ That message is used as a `failedMessage` property of the `MessagingException` w See <> for more information. -The aggregator expression-based `ReleaseStrategy` now evaluates the expression against the `MesageGroup` instead of just the collection of `Message`. +The aggregator expression-based `ReleaseStrategy` now evaluates the expression against the `MessageGroup` instead of just the collection of `Message`. + +See <> for more information. + +The `ObjectToMapTransformer` can now be supplied with a customised `JsonObjectMapper`. See <> for more information.