Skip to content

Commit b73a7a8

Browse files
committed
Added MappingJackson2MessageConverter for JMS
Issue: SPR-10099
1 parent c954d10 commit b73a7a8

File tree

5 files changed

+554
-5
lines changed

5 files changed

+554
-5
lines changed

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,10 +335,11 @@ project("spring-jms") {
335335
compile(project(":spring-tx"))
336336
optional(project(":spring-oxm"))
337337
compile("aopalliance:aopalliance:1.0")
338-
optional("org.codehaus.jackson:jackson-mapper-asl:1.4.2")
339338
provided("org.apache.geronimo.specs:geronimo-jms_1.1_spec:1.1")
340339
optional("org.apache.geronimo.specs:geronimo-jta_1.1_spec:1.1")
341340
optional("javax.resource:connector-api:1.5")
341+
optional("org.codehaus.jackson:jackson-mapper-asl:1.4.2")
342+
optional("com.fasterxml.jackson.core:jackson-databind:2.0.1")
342343
}
343344
}
344345

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

0 commit comments

Comments
 (0)