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