diff --git a/org.springframework.integration/.classpath b/org.springframework.integration/.classpath index 3b296092c2..5f1bbf241c 100644 --- a/org.springframework.integration/.classpath +++ b/org.springframework.integration/.classpath @@ -21,5 +21,7 @@ + + diff --git a/org.springframework.integration/ivy.xml b/org.springframework.integration/ivy.xml index 2ff16745c3..2ff781f38c 100644 --- a/org.springframework.integration/ivy.xml +++ b/org.springframework.integration/ivy.xml @@ -29,7 +29,8 @@ - + + \ No newline at end of file diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/json/InboundJsonMessageMapper.java b/org.springframework.integration/src/main/java/org/springframework/integration/json/InboundJsonMessageMapper.java new file mode 100644 index 0000000000..86f9c8b08b --- /dev/null +++ b/org.springframework.integration/src/main/java/org/springframework/integration/json/InboundJsonMessageMapper.java @@ -0,0 +1,102 @@ +package org.springframework.integration.json; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.codehaus.jackson.JsonFactory; +import org.codehaus.jackson.JsonParser; +import org.codehaus.jackson.JsonToken; +import org.codehaus.jackson.map.JsonMappingException; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.type.TypeFactory; +import org.codehaus.jackson.type.JavaType; +import org.codehaus.jackson.type.TypeReference; +import org.springframework.integration.core.Message; +import org.springframework.integration.core.MessageHeaders; +import org.springframework.integration.message.InboundMessageMapper; +import org.springframework.integration.message.MessageBuilder; +import org.springframework.util.Assert; + +/** + * {@link InboundMessageMapper} implementation that maps incoming JSON messages to a {@link Message} with the specified payload type. + * + * TODO - Need to figure out if we need to go as deep in mapping HeaderTypes...right now it wouldn't work if the header type was something like List + * + * @author Jeremy Grelle + */ +public class InboundJsonMessageMapper implements + InboundMessageMapper { + + private static final String MESSAGE_FORMAT_ERROR = "JSON message is invalid. Expected a message in the format of {\"headers\":{...},\"payload\":{...}} but was "; + + private ObjectMapper objectMapper = new ObjectMapper(); + + private static Map> DEFAULT_HEADER_TYPES; + + private Map> headerTypes = DEFAULT_HEADER_TYPES; + + private boolean mapToPayload = false; + + private JavaType payloadType; + + static { + DEFAULT_HEADER_TYPES = new HashMap>(); + DEFAULT_HEADER_TYPES.put(MessageHeaders.TIMESTAMP, Long.class); + DEFAULT_HEADER_TYPES.put(MessageHeaders.EXPIRATION_DATE, Long.class); + } + + public InboundJsonMessageMapper(Class payloadType) { + this.payloadType = TypeFactory.type(payloadType); + } + + public InboundJsonMessageMapper(TypeReference typeReference) { + this.payloadType = TypeFactory.type(typeReference); + } + + public void setHeaderTypes(Map> headerTypes) { + this.headerTypes.putAll(headerTypes); + } + + public void setMapToPayload(boolean mapToPayload) { + this.mapToPayload = mapToPayload; + } + + public Message toMessage(String jsonMessage) throws Exception { + JsonParser parser = new JsonFactory().createJsonParser(jsonMessage); + if (mapToPayload) { + try { + Object payload = objectMapper.readValue(parser, payloadType); + return MessageBuilder.withPayload(payload).build(); + } catch (JsonMappingException ex) { + throw new IllegalArgumentException("Mapping of JSON message "+jsonMessage+" directly to payload of type "+payloadType.getRawClass().getName()+" failed.", ex); + } + } else { + String error = MESSAGE_FORMAT_ERROR + jsonMessage; + Assert.isTrue(parser.nextToken() == JsonToken.START_OBJECT, error); + Assert.isTrue(parser.nextToken() == JsonToken.FIELD_NAME, error); + Assert.isTrue(parser.getCurrentName().equals("headers"), error); + Assert.isTrue(parser.nextToken() == JsonToken.START_OBJECT, error); + Map headers = new LinkedHashMap(); + while (parser.nextToken() != JsonToken.END_OBJECT) { + String headerName = parser.getCurrentName(); + parser.nextToken(); + Class headerType = headerTypes.containsKey(headerName) ? headerTypes.get(headerName) : Object.class; + try { + headers.put(headerName, objectMapper.readValue(parser, headerType)); + } catch (JsonMappingException ex) { + throw new IllegalArgumentException("Mapping header \""+headerName+"\" of JSON message "+jsonMessage+" to header type "+payloadType.getRawClass().getName()+" failed.", ex); + } + } + Assert.isTrue(parser.nextToken() == JsonToken.FIELD_NAME, error); + Assert.isTrue(parser.getCurrentName().equals("payload"), error); + parser.nextToken(); + try { + Object payload = objectMapper.readValue(parser, payloadType); + return MessageBuilder.withPayload(payload).copyHeaders(headers).build(); + } catch (JsonMappingException ex) { + throw new IllegalArgumentException("Mapping payload of JSON message "+jsonMessage+" to payload type "+payloadType.getRawClass().getName()+" failed.", ex); + } + } + } +} diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/json/OutboundJsonMessageMapper.java b/org.springframework.integration/src/main/java/org/springframework/integration/json/OutboundJsonMessageMapper.java new file mode 100644 index 0000000000..eb2d1d9713 --- /dev/null +++ b/org.springframework.integration/src/main/java/org/springframework/integration/json/OutboundJsonMessageMapper.java @@ -0,0 +1,35 @@ +package org.springframework.integration.json; + +import java.io.StringWriter; + +import org.codehaus.jackson.map.ObjectMapper; +import org.springframework.integration.core.Message; +import org.springframework.integration.message.OutboundMessageMapper; + +/** + * {@link OutboundMessageMapper} implementation the converts a {@link Message} to a JSON string representation. + * + * TODO - We might need to add special handling for MessageHistory + * + * @author Jeremy Grelle + */ +public class OutboundJsonMessageMapper implements OutboundMessageMapper { + + private boolean shouldExtractPayload = false; + + private ObjectMapper objectMapper = new ObjectMapper(); + + public String fromMessage(Message message) throws Exception { + StringWriter writer = new StringWriter(); + if (shouldExtractPayload) { + objectMapper.writeValue(writer, message.getPayload()); + } else { + objectMapper.writeValue(writer, message); + } + return writer.toString(); + } + + public void setShouldExtractPayload(boolean shouldExtractPayload) { + this.shouldExtractPayload = shouldExtractPayload; + } +} diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/json/InboundJsonMessageMapperTests.java b/org.springframework.integration/src/test/java/org/springframework/integration/json/InboundJsonMessageMapperTests.java new file mode 100644 index 0000000000..26bc3a3540 --- /dev/null +++ b/org.springframework.integration/src/test/java/org/springframework/integration/json/InboundJsonMessageMapperTests.java @@ -0,0 +1,203 @@ +package org.springframework.integration.json; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.codehaus.jackson.JsonGenerationException; +import org.codehaus.jackson.map.JsonMappingException; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.type.TypeReference; +import org.junit.Test; +import org.springframework.integration.core.Message; +import org.springframework.integration.core.MessageHeaders; +import org.springframework.integration.message.MessageBuilder; + +public class InboundJsonMessageMapperTests { + + ObjectMapper mapper = new ObjectMapper(); + + @Test + public void testToMessageWithHeadersAndStringPayload() throws Exception { + String jsonMessage = "{\"headers\":{\"$timestamp\":1,\"$id\":\"myUniqueId\"},\"payload\":\"myPayloadStuff\"}"; + Message expected = MessageBuilder.withPayload("myPayloadStuff").setHeader(MessageHeaders.TIMESTAMP, new Long(1)).setHeader(MessageHeaders.ID,"myUniqueId").build(); + InboundJsonMessageMapper mapper = new InboundJsonMessageMapper(String.class); + Message result = mapper.toMessage(jsonMessage); + assertEquals(expected, result); + } + + @Test + public void testToMessageWithStringPayload() throws Exception { + String jsonMessage = "\"myPayloadStuff\""; + String expected = "myPayloadStuff"; + InboundJsonMessageMapper mapper = new InboundJsonMessageMapper(String.class); + mapper.setMapToPayload(true); + Message result = mapper.toMessage(jsonMessage); + assertEquals(expected, result.getPayload()); + } + + @Test + public void testToMessageWithHeadersAndBeanPayload() throws Exception { + TestBean bean = new TestBean(); + String jsonMessage = "{\"headers\":{\"$timestamp\":1,\"$id\":\"myUniqueId\"},\"payload\":"+getBeanAsJson(bean)+"}"; + Message expected = MessageBuilder.withPayload(bean).setHeader(MessageHeaders.TIMESTAMP, new Long(1)).setHeader(MessageHeaders.ID,"myUniqueId").build(); + InboundJsonMessageMapper mapper = new InboundJsonMessageMapper(TestBean.class); + Message result = mapper.toMessage(jsonMessage); + assertEquals(expected, result); + } + + @Test + public void testToMessageWithBeanPayload() throws Exception { + TestBean expected = new TestBean(); + String jsonMessage = getBeanAsJson(expected); + InboundJsonMessageMapper mapper = new InboundJsonMessageMapper(TestBean.class); + mapper.setMapToPayload(true); + Message result = mapper.toMessage(jsonMessage); + assertEquals(expected, result.getPayload()); + } + + @Test + public void testToMessageWithBeanHeaderAndStringPayload() throws Exception { + TestBean bean = new TestBean(); + String jsonMessage = "{\"headers\":{\"$timestamp\":1,\"$id\":\"myUniqueId\", \"myHeader\":"+getBeanAsJson(bean)+"},\"payload\":\"myPayloadStuff\"}"; + Message expected = MessageBuilder.withPayload("myPayloadStuff"). + setHeader(MessageHeaders.TIMESTAMP, new Long(1)).setHeader(MessageHeaders.ID,"myUniqueId").setHeader("myHeader", bean).build(); + InboundJsonMessageMapper mapper = new InboundJsonMessageMapper(String.class); + Map> headerTypes = new HashMap>(); + headerTypes.put("myHeader", TestBean.class); + mapper.setHeaderTypes(headerTypes); + Message result = mapper.toMessage(jsonMessage); + assertEquals(expected, result); + } + + @Test + public void testToMessageWithHeadersAndListOfStringsPayload() throws Exception { + String jsonMessage = "{\"headers\":{\"$timestamp\":1,\"$id\":\"myUniqueId\"},\"payload\":[\"myPayloadStuff1\",\"myPayloadStuff2\",\"myPayloadStuff3\"]}"; + List expectedList = Arrays.asList(new String[]{"myPayloadStuff1", "myPayloadStuff2", "myPayloadStuff3"}); + Message> expected = MessageBuilder.withPayload(expectedList).setHeader(MessageHeaders.TIMESTAMP, new Long(1)).setHeader(MessageHeaders.ID,"myUniqueId").build(); + InboundJsonMessageMapper mapper = new InboundJsonMessageMapper(new TypeReference>(){}); + Message result = mapper.toMessage(jsonMessage); + assertEquals(expected, result); + } + + @Test + public void testToMessageWithHeadersAndListOfBeansPayload() throws Exception { + TestBean bean1 = new TestBean(); + TestBean bean2 = new TestBean(); + String jsonMessage = "{\"headers\":{\"$timestamp\":1,\"$id\":\"myUniqueId\"},\"payload\":["+getBeanAsJson(bean1)+","+getBeanAsJson(bean2)+"]}"; + List expectedList = Arrays.asList(new TestBean[]{bean1, bean2}); + Message> expected = MessageBuilder.withPayload(expectedList).setHeader(MessageHeaders.TIMESTAMP, new Long(1)).setHeader(MessageHeaders.ID,"myUniqueId").build(); + InboundJsonMessageMapper mapper = new InboundJsonMessageMapper(new TypeReference>(){}); + Message result = mapper.toMessage(jsonMessage); + assertEquals(expected, result); + } + + @Test + public void testToMessageInvalidFormatPayloadAndHeadersReversed() throws Exception { + String jsonMessage = "{\"payload\":\"myPayloadStuff\",\"headers\":{\"$timestamp\":1,\"$id\":\"myUniqueId\"}}"; + InboundJsonMessageMapper mapper = new InboundJsonMessageMapper(String.class); + + try { + mapper.toMessage(jsonMessage); + fail(); + } catch(IllegalArgumentException ex) { + //Expected + } + } + + @Test + public void testToMessageInvalidFormatPayloadNoHeaders() throws Exception { + String jsonMessage = "{\"payload\":\"myPayloadStuff\"}"; + InboundJsonMessageMapper mapper = new InboundJsonMessageMapper(String.class); + + try { + mapper.toMessage(jsonMessage); + fail(); + } catch(IllegalArgumentException ex) { + //Expected + } + } + + @Test + public void testToMessageInvalidFormatHeadersNoPayload() throws Exception { + String jsonMessage = "{\"headers\":{\"$timestamp\":1,\"$id\":\"myUniqueId\"}}"; + InboundJsonMessageMapper mapper = new InboundJsonMessageMapper(String.class); + + try { + mapper.toMessage(jsonMessage); + fail(); + } catch(IllegalArgumentException ex) { + //Expected + } + } + + @Test + public void testToMessageInvalidFormatHeadersAndStringPayloadWithMapToPayload() throws Exception { + String jsonMessage = "{\"headers\":{\"$timestamp\":1,\"$id\":\"myUniqueId\"},\"payload\":\"myPayloadStuff\"}"; + InboundJsonMessageMapper mapper = new InboundJsonMessageMapper(String.class); + mapper.setMapToPayload(true); + try { + mapper.toMessage(jsonMessage); + fail(); + } catch(IllegalArgumentException ex) { + //Expected + } + } + + @Test + public void testToMessageInvalidFormatHeadersAndBeanPayloadWithMapToPayload() throws Exception { + TestBean bean = new TestBean(); + String jsonMessage = "{\"headers\":{\"$timestamp\":1,\"$id\":\"myUniqueId\"},\"payload\":"+getBeanAsJson(bean)+"}"; + InboundJsonMessageMapper mapper = new InboundJsonMessageMapper(TestBean.class); + mapper.setMapToPayload(true); + try { + mapper.toMessage(jsonMessage); + fail(); + } catch(IllegalArgumentException ex) { + //Expected + } + } + + @Test + public void testToMessageWithHeadersAndPayloadTypeMappingFailure() throws Exception { + TestBean bean = new TestBean(); + String jsonMessage = "{\"headers\":{\"$timestamp\":1,\"$id\":\"myUniqueId\"},\"payload\":"+getBeanAsJson(bean)+"}"; + InboundJsonMessageMapper mapper = new InboundJsonMessageMapper(Long.class); + try { + mapper.toMessage(jsonMessage); + fail(); + } catch(IllegalArgumentException ex) { + //Expected + } + } + + @Test + public void testToMessageWithBeanHeaderTypeMappingFailure() throws Exception { + TestBean bean = new TestBean(); + String jsonMessage = "{\"headers\":{\"$timestamp\":1,\"$id\":\"myUniqueId\",\"myHeader\":"+getBeanAsJson(bean)+"},\"payload\":\"myPayloadStuff\"}"; + InboundJsonMessageMapper mapper = new InboundJsonMessageMapper(String.class); + Map> headerTypes = new HashMap>(); + headerTypes.put("myHeader", Long.class); + mapper.setHeaderTypes(headerTypes); + try { + mapper.toMessage(jsonMessage); + fail(); + } catch(IllegalArgumentException ex) { + //Expected + } + } + + + + private String getBeanAsJson(TestBean bean) throws JsonGenerationException, JsonMappingException, IOException { + StringWriter writer = new StringWriter(); + mapper.writeValue(writer, bean); + return writer.toString(); + } +} diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/json/OutboundJsonMessageMapperTests.java b/org.springframework.integration/src/test/java/org/springframework/integration/json/OutboundJsonMessageMapperTests.java new file mode 100644 index 0000000000..ec016fdc75 --- /dev/null +++ b/org.springframework.integration/src/test/java/org/springframework/integration/json/OutboundJsonMessageMapperTests.java @@ -0,0 +1,76 @@ +package org.springframework.integration.json; + +import static org.junit.Assert.*; + +import java.io.IOException; + +import org.codehaus.jackson.JsonFactory; +import org.codehaus.jackson.JsonParseException; +import org.codehaus.jackson.JsonParser; +import org.codehaus.jackson.JsonToken; +import org.codehaus.jackson.map.ObjectMapper; +import org.junit.Test; +import org.springframework.integration.core.Message; +import org.springframework.integration.core.MessageHeaders; +import org.springframework.integration.message.MessageBuilder; + + +public class OutboundJsonMessageMapperTests { + + private JsonFactory jsonFactory = new JsonFactory(); + private ObjectMapper objectMapper = new ObjectMapper(); + + @Test + public void testFromMessageWithHeadersAndStringPayload() throws Exception { + Message testMessage = MessageBuilder.withPayload("myPayloadStuff").build(); + String expected = "{\"headers\":{\"$timestamp\":"+testMessage.getHeaders().getTimestamp()+ + ",\"$history\":[],\"$id\":\""+testMessage.getHeaders().getId()+"\"},\"payload\":\"myPayloadStuff\"}"; + OutboundJsonMessageMapper mapper = new OutboundJsonMessageMapper(); + String result = mapper.fromMessage(testMessage); + assertEquals(expected, result); + } + + @Test + public void testFromMessageExtractStringPayload() throws Exception { + Message testMessage = MessageBuilder.withPayload("myPayloadStuff").build(); + String expected = "\"myPayloadStuff\""; + OutboundJsonMessageMapper mapper = new OutboundJsonMessageMapper(); + mapper.setShouldExtractPayload(true); + String result = mapper.fromMessage(testMessage); + assertEquals(expected, result); + } + + @Test + public void testFromMessageWithHeadersAndBeanPayload() throws Exception { + TestBean payload = new TestBean(); + Message testMessage = MessageBuilder.withPayload(payload).setHeader(MessageHeaders.TIMESTAMP, new Long(1234)).setHeader(MessageHeaders.ID, "myUniqueId").build(); + String expectedHeaders = "\"headers\":{\"$timestamp\":"+testMessage.getHeaders().getTimestamp()+ + ",\"$history\":[],\"$id\":\""+testMessage.getHeaders().getId()+"\""; + OutboundJsonMessageMapper mapper = new OutboundJsonMessageMapper(); + String result = mapper.fromMessage(testMessage); + assertTrue(result.contains(expectedHeaders)); + TestBean parsedPayload = extractJsonPayloadToTestBean(result); + assertEquals(payload, parsedPayload); + } + + @Test + public void testFromMessageExtractBeanPayload() throws Exception { + TestBean payload = new TestBean(); + Message testMessage = MessageBuilder.withPayload(payload).build(); + OutboundJsonMessageMapper mapper = new OutboundJsonMessageMapper(); + mapper.setShouldExtractPayload(true); + String result = mapper.fromMessage(testMessage); + assertTrue(!result.contains("headers")); + TestBean parsedPayload = objectMapper.readValue(result, TestBean.class); + assertEquals(payload, parsedPayload); + } + + private TestBean extractJsonPayloadToTestBean(String json) throws JsonParseException, IOException { + JsonParser parser = jsonFactory.createJsonParser(json); + do { + parser.nextToken(); + } while(parser.getCurrentToken() != JsonToken.FIELD_NAME || !parser.getCurrentName().equals("payload")); + parser.nextToken(); + return objectMapper.readValue(parser, TestBean.class); + } +} diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/json/TestBean.java b/org.springframework.integration/src/test/java/org/springframework/integration/json/TestBean.java new file mode 100644 index 0000000000..d916e2c1a5 --- /dev/null +++ b/org.springframework.integration/src/test/java/org/springframework/integration/json/TestBean.java @@ -0,0 +1,93 @@ +package org.springframework.integration.json; + +import java.util.Date; + +public class TestBean { + + private String value = "foo"; + + private boolean test = false; + + private long number = 42; + + private Date now = new Date(); + + private TestChildBean child = new TestChildBean(); + + public String getValue() { + return value; + } + + public boolean getTest() { + return test; + } + + public long getNumber() { + return number; + } + + public Date getNow() { + return this.now; + } + + public TestChildBean getChild() { + return child; + } + + public void setValue(String value) { + this.value = value; + } + + public void setTest(boolean test) { + this.test = test; + } + + public void setNumber(long number) { + this.number = number; + } + + public void setChild(TestChildBean child) { + this.child = child; + } + + public void setNow(Date now) { + this.now = now; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((child == null) ? 0 : child.hashCode()); + result = prime * result + (int) (number ^ (number >>> 32)); + result = prime * result + (test ? 1231 : 1237); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TestBean other = (TestBean) obj; + if (child == null) { + if (other.child != null) + return false; + } else if (!child.equals(other.child)) + return false; + if (number != other.number) + return false; + if (test != other.test) + return false; + if (value == null) { + if (other.value != null) + return false; + } else if (!value.equals(other.value)) + return false; + return true; + } +} diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/json/TestChildBean.java b/org.springframework.integration/src/test/java/org/springframework/integration/json/TestChildBean.java new file mode 100644 index 0000000000..ff1438d6c7 --- /dev/null +++ b/org.springframework.integration/src/test/java/org/springframework/integration/json/TestChildBean.java @@ -0,0 +1,71 @@ +package org.springframework.integration.json; + +public class TestChildBean { + private String value = "bar"; + + private String baz = null; + + private TestBean parent = null; + + public String getValue() { + return value; + } + + public String getBaz() { + return baz; + } + + public TestBean getParent() { + return parent; + } + + public void setParent(TestBean parent) { + this.parent = parent; + } + + public void setValue(String value) { + this.value = value; + } + + public void setBaz(String baz) { + this.baz = baz; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((baz == null) ? 0 : baz.hashCode()); + result = prime * result + + ((parent == null) ? 0 : parent.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TestChildBean other = (TestChildBean) obj; + if (baz == null) { + if (other.baz != null) + return false; + } else if (!baz.equals(other.baz)) + return false; + if (parent == null) { + if (other.parent != null) + return false; + } else if (!parent.equals(other.parent)) + return false; + if (value == null) { + if (other.value != null) + return false; + } else if (!value.equals(other.value)) + return false; + return true; + } +} \ No newline at end of file diff --git a/org.springframework.integration/template.mf b/org.springframework.integration/template.mf index 8465d708be..ed0305ee45 100644 --- a/org.springframework.integration/template.mf +++ b/org.springframework.integration/template.mf @@ -6,4 +6,5 @@ Import-Template: org.springframework.*;version="[3.0.0, 4.0.0)", org.apache.commons.logging;version="[1.1.1, 2.0.0)", org.aopalliance.*;version="[1.0.0, 2.0.0)", + org.codehaus.jackson.*;version="[1.0.0, 2.0.0)", org.w3c.dom.*;version="0"