Merge pull request #699 from garyrussell/INT-2874

* INT-2874: Fix JSON/JMS Incompatibility
This commit is contained in:
Mark Fisher
2013-01-23 13:07:16 -05:00
8 changed files with 117 additions and 38 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-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.
@@ -18,13 +18,13 @@ package org.springframework.integration.json;
import java.io.StringWriter;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.integration.Message;
import org.springframework.integration.MessageHeaders;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.transformer.AbstractTransformer;
import org.springframework.util.Assert;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.StringUtils;
/**
* Transformer implementation that converts a payload instance into a JSON string representation.
@@ -32,6 +32,7 @@ import org.springframework.util.LinkedCaseInsensitiveMap;
* @author Mark Fisher
* @author James Carr
* @author Oleg Zhurakousky
* @author Gary Russell
* @since 2.0
*/
public class ObjectToJsonTransformer extends AbstractTransformer {
@@ -61,7 +62,7 @@ public class ObjectToJsonTransformer extends AbstractTransformer {
// only null assertion is needed since "" is a valid value
Assert.notNull(contentType, "'contentType' must not be null");
this.contentTypeExplicitlySet = true;
this.contentType = contentType;
this.contentType = contentType.trim();
}
private String transformPayload(Object payload) throws Exception {
@@ -80,11 +81,13 @@ public class ObjectToJsonTransformer extends AbstractTransformer {
if (headers.containsKey(MessageHeaders.CONTENT_TYPE)) {
if (this.contentTypeExplicitlySet){
// override
headers.put(MessageHeaders.CONTENT_TYPE, this.contentType);
// override, unless empty
if (StringUtils.hasLength(this.contentType)) {
headers.put(MessageHeaders.CONTENT_TYPE, this.contentType);
}
}
}
else {
else if (StringUtils.hasLength(this.contentType)) {
headers.put(MessageHeaders.CONTENT_TYPE, this.contentType);
}
messageBuilder.copyHeaders(headers);

View File

@@ -2106,11 +2106,17 @@
</xsd:choice>
<xsd:attribute name="content-type" use="optional">
<xsd:annotation>
<xsd:documentation>
Allows you to set 'content-type' Message header. In the case that a content-type header is already present,
we will only override that value IF this attribute is explicitely set with
content-type value (e.g., content-type="text/xml")
</xsd:documentation>
<xsd:documentation><![CDATA[
Allows you to set the 'content-type' Message header. When a 'content-type' header is already present
on the input message, the transformer will only override that value IF this attribute is explicitly set
(e.g., content-type="text/x-json").
If this attribute is omitted, the transformer will set the 'content-type' header to "application/json",
when there is no existing 'content-type' header on the input message.
Setting the attribute to an empty string ("")
will suppress setting the header to the default value,
but will not remove the header, if present on the input message. If you wish to remove an existing
header, use a <header-filter/> before or after the transformer.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="object-mapper" use="optional">

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-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.
@@ -17,6 +17,7 @@
package org.springframework.integration.json;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -31,6 +32,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.integration.Message;
import org.springframework.integration.MessageChannel;
import org.springframework.integration.MessageHeaders;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
@@ -40,12 +42,13 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Gary Russell
* @since 2.0
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class ObjectToJsonTransformerParserTests {
@Autowired
private volatile ApplicationContext context;
@@ -56,11 +59,15 @@ public class ObjectToJsonTransformerParserTests {
private volatile MessageChannel customObjectMapperInput;
@Test
public void testContextType(){
ObjectToJsonTransformer transformer =
public void testContentType(){
ObjectToJsonTransformer transformer =
TestUtils.getPropertyValue(context.getBean("defaultTransformer"), "handler.transformer", ObjectToJsonTransformer.class);
assertEquals("application/json", TestUtils.getPropertyValue(transformer, "contentType"));
Message<?> transformed = transformer.transform(MessageBuilder.withPayload("foo").build());
assertTrue(transformed.getHeaders().containsKey(MessageHeaders.CONTENT_TYPE));
assertEquals("application/json", transformed.getHeaders().get(MessageHeaders.CONTENT_TYPE));
transformer =
TestUtils.getPropertyValue(context.getBean("customTransformer"), "handler.transformer", ObjectToJsonTransformer.class);
assertEquals("application/json", TestUtils.getPropertyValue(transformer, "contentType"));
@@ -69,6 +76,13 @@ public class ObjectToJsonTransformerParserTests {
TestUtils.getPropertyValue(context.getBean("emptyContentTypeTransformer"), "handler.transformer", ObjectToJsonTransformer.class);
assertEquals("", TestUtils.getPropertyValue(transformer, "contentType"));
transformed = transformer.transform(MessageBuilder.withPayload("foo").build());
assertFalse(transformed.getHeaders().containsKey(MessageHeaders.CONTENT_TYPE));
transformed = transformer.transform(MessageBuilder.withPayload("foo").setHeader(MessageHeaders.CONTENT_TYPE, "foo").build());
assertNotNull(transformed.getHeaders().get(MessageHeaders.CONTENT_TYPE));
assertEquals("foo", transformed.getHeaders().get(MessageHeaders.CONTENT_TYPE));
transformer =
TestUtils.getPropertyValue(context.getBean("overridenContentTypeTransformer"), "handler.transformer", ObjectToJsonTransformer.class);
assertEquals("text/xml", TestUtils.getPropertyValue(transformer, "contentType"));

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-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,24 +16,25 @@
package org.springframework.integration.json;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.codehaus.jackson.JsonGenerator.Feature;
import org.codehaus.jackson.map.ObjectMapper;
import org.junit.Test;
import org.springframework.integration.Message;
import org.springframework.integration.MessageHeaders;
import org.springframework.integration.message.GenericMessage;
import org.springframework.integration.support.MessageBuilder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Gary Russell
* @since 2.0
*/
public class ObjectToJsonTransformerTests {
@@ -71,13 +72,22 @@ public class ObjectToJsonTransformerTests {
@Test
public void withProvidedContentTypeAsEmptyString() throws Exception {
ObjectToJsonTransformer transformer = new ObjectToJsonTransformer();
transformer.setContentType("");
Message<?> message = MessageBuilder.withPayload("foo").build();
Message<?> result = transformer.transform(message);
assertFalse(result.getHeaders().containsKey(MessageHeaders.CONTENT_TYPE));
}
@Test
public void withProvidedContentTypeAsEmptyStringDoesNotOverride() throws Exception {
ObjectToJsonTransformer transformer = new ObjectToJsonTransformer();
transformer.setContentType("");
Message<?> message = MessageBuilder.withPayload("foo").setHeader(MessageHeaders.CONTENT_TYPE, "text/xml").build();
Message<?> result = transformer.transform(message);
assertEquals("", result.getHeaders().get(MessageHeaders.CONTENT_TYPE));
assertEquals("text/xml", result.getHeaders().get(MessageHeaders.CONTENT_TYPE));
}
@Test(expected=IllegalArgumentException.class)
public void withProvidedContentTypeAsNull() throws Exception {
ObjectToJsonTransformer transformer = new ObjectToJsonTransformer();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-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.
@@ -28,7 +28,6 @@ import javax.jms.JMSException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.integration.MessageHeaders;
import org.springframework.util.StringUtils;
@@ -47,8 +46,9 @@ import org.springframework.util.StringUtils;
* Note that the JMSMessageID and JMSRedelivered flag are only copied
* <em>from</em> a JMS Message. Those values will <em>not</em> be passed
* along from a Spring Integration Message to an outbound JMS Message.
*
*
* @author Mark Fisher
* @author Gary Russell
*/
public class DefaultJmsHeaderMapper implements JmsHeaderMapper {
@@ -68,9 +68,9 @@ public class DefaultJmsHeaderMapper implements JmsHeaderMapper {
* for any JMS property that is being mapped into the MessageHeaders.
* The Default is an empty string (no prefix).
* <p/>
* This does not affect the JMS properties covered by the specification/API,
* This does not affect the JMS properties covered by the specification/API,
* such as JMSCorrelationID, etc. The header names used for mapping such
* properties are all defined in our {@link JmsHeaders}.
* properties are all defined in our {@link JmsHeaders}.
*/
public void setInboundPrefix(String inboundPrefix) {
this.inboundPrefix = (inboundPrefix != null) ? inboundPrefix : "";
@@ -81,9 +81,9 @@ public class DefaultJmsHeaderMapper implements JmsHeaderMapper {
* integration message header that is being mapped into the JMS Message.
* The Default is an empty string (no prefix).
* <p/>
* This does not affect the JMS properties covered by the specification/API,
* This does not affect the JMS properties covered by the specification/API,
* such as JMSCorrelationID, etc. The header names used for mapping such
* properties are all defined in our {@link JmsHeaders}.
* properties are all defined in our {@link JmsHeaders}.
*/
public void setOutboundPrefix(String outboundPrefix) {
this.outboundPrefix = (outboundPrefix != null) ? outboundPrefix : "";
@@ -228,23 +228,31 @@ public class DefaultJmsHeaderMapper implements JmsHeaderMapper {
/**
* Adds the outbound prefix if necessary.
* Converts {@link MessageHeaders#CONTENT_TYPE} to content_type for JMS compliance.
*/
private String fromHeaderName(String headerName) {
String propertyName = headerName;
if (StringUtils.hasText(this.outboundPrefix) && !propertyName.startsWith(this.outboundPrefix)) {
propertyName = this.outboundPrefix + headerName;
}
else if (MessageHeaders.CONTENT_TYPE.equals(headerName)) {
propertyName = CONTENT_TYPE_PROPERTY;
}
return propertyName;
}
/**
* Adds the inbound prefix if necessary.
* Converts content_type to {@link MessageHeaders#CONTENT_TYPE}.
*/
private String toHeaderName(String propertyName) {
String headerName = propertyName;
if (StringUtils.hasText(this.inboundPrefix) && !headerName.startsWith(this.inboundPrefix)) {
headerName = this.inboundPrefix + propertyName;
}
else if (CONTENT_TYPE_PROPERTY.equals(propertyName)) {
headerName = MessageHeaders.CONTENT_TYPE;
}
return headerName;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-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.
@@ -24,9 +24,14 @@ import org.springframework.integration.mapping.HeaderMapper;
* Strategy interface for mapping integration Message headers to an outbound
* JMS Message (e.g. to configure JMS properties) or extracting integration
* header values from an inbound JMS Message.
*
*
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Gary Russell
*/
public interface JmsHeaderMapper extends HeaderMapper<Message> {}
public interface JmsHeaderMapper extends HeaderMapper<Message> {
static final String CONTENT_TYPE_PROPERTY = "content_type";
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* Copyright 2002-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.
@@ -28,14 +28,13 @@ import javax.jms.Destination;
import javax.jms.JMSException;
import org.junit.Test;
import org.springframework.integration.Message;
import org.springframework.integration.jms.DefaultJmsHeaderMapper;
import org.springframework.integration.jms.JmsHeaders;
import org.springframework.integration.MessageHeaders;
import org.springframework.integration.support.MessageBuilder;
/**
* @author Mark Fisher
* @author Gary Russell
*/
public class DefaultJmsHeaderMapperTests {
@@ -115,6 +114,19 @@ public class DefaultJmsHeaderMapperTests {
assertNull(jmsMessage.getJMSType());
}
@Test
public void testContentTypePropertyMappedFromHeader() throws JMSException {
Message<String> message = MessageBuilder.withPayload("test")
.setHeader(MessageHeaders.CONTENT_TYPE, "foo")
.build();
DefaultJmsHeaderMapper mapper = new DefaultJmsHeaderMapper();
javax.jms.Message jmsMessage = new StubTextMessage();
mapper.fromHeaders(message.getHeaders(), jmsMessage);
Object value = jmsMessage.getObjectProperty("content_type");
assertNotNull(value);
assertEquals("foo", value);
}
@Test
public void testUserDefinedPropertyMappedFromHeader() throws JMSException {
Message<String> message = MessageBuilder.withPayload("test")
@@ -217,6 +229,17 @@ public class DefaultJmsHeaderMapperTests {
assertEquals(timestamp, attrib);
}
@Test
public void testContentTypePropertyMappedToHeader() throws JMSException {
javax.jms.Message jmsMessage = new StubTextMessage();
jmsMessage.setStringProperty("content_type", "foo");
DefaultJmsHeaderMapper mapper = new DefaultJmsHeaderMapper();
Map<String, Object> headers = mapper.toHeaders(jmsMessage);
Object attrib = headers.get(MessageHeaders.CONTENT_TYPE);
assertNotNull(attrib);
assertEquals("foo", attrib);
}
@Test
public void testUserDefinedPropertyMappedToHeader() throws JMSException {
javax.jms.Message jmsMessage = new StubTextMessage();

View File

@@ -230,8 +230,18 @@ public class Kid {
<important>
<para>
Beginning with version 2.2, the <code>object-to-json-transformer</code> sets the <emphasis>content-type</emphasis>
header to <emphasis>application/json</emphasis>, by default. It can be overridden using the <code>content-type</code>
attribute. The default behavior has a side affect - causing applications with the following
header to <code>application/json</code>, by default, if the input message does not already have that header
present.
</para>
<para>
It you wish to set the <emphasis>content type</emphasis> header to some other value, or explicitly overwrite any existing header
with some value (including <code>application/json</code>), use the <code>content-type</code>
attribute. If you wish to suppress the setting of the header, set the <code>content-type</code>
attribute to an empty string (<code>""</code>). This will result in a message with no <code>content-type</code>
header, unless such a header was present on the input message.
</para>
<para>
The behavior of adding the default header has a side affect - causing applications with the following
sequence to fail:
</para>
<para>