1 /* 2 * Copyright 2002-2011 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.springframework.jms.support.converter; 18 19 import java.io.ByteArrayOutputStream; 20 import java.io.IOException; 21 import java.io.OutputStreamWriter; 22 import java.io.StringWriter; 23 import java.io.UnsupportedEncodingException; 24 import java.util.HashMap; 25 import java.util.Map; 26 import javax.jms.BytesMessage; 27 import javax.jms.JMSException; 28 import javax.jms.Message; 29 import javax.jms.Session; 30 import javax.jms.TextMessage; 31 import javax.xml.transform.Result; 32 33 import org.codehaus.jackson.map.ObjectMapper; 34 import org.codehaus.jackson.map.type.TypeFactory; 35 import org.codehaus.jackson.type.JavaType; 36 37 import org.springframework.oxm.Marshaller; 38 import org.springframework.util.Assert; 39 import org.springframework.util.ClassUtils; 40 41 /** 42 * Message converter that uses the Jackson library to convert messages to and from JSON. 43 * Maps an object to a {@link BytesMessage}, or to a {@link TextMessage} if the 44 * {@link #setTargetType targetType} is set to {@link MessageType#TEXT}. 45 * Converts from a {@link TextMessage} or {@link BytesMessage} to an object. 46 * 47 * @author Mark Pollack 48 * @author Dave Syer 49 * @author Juergen Hoeller 50 * @since 3.1 51 */ 52 public class MappingJacksonMessageConverter implements MessageConverter { 53 54 /** 55 * The default encoding used for writing to text messages: UTF-8. 56 */ 57 public static final String DEFAULT_ENCODING = "UTF-8"; 58 59 60 private ObjectMapper objectMapper = new ObjectMapper(); 61 62 private MessageType targetType = MessageType.BYTES; 63 64 private String encoding = DEFAULT_ENCODING; 65 66 private String encodingPropertyName; 67 68 private String typeIdPropertyName; 69 70 private Map<String, Class<?>> idClassMappings = new HashMap<String, Class<?>>(); 71 72 private Map<Class<?>, String> classIdMappings = new HashMap<Class<?>, String>(); 73 74 75 /** 76 * Specify the {@link ObjectMapper} to use instead of using the default. 77 */ setObjectMapper(ObjectMapper objectMapper)78 public void setObjectMapper(ObjectMapper objectMapper) { 79 Assert.notNull(objectMapper, "ObjectMapper must not be null"); 80 this.objectMapper = objectMapper; 81 } 82 83 /** 84 * Specify whether {@link #toMessage(Object, Session)} should marshal to a 85 * {@link BytesMessage} or a {@link TextMessage}. 86 * <p>The default is {@link MessageType#BYTES}, i.e. this converter marshals to 87 * a {@link BytesMessage}. Note that the default version of this converter 88 * supports {@link MessageType#BYTES} and {@link MessageType#TEXT} only. 89 * @see MessageType#BYTES 90 * @see MessageType#TEXT 91 */ setTargetType(MessageType targetType)92 public void setTargetType(MessageType targetType) { 93 Assert.notNull(targetType, "MessageType must not be null"); 94 this.targetType = targetType; 95 } 96 97 /** 98 * Specify the encoding to use when converting to and from text-based 99 * message body content. The default encoding will be "UTF-8". 100 * <p>When reading from a a text-based message, an encoding may have been 101 * suggested through a special JMS property which will then be preferred 102 * over the encoding set on this MessageConverter instance. 103 * @see #setEncodingPropertyName 104 */ setEncoding(String encoding)105 public void setEncoding(String encoding) { 106 this.encoding = encoding; 107 } 108 109 /** 110 * Specify the name of the JMS message property that carries the encoding from 111 * bytes to String and back is BytesMessage is used during the conversion process. 112 * <p>Default is none. Setting this property is optional; if not set, UTF-8 will 113 * be used for decoding any incoming bytes message. 114 * @see #setEncoding 115 */ setEncodingPropertyName(String encodingPropertyName)116 public void setEncodingPropertyName(String encodingPropertyName) { 117 this.encodingPropertyName = encodingPropertyName; 118 } 119 120 /** 121 * Specify the name of the JMS message property that carries the type id for the 122 * contained object: either a mapped id value or a raw Java class name. 123 * <p>Default is none. <b>NOTE: This property needs to be set in order to allow 124 * for converting from an incoming message to a Java object.</b> 125 * @see #setTypeIdMappings 126 */ setTypeIdPropertyName(String typeIdPropertyName)127 public void setTypeIdPropertyName(String typeIdPropertyName) { 128 this.typeIdPropertyName = typeIdPropertyName; 129 } 130 131 /** 132 * Specify mappings from type ids to Java classes, if desired. 133 * This allows for synthetic ids in the type id message property, 134 * instead of transferring Java class names. 135 * <p>Default is no custom mappings, i.e. transferring raw Java class names. 136 * @param typeIdMappings a Map with type id values as keys and Java classes as values 137 */ setTypeIdMappings(Map<String, Class<?>> typeIdMappings)138 public void setTypeIdMappings(Map<String, Class<?>> typeIdMappings) { 139 this.idClassMappings = new HashMap<String, Class<?>>(); 140 for (Map.Entry<String, Class<?>> entry : typeIdMappings.entrySet()) { 141 String id = entry.getKey(); 142 Class<?> clazz = entry.getValue(); 143 this.idClassMappings.put(id, clazz); 144 this.classIdMappings.put(clazz, id); 145 } 146 } 147 148 toMessage(Object object, Session session)149 public Message toMessage(Object object, Session session) throws JMSException, MessageConversionException { 150 Message message; 151 try { 152 switch (this.targetType) { 153 case TEXT: 154 message = mapToTextMessage(object, session, this.objectMapper); 155 break; 156 case BYTES: 157 message = mapToBytesMessage(object, session, this.objectMapper); 158 break; 159 default: 160 message = mapToMessage(object, session, this.objectMapper, this.targetType); 161 } 162 } 163 catch (IOException ex) { 164 throw new MessageConversionException("Could not map JSON object [" + object + "]", ex); 165 } 166 setTypeIdOnMessage(object, message); 167 return message; 168 } 169 fromMessage(Message message)170 public Object fromMessage(Message message) throws JMSException, MessageConversionException { 171 try { 172 JavaType targetJavaType = getJavaTypeForMessage(message); 173 return convertToObject(message, targetJavaType); 174 } 175 catch (IOException ex) { 176 throw new MessageConversionException("Failed to convert JSON message content", ex); 177 } 178 } 179 180 181 /** 182 * Map the given object to a {@link TextMessage}. 183 * @param object the object to be mapped 184 * @param session current JMS session 185 * @param objectMapper the mapper to use 186 * @return the resulting message 187 * @throws JMSException if thrown by JMS methods 188 * @throws IOException in case of I/O errors 189 * @see Session#createBytesMessage 190 * @see Marshaller#marshal(Object, Result) 191 */ mapToTextMessage(Object object, Session session, ObjectMapper objectMapper)192 protected TextMessage mapToTextMessage(Object object, Session session, ObjectMapper objectMapper) 193 throws JMSException, IOException { 194 195 StringWriter writer = new StringWriter(); 196 objectMapper.writeValue(writer, object); 197 return session.createTextMessage(writer.toString()); 198 } 199 200 /** 201 * Map the given object to a {@link BytesMessage}. 202 * @param object the object to be mapped 203 * @param session current JMS session 204 * @param objectMapper the mapper to use 205 * @return the resulting message 206 * @throws JMSException if thrown by JMS methods 207 * @throws IOException in case of I/O errors 208 * @see Session#createBytesMessage 209 * @see Marshaller#marshal(Object, Result) 210 */ mapToBytesMessage(Object object, Session session, ObjectMapper objectMapper)211 protected BytesMessage mapToBytesMessage(Object object, Session session, ObjectMapper objectMapper) 212 throws JMSException, IOException { 213 214 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 215 OutputStreamWriter writer = new OutputStreamWriter(bos, this.encoding); 216 objectMapper.writeValue(writer, object); 217 218 BytesMessage message = session.createBytesMessage(); 219 message.writeBytes(bos.toByteArray()); 220 if (this.encodingPropertyName != null) { 221 message.setStringProperty(this.encodingPropertyName, this.encoding); 222 } 223 return message; 224 } 225 226 /** 227 * Template method that allows for custom message mapping. 228 * Invoked when {@link #setTargetType} is not {@link MessageType#TEXT} or 229 * {@link MessageType#BYTES}. 230 * <p>The default implementation throws an {@link IllegalArgumentException}. 231 * @param object the object to marshal 232 * @param session the JMS Session 233 * @param objectMapper the mapper to use 234 * @param targetType the target message type (other than TEXT or BYTES) 235 * @return the resulting message 236 * @throws JMSException if thrown by JMS methods 237 * @throws IOException in case of I/O errors 238 */ mapToMessage(Object object, Session session, ObjectMapper objectMapper, MessageType targetType)239 protected Message mapToMessage(Object object, Session session, ObjectMapper objectMapper, MessageType targetType) 240 throws JMSException, IOException { 241 242 throw new IllegalArgumentException("Unsupported message type [" + targetType + 243 "]. MappingJacksonMessageConverter by default only supports TextMessages and BytesMessages."); 244 } 245 246 /** 247 * Set a type id for the given payload object on the given JMS Message. 248 * <p>The default implementation consults the configured type id mapping and 249 * sets the resulting value (either a mapped id or the raw Java class name) 250 * into the configured type id message property. 251 * @param object the payload object to set a type id for 252 * @param message the JMS Message to set the type id on 253 * @throws JMSException if thrown by JMS methods 254 * @see #getJavaTypeForMessage(javax.jms.Message) 255 * @see #setTypeIdPropertyName(String) 256 * @see #setTypeIdMappings(java.util.Map) 257 */ setTypeIdOnMessage(Object object, Message message)258 protected void setTypeIdOnMessage(Object object, Message message) throws JMSException { 259 if (this.typeIdPropertyName != null) { 260 String typeId = this.classIdMappings.get(object.getClass()); 261 if (typeId == null) { 262 typeId = object.getClass().getName(); 263 } 264 message.setStringProperty(this.typeIdPropertyName, typeId); 265 } 266 } 267 268 269 /** 270 * Convenience method to dispatch to converters for individual message types. 271 */ convertToObject(Message message, JavaType targetJavaType)272 private Object convertToObject(Message message, JavaType targetJavaType) throws JMSException, IOException { 273 if (message instanceof TextMessage) { 274 return convertFromTextMessage((TextMessage) message, targetJavaType); 275 } 276 else if (message instanceof BytesMessage) { 277 return convertFromBytesMessage((BytesMessage) message, targetJavaType); 278 } 279 else { 280 return convertFromMessage(message, targetJavaType); 281 } 282 } 283 284 /** 285 * Convert a TextMessage to a Java Object with the specified type. 286 * @param message the input message 287 * @param targetJavaType the target type 288 * @return the message converted to an object 289 * @throws JMSException if thrown by JMS 290 * @throws IOException in case of I/O errors 291 */ convertFromTextMessage(TextMessage message, JavaType targetJavaType)292 protected Object convertFromTextMessage(TextMessage message, JavaType targetJavaType) 293 throws JMSException, IOException { 294 295 String body = message.getText(); 296 return this.objectMapper.readValue(body, targetJavaType); 297 } 298 299 /** 300 * Convert a BytesMessage to a Java Object with the specified type. 301 * @param message the input message 302 * @param targetJavaType the target type 303 * @return the message converted to an object 304 * @throws JMSException if thrown by JMS 305 * @throws IOException in case of I/O errors 306 */ convertFromBytesMessage(BytesMessage message, JavaType targetJavaType)307 protected Object convertFromBytesMessage(BytesMessage message, JavaType targetJavaType) 308 throws JMSException, IOException { 309 310 String encoding = this.encoding; 311 if (this.encodingPropertyName != null && message.propertyExists(this.encodingPropertyName)) { 312 encoding = message.getStringProperty(this.encodingPropertyName); 313 } 314 byte[] bytes = new byte[(int) message.getBodyLength()]; 315 message.readBytes(bytes); 316 try { 317 String body = new String(bytes, encoding); 318 return this.objectMapper.readValue(body, targetJavaType); 319 } 320 catch (UnsupportedEncodingException ex) { 321 throw new MessageConversionException("Cannot convert bytes to String", ex); 322 } 323 } 324 325 /** 326 * Template method that allows for custom message mapping. 327 * Invoked when {@link #setTargetType} is not {@link MessageType#TEXT} or 328 * {@link MessageType#BYTES}. 329 * <p>The default implementation throws an {@link IllegalArgumentException}. 330 * @param message the input message 331 * @param targetJavaType the target type 332 * @return the message converted to an object 333 * @throws JMSException if thrown by JMS 334 * @throws IOException in case of I/O errors 335 */ convertFromMessage(Message message, JavaType targetJavaType)336 protected Object convertFromMessage(Message message, JavaType targetJavaType) 337 throws JMSException, IOException { 338 339 throw new IllegalArgumentException("Unsupported message type [" + message.getClass() + 340 "]. MappingJacksonMessageConverter by default only supports TextMessages and BytesMessages."); 341 } 342 343 /** 344 * Determine a Jackson JavaType for the given JMS Message, 345 * typically parsing a type id message property. 346 * <p>The default implementation parses the configured type id property name 347 * and consults the configured type id mapping. This can be overridden with 348 * a different strategy, e.g. doing some heuristics based on message origin. 349 * @param object the payload object to set a type id for 350 * @param message the JMS Message to set the type id on 351 * @throws JMSException if thrown by JMS methods 352 * @see #setTypeIdOnMessage(Object, javax.jms.Message) 353 * @see #setTypeIdPropertyName(String) 354 * @see #setTypeIdMappings(java.util.Map) 355 */ getJavaTypeForMessage(Message message)356 protected JavaType getJavaTypeForMessage(Message message) throws JMSException { 357 String typeId = message.getStringProperty(this.typeIdPropertyName); 358 if (typeId == null) { 359 throw new MessageConversionException("Could not find type id property [" + this.typeIdPropertyName + "]"); 360 } 361 Class mappedClass = this.idClassMappings.get(typeId); 362 if (mappedClass != null) { 363 return TypeFactory.type(mappedClass); 364 } 365 try { 366 return TypeFactory.type(ClassUtils.forName(typeId, getClass().getClassLoader())); 367 } 368 catch (Throwable ex) { 369 throw new MessageConversionException("Failed to resolve type id [" + typeId + "]", ex); 370 } 371 } 372 373 } 374