From 098342918c39ef51791290c0007135efe3add25e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Sun, 11 Dec 2011 22:09:20 +0000 Subject: [PATCH] reworked JsonMessageConverter contribution into MappingJacksonMessageConverter, aligned with MappingJacksonHttpMessageConverter and MappingJacksonJsonView --- .../converter/DefaultJavaTypeMapper.java | 143 ------- .../jms/support/converter/JavaTypeMapper.java | 30 -- .../converter/JsonMessageConverter.java | 359 ----------------- .../MappingJacksonMessageConverter.java | 373 ++++++++++++++++++ ... MappingJacksonMessageConverterTests.java} | 65 +-- 5 files changed, 391 insertions(+), 579 deletions(-) delete mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/converter/DefaultJavaTypeMapper.java delete mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/converter/JavaTypeMapper.java delete mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/converter/JsonMessageConverter.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/converter/MappingJacksonMessageConverter.java rename org.springframework.jms/src/test/java/org/springframework/jms/support/converter/{JsonMessageConverterTests.java => MappingJacksonMessageConverterTests.java} (70%) diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/DefaultJavaTypeMapper.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/DefaultJavaTypeMapper.java deleted file mode 100644 index 41913daa5f..0000000000 --- a/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/DefaultJavaTypeMapper.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2002-2010 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.jms.support.converter; - -import static org.codehaus.jackson.map.type.TypeFactory.collectionType; -import static org.codehaus.jackson.map.type.TypeFactory.mapType; -import static org.codehaus.jackson.map.type.TypeFactory.type; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; - -import javax.jms.JMSException; -import javax.jms.Message; - -import org.codehaus.jackson.type.JavaType; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.util.ClassUtils; - -/** - * Default implementation of {@link JavaTypeMapper} using hard coded message - * properties to store and retrieve the content type information. - * - * @author Mark Pollack - * @author Sam Nelson - * @author Dave Syer - **/ -public class DefaultJavaTypeMapper implements JavaTypeMapper, InitializingBean { - - public static final String CLASSID_PROPERTY_NAME = "__TypeId__"; - public static final String CONTENT_CLASSID_PROPERTY_NAME = "__ContentTypeId__"; - public static final String KEY_CLASSID_PROPERTY_NAME = "__KeyTypeId__"; - - private Map> idClassMapping = new HashMap>(); - private Map, String> classIdMapping = new HashMap, String>(); - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public JavaType toJavaType(Message message) throws JMSException { - JavaType classType = getClassIdType(retrieveHeader(message, - CLASSID_PROPERTY_NAME)); - if (!classType.isContainerType()) { - return classType; - } - - JavaType contentClassType = getClassIdType(retrieveHeader(message, - CONTENT_CLASSID_PROPERTY_NAME)); - if (classType.getKeyType() == null) { - return collectionType( - (Class) classType.getRawClass(), - contentClassType); - } - - JavaType keyClassType = getClassIdType(retrieveHeader(message, - KEY_CLASSID_PROPERTY_NAME)); - JavaType mapType = mapType( - (Class) classType.getRawClass(), keyClassType, - contentClassType); - return mapType; - - } - - private JavaType getClassIdType(String classId) { - if (this.idClassMapping.containsKey(classId)) { - return type(idClassMapping.get(classId)); - } - - try { - return type(ClassUtils - .forName(classId, getClass().getClassLoader())); - } catch (ClassNotFoundException e) { - throw new MessageConversionException( - "failed to resolve class name. Class not found [" + classId - + "]", e); - } catch (LinkageError e) { - throw new MessageConversionException( - "failed to resolve class name. Linkage error [" + classId - + "]", e); - } - } - - private String retrieveHeader(Message message, String headerName) - throws JMSException { - String classId = message.getStringProperty(headerName); - if (classId == null) { - throw new MessageConversionException( - "failed to convert Message content. Could not resolve " - + headerName + " in header"); - } - return classId; - } - - public void setIdClassMapping(Map> idClassMapping) { - this.idClassMapping = idClassMapping; - } - - public void fromJavaType(JavaType javaType, Message message) - throws JMSException { - addHeader(message, CLASSID_PROPERTY_NAME, - (Class) javaType.getRawClass()); - - if (javaType.isContainerType()) { - addHeader(message, CONTENT_CLASSID_PROPERTY_NAME, javaType - .getContentType().getRawClass()); - } - - if (javaType.getKeyType() != null) { - addHeader(message, KEY_CLASSID_PROPERTY_NAME, javaType.getKeyType() - .getRawClass()); - } - } - - public void afterPropertiesSet() throws Exception { - validateIdTypeMapping(); - } - - private void addHeader(Message message, String headerName, Class clazz) - throws JMSException { - if (classIdMapping.containsKey(clazz)) { - message.setStringProperty(headerName, classIdMapping.get(clazz)); - } else { - message.setStringProperty(headerName, clazz.getName()); - } - } - - private void validateIdTypeMapping() { - Map> finalIdClassMapping = new HashMap>(); - for (Entry> entry : idClassMapping.entrySet()) { - String id = entry.getKey(); - Class clazz = entry.getValue(); - finalIdClassMapping.put(id, clazz); - classIdMapping.put(clazz, id); - } - this.idClassMapping = finalIdClassMapping; - } - -} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/JavaTypeMapper.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/JavaTypeMapper.java deleted file mode 100644 index 31eb7d51f6..0000000000 --- a/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/JavaTypeMapper.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2002-2010 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.jms.support.converter; - -import javax.jms.JMSException; -import javax.jms.Message; - -import org.codehaus.jackson.type.JavaType; - -/** - * Strategy for setting metadata on messages such that one can create the class that needs to be instantiated when - * receiving a message. - * - * @author Dave Syer - * @author Sam Nelson - */ -public interface JavaTypeMapper { - - void fromJavaType(JavaType javaType, Message message) throws JMSException; - - JavaType toJavaType(Message message) throws JMSException; - -} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/JsonMessageConverter.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/JsonMessageConverter.java deleted file mode 100644 index a9bb279d29..0000000000 --- a/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/JsonMessageConverter.java +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Copyright 2002-2010 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.jms.support.converter; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; - -import javax.jms.BytesMessage; -import javax.jms.JMSException; -import javax.jms.Message; -import javax.jms.Session; -import javax.jms.TextMessage; -import javax.xml.transform.Result; - -import org.codehaus.jackson.JsonParseException; -import org.codehaus.jackson.map.DeserializationConfig; -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.springframework.oxm.Marshaller; - -/** - * Message converter that uses the Jackson library to convert messages to and - * from JSON. Maps an object to a {@link BytesMessage}, or to a - * {@link TextMessage} if the {@link #setTargetType marshalTo} is set to - * {@link MessageType#TEXT}. Converts from a {@link TextMessage} or - * {@link BytesMessage} to an object. - * - * @author Mark Pollack - * @author James Carr - * @author Dave Syer - */ -public class JsonMessageConverter implements MessageConverter { - - public static final String DEFAULT_CHARSET = "UTF-8"; - - public static final String DEFAULT_ENCODING_PROPERTY_NAME = "__Encoding__"; - - private volatile String defaultCharset = DEFAULT_CHARSET; - - private MessageType targetType = MessageType.BYTES; - - private ObjectMapper jsonObjectMapper = new ObjectMapper(); - - private JavaTypeMapper javaTypeMapper = new DefaultJavaTypeMapper(); - - private String encodingPropertyName = DEFAULT_ENCODING_PROPERTY_NAME; - - public JsonMessageConverter() { - super(); - initializeJsonObjectMapper(); - } - - /** - * Specify whether {@link #toMessage(Object, Session)} should marshal to a - * {@link BytesMessage} or a {@link TextMessage}. - *

- * The default is {@link MessageType#BYTES}, i.e. this converter marshals to - * a {@link BytesMessage}. Note that the default version of this converter - * supports {@link MessageType#BYTES} and {@link MessageType#TEXT} only. - * - * @see MessageType#BYTES - * @see MessageType#TEXT - */ - public void setTargetType(MessageType targetType) { - this.targetType = targetType; - } - - /** - * A mapper to extract a Jackson {@link JavaType} from a message. - * - * @param javaTypeMapper - * the javaTypeMapper to set - */ - public void setJavaTypeMapper(JavaTypeMapper javaTypeMapper) { - this.javaTypeMapper = javaTypeMapper; - } - - /** - * Specify the default charset to use when converting from and from - * text-based Message body content. If not specified, the charset will be - * "UTF-8". - */ - public void setDefaultCharset(String defaultCharset) { - this.defaultCharset = (defaultCharset != null) ? defaultCharset - : DEFAULT_CHARSET; - } - - /** - * Specify the name of the JMS message property that carries the encoding - * from bytes to String and back is BytesMessage is used during the - * conversion process. - * - * @param encodingPropertyName - * the name of the message property - */ - public void setEncodingPropertyName(String encodingPropertyName) { - this.encodingPropertyName = encodingPropertyName; - } - - /** - * The {@link ObjectMapper} to use instead of using the default. An - * alternative to injecting a mapper is to extend this class and override - * {@link #initializeJsonObjectMapper()}. - * - * @param jsonObjectMapper - * the object mapper to set - */ - public void setJsonObjectMapper(ObjectMapper jsonObjectMapper) { - this.jsonObjectMapper = jsonObjectMapper; - } - - /** - * Subclass and override to customize the mapper. - */ - protected void initializeJsonObjectMapper() { - jsonObjectMapper - .configure( - DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, - false); - } - - public Message toMessage(Object object, Session session) - throws JMSException, MessageConversionException { - Message message; - try { - switch (this.targetType) { - case TEXT: - message = mapToTextMessage(object, session, - this.jsonObjectMapper); - break; - case BYTES: - message = mapToBytesMessage(object, session, - this.jsonObjectMapper); - break; - default: - message = mapToMessage(object, session, this.jsonObjectMapper, - this.targetType); - } - } catch (JsonMappingException ex) { - throw new MessageConversionException("Could not map [" + object - + "]", ex); - } catch (IOException ex) { - throw new MessageConversionException("Could not map [" + object - + "]", ex); - } - javaTypeMapper.fromJavaType(TypeFactory.type(object.getClass()), - message); - return message; - } - - /** - * Map the given object to a {@link TextMessage}. - * @param object the object to be mapped - * @param session current JMS session - * @param jsonObjectMapper the mapper to use - * @return the resulting message - * @throws JMSException if thrown by JMS methods - * @throws IOException in case of I/O errors - * @throws JsonMappingException in case of Jackson mapping errors - * @see Session#createBytesMessage - * @see Marshaller#marshal(Object, Result) - */ - protected Message mapToTextMessage(Object object, Session session, - ObjectMapper jsonObjectMapper) throws JsonMappingException, - IOException, JMSException { - StringWriter writer = new StringWriter(); - jsonObjectMapper.writeValue(writer, object); - return session.createTextMessage(writer.toString()); - } - - /** - * Map the given object to a {@link BytesMessage}. - * @param object the object to be mapped - * @param session current JMS session - * @param jsonObjectMapper the mapper to use - * @return the resulting message - * @throws JMSException if thrown by JMS methods - * @throws IOException in case of I/O errors - * @throws JsonMappingException in case of Jackson mapping errors - * @see Session#createBytesMessage - * @see Marshaller#marshal(Object, Result) - */ - protected Message mapToBytesMessage(Object object, Session session, - ObjectMapper jsonObjectMapper) throws JsonMappingException, - IOException, JMSException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - OutputStreamWriter writer = new OutputStreamWriter(bos, defaultCharset); - jsonObjectMapper.writeValue(writer, object); - BytesMessage message = session.createBytesMessage(); - message.writeBytes(bos.toByteArray()); - message.setStringProperty(encodingPropertyName, defaultCharset); - return message; - } - - /** - * Template method that allows for custom message mapping. Invoked when - * {@link #setTargetType} is not {@link MessageType#TEXT} or - * {@link MessageType#BYTES}. - *

- * The default implementation throws an {@link IllegalArgumentException}. - * - * @param object - * the object to marshal - * @param session - * the JMS session - * @param jsonObjectMapper - * the mapper to use - * @param targetType - * the target message type (other than TEXT or BYTES) - * @return the resulting message - * @throws JMSException - * if thrown by JMS methods - * @throws IOException - * in case of I/O errors - * @throws JsonMappingException - * in case of Jackson mapping errors - */ - protected Message mapToMessage(Object object, Session session, - ObjectMapper jsonObjectMapper, MessageType targetType) - throws JsonMappingException, IOException, JMSException { - throw new IllegalArgumentException("Unsupported message type [" - + targetType + "]. Cannot map to the specified message type."); - } - - public Object fromMessage(Message message) throws JMSException, - MessageConversionException { - Object content = null; - try { - JavaType targetJavaType = javaTypeMapper.toJavaType(message); - content = convertToObject(message, targetJavaType); - } catch (JsonParseException e) { - throw new MessageConversionException( - "Failed to convert Message content", e); - } catch (JsonMappingException e) { - throw new MessageConversionException( - "Failed to convert Message content", e); - } catch (IOException e) { - throw new MessageConversionException( - "Failed to convert Message content", e); - } - return content; - } - - /** - * Convenience method to dispatch to converters for individual message - * types. - */ - private Object convertToObject(Message message, JavaType targetJavaType) - throws JMSException, JsonParseException, JsonMappingException, - IOException { - if (message instanceof TextMessage) { - return convertFromTextMessage((TextMessage) message, targetJavaType); - } else if (message instanceof BytesMessage) { - return convertFromBytesMessage((BytesMessage) message, - targetJavaType); - } else { - return convertFromMessage(message, targetJavaType); - } - } - - /** - * Convert a generic Message to a Java Object with the specified type. - * Default implementation throws IllegalArgumentException. - * - * @param message - * the input message - * @param targetJavaType - * the target type - * @return the message converted to an object - * @throws JsonParseException - * if thrown by Jackson - * @throws JsonMappingException - * if thrown by Jackson - * @throws IOException - * in case of I/O errors - * @throws JMSException - * if thrown by JMS - */ - protected Object convertFromMessage(Message message, JavaType targetJavaType) { - throw new IllegalArgumentException( - "JsonMessageConverter only supports TextMessages and BytesMessages"); - } - - /** - * Convert a TextMessage to a Java Object with the specified type. - * - * @param message - * the input message - * @param targetJavaType - * the target type - * @return the message converted to an object - * @throws JsonParseException - * if thrown by Jackson - * @throws JsonMappingException - * if thrown by Jackson - * @throws IOException - * in case of I/O errors - * @throws JMSException - * if thrown by JMS - */ - protected Object convertFromTextMessage(TextMessage message, - JavaType targetJavaType) throws JsonParseException, - JsonMappingException, IOException, JMSException { - String body = message.getText(); - return jsonObjectMapper.readValue(body, targetJavaType); - } - - /** - * Convert a BytesMessage to a Java Object with the specified type. - * - * @param message - * the input message - * @param targetJavaType - * the target type - * @return the message converted to an object - * @throws JsonParseException - * if thrown by Jackson - * @throws JsonMappingException - * if thrown by Jackson - * @throws IOException - * in case of I/O errors - * @throws JMSException - * if thrown by JMS - */ - protected Object convertFromBytesMessage(BytesMessage message, - JavaType targetJavaType) throws JsonParseException, - JsonMappingException, IOException, JMSException { - String encoding = defaultCharset; - if (message.propertyExists(encodingPropertyName)) { - encoding = message.getStringProperty(encodingPropertyName); - } - byte[] bytes = new byte[(int) message.getBodyLength()]; - message.readBytes(bytes); - try { - String body = new String(bytes, encoding); - return jsonObjectMapper.readValue(body, targetJavaType); - } catch (UnsupportedEncodingException e) { - throw new MessageConversionException( - "Cannot convert bytes to String", e); - } - } - -} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/MappingJacksonMessageConverter.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/MappingJacksonMessageConverter.java new file mode 100644 index 0000000000..1f31f8cead --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/MappingJacksonMessageConverter.java @@ -0,0 +1,373 @@ +/* + * Copyright 2002-2011 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.jms.support.converter; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Map; +import javax.jms.BytesMessage; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.Session; +import javax.jms.TextMessage; +import javax.xml.transform.Result; + +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.type.TypeFactory; +import org.codehaus.jackson.type.JavaType; + +import org.springframework.oxm.Marshaller; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Message converter that uses the Jackson library to convert messages to and from JSON. + * Maps an object to a {@link BytesMessage}, or to a {@link TextMessage} if the + * {@link #setTargetType marshalTo} is set to {@link MessageType#TEXT}. + * Converts from a {@link TextMessage} or {@link BytesMessage} to an object. + * + * @author Mark Pollack + * @author Dave Syer + * @author Juergen Hoeller + * @since 3.1 + */ +public class MappingJacksonMessageConverter implements MessageConverter { + + /** + * The default encoding used for writing to text messages: UTF-8. + */ + public static final String DEFAULT_ENCODING = "UTF-8"; + + + private ObjectMapper objectMapper = new ObjectMapper(); + + private MessageType targetType = MessageType.BYTES; + + private String encoding = DEFAULT_ENCODING; + + private String encodingPropertyName; + + private String typeIdPropertyName; + + private Map> idClassMappings = new HashMap>(); + + private Map, String> classIdMappings = new HashMap, String>(); + + + /** + * Specify the {@link ObjectMapper} to use instead of using the default. + */ + public void setObjectMapper(ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "ObjectMapper must not be null"); + this.objectMapper = objectMapper; + } + + /** + * Specify whether {@link #toMessage(Object, Session)} should marshal to a + * {@link BytesMessage} or a {@link TextMessage}. + *

The default is {@link MessageType#BYTES}, i.e. this converter marshals to + * a {@link BytesMessage}. Note that the default version of this converter + * supports {@link MessageType#BYTES} and {@link MessageType#TEXT} only. + * @see MessageType#BYTES + * @see MessageType#TEXT + */ + public void setTargetType(MessageType targetType) { + Assert.notNull(targetType, "MessageType must not be null"); + this.targetType = targetType; + } + + /** + * Specify the encoding to use when converting to and from text-based + * message body content. The default encoding will be "UTF-8". + *

When reading from a a text-based message, an encoding may have been + * suggested through a special JMS property which will then be preferred + * over the encoding set on this MessageConverter instance. + * @see #setEncodingPropertyName + */ + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + /** + * Specify the name of the JMS message property that carries the encoding from + * bytes to String and back is BytesMessage is used during the conversion process. + *

Default is none. Setting this property is optional; if not set, UTF-8 will + * be used for decoding any incoming bytes message. + * @see #setEncoding + */ + public void setEncodingPropertyName(String encodingPropertyName) { + this.encodingPropertyName = encodingPropertyName; + } + + /** + * Specify the name of the JMS message property that carries the type id for the + * contained object: either a mapped id value or a raw Java class name. + *

Default is none. NOTE: This property needs to be set in order to allow + * for converting from an incoming message to a Java object. + * @see #setTypeIdMappings + */ + public void setTypeIdPropertyName(String typeIdPropertyName) { + this.typeIdPropertyName = typeIdPropertyName; + } + + /** + * Specify mappings from type ids to Java classes, if desired. + * This allows for synthetic ids in the type id message property, + * instead of transferring Java class names. + *

Default is no custom mappings, i.e. transferring raw Java class names. + * @param typeIdMappings a Map with type id values as keys and Java classes as values + */ + public void setTypeIdMappings(Map> typeIdMappings) { + this.idClassMappings = new HashMap>(); + for (Map.Entry> entry : typeIdMappings.entrySet()) { + String id = entry.getKey(); + Class clazz = entry.getValue(); + this.idClassMappings.put(id, clazz); + this.classIdMappings.put(clazz, id); + } + } + + + public Message toMessage(Object object, Session session) throws JMSException, MessageConversionException { + Message message; + try { + switch (this.targetType) { + case TEXT: + message = mapToTextMessage(object, session, this.objectMapper); + break; + case BYTES: + message = mapToBytesMessage(object, session, this.objectMapper); + break; + default: + message = mapToMessage(object, session, this.objectMapper, this.targetType); + } + } + catch (IOException ex) { + throw new MessageConversionException("Could not map JSON object [" + object + "]", ex); + } + setTypeIdOnMessage(object, message); + return message; + } + + public Object fromMessage(Message message) throws JMSException, MessageConversionException { + try { + JavaType targetJavaType = getJavaTypeForMessage(message); + return convertToObject(message, targetJavaType); + } + catch (IOException ex) { + throw new MessageConversionException("Failed to convert JSON message content", ex); + } + } + + + /** + * Map the given object to a {@link TextMessage}. + * @param object the object to be mapped + * @param session current JMS session + * @param objectMapper the mapper to use + * @return the resulting message + * @throws JMSException if thrown by JMS methods + * @throws IOException in case of I/O errors + * @see Session#createBytesMessage + * @see Marshaller#marshal(Object, Result) + */ + protected TextMessage mapToTextMessage(Object object, Session session, ObjectMapper objectMapper) + throws JMSException, IOException { + + StringWriter writer = new StringWriter(); + objectMapper.writeValue(writer, object); + return session.createTextMessage(writer.toString()); + } + + /** + * Map the given object to a {@link BytesMessage}. + * @param object the object to be mapped + * @param session current JMS session + * @param objectMapper the mapper to use + * @return the resulting message + * @throws JMSException if thrown by JMS methods + * @throws IOException in case of I/O errors + * @see Session#createBytesMessage + * @see Marshaller#marshal(Object, Result) + */ + protected BytesMessage mapToBytesMessage(Object object, Session session, ObjectMapper objectMapper) + throws JMSException, IOException { + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(bos, this.encoding); + objectMapper.writeValue(writer, object); + + BytesMessage message = session.createBytesMessage(); + message.writeBytes(bos.toByteArray()); + if (this.encodingPropertyName != null) { + message.setStringProperty(this.encodingPropertyName, this.encoding); + } + return message; + } + + /** + * Template method that allows for custom message mapping. + * Invoked when {@link #setTargetType} is not {@link MessageType#TEXT} or + * {@link MessageType#BYTES}. + *

The default implementation throws an {@link IllegalArgumentException}. + * @param object the object to marshal + * @param session the JMS Session + * @param objectMapper the mapper to use + * @param targetType the target message type (other than TEXT or BYTES) + * @return the resulting message + * @throws JMSException if thrown by JMS methods + * @throws IOException in case of I/O errors + */ + protected Message mapToMessage(Object object, Session session, ObjectMapper objectMapper, MessageType targetType) + throws JMSException, IOException { + + throw new IllegalArgumentException("Unsupported message type [" + targetType + + "]. MappingJacksonMessageConverter by default only supports TextMessages and BytesMessages."); + } + + /** + * Set a type id for the given payload object on the given JMS Message. + *

The default implementation consults the configured type id mapping and + * sets the resulting value (either a mapped id or the raw Java class name) + * into the configured type id message property. + * @param object the payload object to set a type id for + * @param message the JMS Message to set the type id on + * @throws JMSException if thrown by JMS methods + * @see #getJavaTypeForMessage(javax.jms.Message) + * @see #setTypeIdPropertyName(String) + * @see #setTypeIdMappings(java.util.Map) + */ + protected void setTypeIdOnMessage(Object object, Message message) throws JMSException { + if (this.typeIdPropertyName != null) { + String typeId = this.classIdMappings.get(object.getClass()); + if (typeId == null) { + typeId = object.getClass().getName(); + } + message.setStringProperty(this.typeIdPropertyName, typeId); + } + } + + + /** + * Convenience method to dispatch to converters for individual message types. + */ + private Object convertToObject(Message message, JavaType targetJavaType) throws JMSException, IOException { + if (message instanceof TextMessage) { + return convertFromTextMessage((TextMessage) message, targetJavaType); + } + else if (message instanceof BytesMessage) { + return convertFromBytesMessage((BytesMessage) message, targetJavaType); + } + else { + return convertFromMessage(message, targetJavaType); + } + } + + /** + * Convert a TextMessage to a Java Object with the specified type. + * @param message the input message + * @param targetJavaType the target type + * @return the message converted to an object + * @throws JMSException if thrown by JMS + * @throws IOException in case of I/O errors + */ + protected Object convertFromTextMessage(TextMessage message, JavaType targetJavaType) + throws JMSException, IOException { + + String body = message.getText(); + return this.objectMapper.readValue(body, targetJavaType); + } + + /** + * Convert a BytesMessage to a Java Object with the specified type. + * @param message the input message + * @param targetJavaType the target type + * @return the message converted to an object + * @throws JMSException if thrown by JMS + * @throws IOException in case of I/O errors + */ + protected Object convertFromBytesMessage(BytesMessage message, JavaType targetJavaType) + throws JMSException, IOException { + + String encoding = this.encoding; + if (this.encodingPropertyName != null && message.propertyExists(this.encodingPropertyName)) { + encoding = message.getStringProperty(this.encodingPropertyName); + } + byte[] bytes = new byte[(int) message.getBodyLength()]; + message.readBytes(bytes); + try { + String body = new String(bytes, encoding); + return this.objectMapper.readValue(body, targetJavaType); + } + catch (UnsupportedEncodingException ex) { + throw new MessageConversionException("Cannot convert bytes to String", ex); + } + } + + /** + * Template method that allows for custom message mapping. + * Invoked when {@link #setTargetType} is not {@link MessageType#TEXT} or + * {@link MessageType#BYTES}. + *

The default implementation throws an {@link IllegalArgumentException}. + * @param message the input message + * @param targetJavaType the target type + * @return the message converted to an object + * @throws JMSException if thrown by JMS + * @throws IOException in case of I/O errors + */ + protected Object convertFromMessage(Message message, JavaType targetJavaType) + throws JMSException, IOException { + + throw new IllegalArgumentException("Unsupported message type [" + message.getClass() + + "]. MappingJacksonMessageConverter by default only supports TextMessages and BytesMessages."); + } + + /** + * Determine a Jackson JavaType for the given JMS Message, + * typically parsing a type id message property. + *

The default implementation parses the configured type id property name + * and consults the configured type id mapping. This can be overridden with + * a different strategy, e.g. doing some heuristics based on message origin. + * @param object the payload object to set a type id for + * @param message the JMS Message to set the type id on + * @throws JMSException if thrown by JMS methods + * @see #setTypeIdOnMessage(Object, javax.jms.Message) + * @see #setTypeIdPropertyName(String) + * @see #setTypeIdMappings(java.util.Map) + */ + protected JavaType getJavaTypeForMessage(Message message) throws JMSException { + String typeId = message.getStringProperty(this.typeIdPropertyName); + if (typeId == null) { + throw new MessageConversionException("Could not find type id property [" + this.typeIdPropertyName + "]"); + } + Class mappedClass = this.idClassMappings.get(typeId); + if (mappedClass != null) { + return TypeFactory.type(mappedClass); + } + try { + return TypeFactory.type(ClassUtils.forName(typeId, getClass().getClassLoader())); + } + catch (Throwable ex) { + throw new MessageConversionException("Failed to resolve type id [" + typeId + "]", ex); + } + } + +} diff --git a/org.springframework.jms/src/test/java/org/springframework/jms/support/converter/JsonMessageConverterTests.java b/org.springframework.jms/src/test/java/org/springframework/jms/support/converter/MappingJacksonMessageConverterTests.java similarity index 70% rename from org.springframework.jms/src/test/java/org/springframework/jms/support/converter/JsonMessageConverterTests.java rename to org.springframework.jms/src/test/java/org/springframework/jms/support/converter/MappingJacksonMessageConverterTests.java index 9e1c4ad9ce..bf682af413 100644 --- a/org.springframework.jms/src/test/java/org/springframework/jms/support/converter/JsonMessageConverterTests.java +++ b/org.springframework.jms/src/test/java/org/springframework/jms/support/converter/MappingJacksonMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2011 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,9 @@ package org.springframework.jms.support.converter; -import static org.easymock.EasyMock.createMock; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.isA; -import static org.easymock.EasyMock.replay; -import static org.easymock.EasyMock.verify; -import static org.junit.Assert.assertEquals; - import java.util.Collections; import java.util.HashMap; import java.util.Map; - import javax.jms.BytesMessage; import javax.jms.Session; import javax.jms.TextMessage; @@ -36,20 +28,25 @@ import org.easymock.EasyMock; import org.junit.Before; import org.junit.Test; +import static org.easymock.EasyMock.*; +import static org.junit.Assert.*; + /** * @author Arjen Poutsma * @author Dave Syer */ -public class JsonMessageConverterTests { +public class MappingJacksonMessageConverterTests { - private JsonMessageConverter converter; + private MappingJacksonMessageConverter converter; private Session sessionMock; @Before public void setUp() throws Exception { sessionMock = createMock(Session.class); - converter = new JsonMessageConverter(); + converter = new MappingJacksonMessageConverter(); + converter.setEncodingPropertyName("__encoding__"); + converter.setTypeIdPropertyName("__typeid__"); } @Test @@ -58,11 +55,8 @@ public class JsonMessageConverterTests { Object toBeMarshalled = new Object(); expect(sessionMock.createBytesMessage()).andReturn(bytesMessageMock); - bytesMessageMock.setStringProperty( - JsonMessageConverter.DEFAULT_ENCODING_PROPERTY_NAME, "UTF-8"); - bytesMessageMock.setStringProperty( - DefaultJavaTypeMapper.CLASSID_PROPERTY_NAME, - Object.class.getName()); + bytesMessageMock.setStringProperty("__encoding__", "UTF-8"); + bytesMessageMock.setStringProperty("__typeid__", Object.class.getName()); bytesMessageMock.writeBytes(isA(byte[].class)); replay(sessionMock, bytesMessageMock); @@ -88,12 +82,10 @@ public class JsonMessageConverterTests { }; expect( - bytesMessageMock - .getStringProperty(DefaultJavaTypeMapper.CLASSID_PROPERTY_NAME)) + bytesMessageMock.getStringProperty("__typeid__")) .andReturn(Object.class.getName()); expect( - bytesMessageMock - .propertyExists(JsonMessageConverter.DEFAULT_ENCODING_PROPERTY_NAME)) + bytesMessageMock.propertyExists("__encoding__")) .andReturn(false); expect(bytesMessageMock.getBodyLength()).andReturn( new Long(bytes.length)); @@ -114,11 +106,8 @@ public class JsonMessageConverterTests { TextMessage textMessageMock = createMock(TextMessage.class); Object toBeMarshalled = new Object(); - textMessageMock.setStringProperty( - DefaultJavaTypeMapper.CLASSID_PROPERTY_NAME, - Object.class.getName()); - expect(sessionMock.createTextMessage(isA(String.class))).andReturn( - textMessageMock); + textMessageMock.setStringProperty("__typeid__", Object.class.getName()); + expect(sessionMock.createTextMessage(isA(String.class))).andReturn( textMessageMock); replay(sessionMock, textMessageMock); @@ -134,15 +123,7 @@ public class JsonMessageConverterTests { Map toBeMarshalled = new HashMap(); toBeMarshalled.put("foo", "bar"); - textMessageMock.setStringProperty( - DefaultJavaTypeMapper.CLASSID_PROPERTY_NAME, - HashMap.class.getName()); - textMessageMock.setStringProperty( - DefaultJavaTypeMapper.CONTENT_CLASSID_PROPERTY_NAME, - Object.class.getName()); - textMessageMock.setStringProperty( - DefaultJavaTypeMapper.KEY_CLASSID_PROPERTY_NAME, - Object.class.getName()); + textMessageMock.setStringProperty("__typeid__", HashMap.class.getName()); expect(sessionMock.createTextMessage(isA(String.class))).andReturn( textMessageMock); @@ -161,8 +142,7 @@ public class JsonMessageConverterTests { String text = "{\"foo\":\"bar\"}"; expect( - textMessageMock - .getStringProperty(DefaultJavaTypeMapper.CLASSID_PROPERTY_NAME)) + textMessageMock.getStringProperty("__typeid__")) .andReturn(Object.class.getName()); expect(textMessageMock.getText()).andReturn(text); @@ -182,17 +162,8 @@ public class JsonMessageConverterTests { String text = "{\"foo\":\"bar\"}"; expect( - textMessageMock - .getStringProperty(DefaultJavaTypeMapper.CLASSID_PROPERTY_NAME)) + textMessageMock.getStringProperty("__typeid__")) .andReturn(HashMap.class.getName()); - expect( - textMessageMock - .getStringProperty(DefaultJavaTypeMapper.CONTENT_CLASSID_PROPERTY_NAME)) - .andReturn(Object.class.getName()); - expect( - textMessageMock - .getStringProperty(DefaultJavaTypeMapper.KEY_CLASSID_PROPERTY_NAME)) - .andReturn(Object.class.getName()); expect(textMessageMock.getText()).andReturn(text); replay(sessionMock, textMessageMock);