001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.camel.component.jms;
018
019 import java.io.File;
020 import java.io.InputStream;
021 import java.io.Reader;
022 import java.io.Serializable;
023 import java.nio.ByteBuffer;
024 import java.util.Date;
025 import java.util.Enumeration;
026 import java.util.HashMap;
027 import java.util.Map;
028 import java.util.Set;
029
030 import javax.jms.BytesMessage;
031 import javax.jms.Destination;
032 import javax.jms.JMSException;
033 import javax.jms.MapMessage;
034 import javax.jms.Message;
035 import javax.jms.MessageFormatException;
036 import javax.jms.ObjectMessage;
037 import javax.jms.Session;
038 import javax.jms.StreamMessage;
039 import javax.jms.TextMessage;
040
041 import org.w3c.dom.Node;
042
043 import org.apache.camel.CamelContext;
044 import org.apache.camel.Exchange;
045 import org.apache.camel.NoTypeConversionAvailableException;
046 import org.apache.camel.RuntimeCamelException;
047 import org.apache.camel.StreamCache;
048 import org.apache.camel.component.file.GenericFile;
049 import org.apache.camel.impl.DefaultExchangeHolder;
050 import org.apache.camel.spi.HeaderFilterStrategy;
051 import org.apache.camel.util.CamelContextHelper;
052 import org.apache.camel.util.ExchangeHelper;
053 import org.apache.camel.util.ObjectHelper;
054 import org.apache.commons.logging.Log;
055 import org.apache.commons.logging.LogFactory;
056
057 import static org.apache.camel.component.jms.JmsMessageType.Bytes;
058 import static org.apache.camel.component.jms.JmsMessageType.Map;
059 import static org.apache.camel.component.jms.JmsMessageType.Object;
060 import static org.apache.camel.component.jms.JmsMessageType.Text;
061
062 /**
063 * A Strategy used to convert between a Camel {@link Exchange} and {@link JmsMessage}
064 * to and from a JMS {@link Message}
065 *
066 * @version $Revision: 20285 $
067 */
068 public class JmsBinding {
069 private static final transient Log LOG = LogFactory.getLog(JmsBinding.class);
070 private final JmsEndpoint endpoint;
071 private final HeaderFilterStrategy headerFilterStrategy;
072 private final JmsKeyFormatStrategy jmsKeyFormatStrategy;
073
074 public JmsBinding() {
075 this.endpoint = null;
076 headerFilterStrategy = new JmsHeaderFilterStrategy();
077 jmsKeyFormatStrategy = new DefaultJmsKeyFormatStrategy();
078 }
079
080 public JmsBinding(JmsEndpoint endpoint) {
081 this.endpoint = endpoint;
082 if (endpoint.getHeaderFilterStrategy() != null) {
083 headerFilterStrategy = endpoint.getHeaderFilterStrategy();
084 } else {
085 headerFilterStrategy = new JmsHeaderFilterStrategy();
086 }
087 if (endpoint.getJmsKeyFormatStrategy() != null) {
088 jmsKeyFormatStrategy = endpoint.getJmsKeyFormatStrategy();
089 } else {
090 jmsKeyFormatStrategy = new DefaultJmsKeyFormatStrategy();
091 }
092 }
093
094 /**
095 * Extracts the body from the JMS message
096 *
097 * @param exchange the exchange
098 * @param message the message to extract its body
099 * @return the body, can be <tt>null</tt>
100 */
101 public Object extractBodyFromJms(Exchange exchange, Message message) {
102 try {
103 // is a custom message converter configured on endpoint then use it instead of doing the extraction
104 // based on message type
105 if (endpoint != null && endpoint.getMessageConverter() != null) {
106 if (LOG.isTraceEnabled()) {
107 LOG.trace("Extracting body using a custom MessageConverter: " + endpoint.getMessageConverter() + " from JMS message: " + message);
108 }
109 return endpoint.getMessageConverter().fromMessage(message);
110 }
111
112 // if we are configured to not map the jms message then return it as body
113 if (endpoint != null && !endpoint.getConfiguration().isMapJmsMessage()) {
114 if (LOG.isTraceEnabled()) {
115 LOG.trace("Option map JMS message is false so using JMS message as body: " + message);
116 }
117 return message;
118 }
119
120 if (message instanceof ObjectMessage) {
121 if (LOG.isTraceEnabled()) {
122 LOG.trace("Extracting body as a ObjectMessage from JMS message: " + message);
123 }
124 ObjectMessage objectMessage = (ObjectMessage)message;
125 Object payload = objectMessage.getObject();
126 if (payload instanceof DefaultExchangeHolder) {
127 DefaultExchangeHolder holder = (DefaultExchangeHolder) payload;
128 DefaultExchangeHolder.unmarshal(exchange, holder);
129 return exchange.getIn().getBody();
130 } else {
131 return objectMessage.getObject();
132 }
133 } else if (message instanceof TextMessage) {
134 if (LOG.isTraceEnabled()) {
135 LOG.trace("Extracting body as a TextMessage from JMS message: " + message);
136 }
137 TextMessage textMessage = (TextMessage)message;
138 return textMessage.getText();
139 } else if (message instanceof MapMessage) {
140 if (LOG.isTraceEnabled()) {
141 LOG.trace("Extracting body as a MapMessage from JMS message: " + message);
142 }
143 return createMapFromMapMessage((MapMessage)message);
144 } else if (message instanceof BytesMessage) {
145 if (LOG.isTraceEnabled()) {
146 LOG.trace("Extracting body as a BytesMessage from JMS message: " + message);
147 }
148 return createByteArrayFromBytesMessage((BytesMessage)message);
149 } else if (message instanceof StreamMessage) {
150 if (LOG.isTraceEnabled()) {
151 LOG.trace("Extracting body as a StreamMessage from JMS message: " + message);
152 }
153 return message;
154 } else {
155 return null;
156 }
157 } catch (JMSException e) {
158 throw new RuntimeCamelException("Failed to extract body due to: " + e + ". Message: " + message, e);
159 }
160 }
161
162 public Map<String, Object> extractHeadersFromJms(Message jmsMessage, Exchange exchange) {
163 Map<String, Object> map = new HashMap<String, Object>();
164 if (jmsMessage != null) {
165 // lets populate the standard JMS message headers
166 try {
167 map.put("JMSCorrelationID", jmsMessage.getJMSCorrelationID());
168 map.put("JMSDeliveryMode", jmsMessage.getJMSDeliveryMode());
169 map.put("JMSDestination", jmsMessage.getJMSDestination());
170 map.put("JMSExpiration", jmsMessage.getJMSExpiration());
171 map.put("JMSMessageID", jmsMessage.getJMSMessageID());
172 map.put("JMSPriority", jmsMessage.getJMSPriority());
173 map.put("JMSRedelivered", jmsMessage.getJMSRedelivered());
174 map.put("JMSTimestamp", jmsMessage.getJMSTimestamp());
175
176 // to work around OracleAQ not supporting the JMSReplyTo header (CAMEL-2909)
177 try {
178 map.put("JMSReplyTo", jmsMessage.getJMSReplyTo());
179 } catch (JMSException e) {
180 LOG.trace("Cannot read JMSReplyTo header. Will ignore this exception.", e);
181 }
182 // to work around OracleAQ not supporting the JMSType header (CAMEL-2909)
183 try {
184 map.put("JMSType", jmsMessage.getJMSType());
185 } catch (JMSException e) {
186 LOG.trace("Cannot read JMSType header. Will ignore this exception.", e);
187 }
188
189 // this works around a bug in the ActiveMQ property handling
190 map.put("JMSXGroupID", jmsMessage.getStringProperty("JMSXGroupID"));
191 } catch (JMSException e) {
192 throw new RuntimeCamelException(e);
193 }
194
195 Enumeration names;
196 try {
197 names = jmsMessage.getPropertyNames();
198 } catch (JMSException e) {
199 throw new RuntimeCamelException(e);
200 }
201 while (names.hasMoreElements()) {
202 String name = names.nextElement().toString();
203 try {
204 Object value = jmsMessage.getObjectProperty(name);
205 if (headerFilterStrategy != null
206 && headerFilterStrategy.applyFilterToExternalHeaders(name, value, exchange)) {
207 continue;
208 }
209
210 // must decode back from safe JMS header name to original header name
211 // when storing on this Camel JmsMessage object.
212 String key = jmsKeyFormatStrategy.decodeKey(name);
213 map.put(key, value);
214 } catch (JMSException e) {
215 throw new RuntimeCamelException(name, e);
216 }
217 }
218 }
219
220 return map;
221 }
222
223 public Object getObjectProperty(Message jmsMessage, String name) throws JMSException {
224 // try a direct lookup first
225 Object answer = jmsMessage.getObjectProperty(name);
226 if (answer == null) {
227 // then encode the key and do another lookup
228 String key = jmsKeyFormatStrategy.encodeKey(name);
229 answer = jmsMessage.getObjectProperty(key);
230 }
231 return answer;
232 }
233
234 protected byte[] createByteArrayFromBytesMessage(BytesMessage message) throws JMSException {
235 if (message.getBodyLength() > Integer.MAX_VALUE) {
236 LOG.warn("Length of BytesMessage is too long: " + message.getBodyLength());
237 return null;
238 }
239 byte[] result = new byte[(int)message.getBodyLength()];
240 message.readBytes(result);
241 return result;
242 }
243
244 /**
245 * Creates a JMS message from the Camel exchange and message
246 *
247 * @param exchange the current exchange
248 * @param session the JMS session used to create the message
249 * @return a newly created JMS Message instance containing the
250 * @throws JMSException if the message could not be created
251 */
252 public Message makeJmsMessage(Exchange exchange, Session session) throws JMSException {
253 return makeJmsMessage(exchange, exchange.getIn(), session, null);
254 }
255
256 /**
257 * Creates a JMS message from the Camel exchange and message
258 *
259 * @param exchange the current exchange
260 * @param camelMessage the body to make a javax.jms.Message as
261 * @param session the JMS session used to create the message
262 * @param cause optional exception occurred that should be sent as reply instead of a regular body
263 * @return a newly created JMS Message instance containing the
264 * @throws JMSException if the message could not be created
265 */
266 public Message makeJmsMessage(Exchange exchange, org.apache.camel.Message camelMessage, Session session, Exception cause) throws JMSException {
267 Message answer = null;
268
269 boolean alwaysCopy = endpoint != null && endpoint.getConfiguration().isAlwaysCopyMessage();
270 if (!alwaysCopy && camelMessage instanceof JmsMessage) {
271 JmsMessage jmsMessage = (JmsMessage)camelMessage;
272 if (!jmsMessage.shouldCreateNewMessage()) {
273 answer = jmsMessage.getJmsMessage();
274 }
275 }
276
277 if (answer == null) {
278 if (cause != null) {
279 // an exception occurred so send it as response
280 if (LOG.isDebugEnabled()) {
281 LOG.debug("Will create JmsMessage with caused exception: " + cause);
282 }
283 // create jms message containing the caused exception
284 answer = createJmsMessage(cause, session);
285 } else {
286 ObjectHelper.notNull(camelMessage, "message");
287 // create regular jms message using the camel message body
288 answer = createJmsMessage(exchange, camelMessage.getBody(), camelMessage.getHeaders(), session, exchange.getContext());
289 appendJmsProperties(answer, exchange, camelMessage);
290 }
291 }
292
293 return answer;
294 }
295
296 /**
297 * Appends the JMS headers from the Camel {@link JmsMessage}
298 */
299 public void appendJmsProperties(Message jmsMessage, Exchange exchange) throws JMSException {
300 appendJmsProperties(jmsMessage, exchange, exchange.getIn());
301 }
302
303 /**
304 * Appends the JMS headers from the Camel {@link JmsMessage}
305 */
306 public void appendJmsProperties(Message jmsMessage, Exchange exchange, org.apache.camel.Message in) throws JMSException {
307 Set<Map.Entry<String, Object>> entries = in.getHeaders().entrySet();
308 for (Map.Entry<String, Object> entry : entries) {
309 String headerName = entry.getKey();
310 Object headerValue = entry.getValue();
311 appendJmsProperty(jmsMessage, exchange, in, headerName, headerValue);
312 }
313 }
314
315 public void appendJmsProperty(Message jmsMessage, Exchange exchange, org.apache.camel.Message in,
316 String headerName, Object headerValue) throws JMSException {
317 if (isStandardJMSHeader(headerName)) {
318 if (headerName.equals("JMSCorrelationID")) {
319 jmsMessage.setJMSCorrelationID(ExchangeHelper.convertToType(exchange, String.class, headerValue));
320 } else if (headerName.equals("JMSReplyTo") && headerValue != null) {
321 jmsMessage.setJMSReplyTo(ExchangeHelper.convertToType(exchange, Destination.class, headerValue));
322 } else if (headerName.equals("JMSType")) {
323 jmsMessage.setJMSType(ExchangeHelper.convertToType(exchange, String.class, headerValue));
324 } else if (headerName.equals("JMSPriority")) {
325 jmsMessage.setJMSPriority(ExchangeHelper.convertToType(exchange, Integer.class, headerValue));
326 } else if (headerName.equals("JMSDeliveryMode")) {
327 Integer deliveryMode = ExchangeHelper.convertToType(exchange, Integer.class, headerValue);
328 jmsMessage.setJMSDeliveryMode(deliveryMode);
329 jmsMessage.setIntProperty(JmsConstants.JMS_DELIVERY_MODE, deliveryMode);
330 } else if (headerName.equals("JMSExpiration")) {
331 jmsMessage.setJMSExpiration(ExchangeHelper.convertToType(exchange, Long.class, headerValue));
332 } else if (LOG.isTraceEnabled()) {
333 // The following properties are set by the MessageProducer:
334 // JMSDestination
335 // The following are set on the underlying JMS provider:
336 // JMSMessageID, JMSTimestamp, JMSRedelivered
337 // log at trace level to not spam log
338 LOG.trace("Ignoring JMS header: " + headerName + " with value: " + headerValue);
339 }
340 } else if (shouldOutputHeader(in, headerName, headerValue, exchange)) {
341 // only primitive headers and strings is allowed as properties
342 // see message properties: http://java.sun.com/j2ee/1.4/docs/api/javax/jms/Message.html
343 Object value = getValidJMSHeaderValue(headerName, headerValue);
344 if (value != null) {
345 // must encode to safe JMS header name before setting property on jmsMessage
346 String key = jmsKeyFormatStrategy.encodeKey(headerName);
347 // set the property
348 JmsMessageHelper.setProperty(jmsMessage, key, value);
349 } else if (LOG.isDebugEnabled()) {
350 // okay the value is not a primitive or string so we cannot sent it over the wire
351 LOG.debug("Ignoring non primitive header: " + headerName + " of class: "
352 + headerValue.getClass().getName() + " with value: " + headerValue);
353 }
354 }
355 }
356
357 /**
358 * Is the given header a standard JMS header
359 * @param headerName the header name
360 * @return <tt>true</tt> if its a standard JMS header
361 */
362 protected boolean isStandardJMSHeader(String headerName) {
363 if (!headerName.startsWith("JMS")) {
364 return false;
365 }
366 if (headerName.startsWith("JMSX")) {
367 return false;
368 }
369 // IBM WebSphereMQ uses JMS_IBM as special headers
370 if (headerName.startsWith("JMS_")) {
371 return false;
372 }
373
374 // the 4th char must be a letter to be a standard JMS header
375 if (headerName.length() > 3) {
376 Character fourth = headerName.charAt(3);
377 if (Character.isLetter(fourth)) {
378 return true;
379 }
380 }
381
382 return false;
383 }
384
385 /**
386 * Strategy to test if the given header is valid according to the JMS spec to be set as a property
387 * on the JMS message.
388 * <p/>
389 * This default implementation will allow:
390 * <ul>
391 * <li>any primitives and their counter Objects (Integer, Double etc.)</li>
392 * <li>String and any other literals, Character, CharSequence</li>
393 * <li>Boolean</li>
394 * <li>Number</li>
395 * <li>java.util.Date</li>
396 * </ul>
397 *
398 * @param headerName the header name
399 * @param headerValue the header value
400 * @return the value to use, <tt>null</tt> to ignore this header
401 */
402 protected Object getValidJMSHeaderValue(String headerName, Object headerValue) {
403 if (headerValue instanceof String) {
404 return headerValue;
405 } else if (headerValue instanceof Number) {
406 return headerValue;
407 } else if (headerValue instanceof Character) {
408 return headerValue;
409 } else if (headerValue instanceof CharSequence) {
410 return headerValue.toString();
411 } else if (headerValue instanceof Boolean) {
412 return headerValue;
413 } else if (headerValue instanceof Date) {
414 return headerValue.toString();
415 }
416 return null;
417 }
418
419 protected Message createJmsMessage(Exception cause, Session session) throws JMSException {
420 if (LOG.isTraceEnabled()) {
421 LOG.trace("Using JmsMessageType: " + Object);
422 }
423 return session.createObjectMessage(cause);
424 }
425
426 protected Message createJmsMessage(Exchange exchange, Object body, Map<String, Object> headers, Session session, CamelContext context) throws JMSException {
427 JmsMessageType type = null;
428
429 // special for transferExchange
430 if (endpoint != null && endpoint.isTransferExchange()) {
431 if (LOG.isTraceEnabled()) {
432 LOG.trace("Option transferExchange=true so we use JmsMessageType: Object");
433 }
434 Serializable holder = DefaultExchangeHolder.marshal(exchange);
435 return session.createObjectMessage(holder);
436 }
437
438 // use a custom message converter
439 if (endpoint != null && endpoint.getMessageConverter() != null) {
440 if (LOG.isTraceEnabled()) {
441 LOG.trace("Creating JmsMessage using a custom MessageConverter: " + endpoint.getMessageConverter() + " with body: " + body);
442 }
443 return endpoint.getMessageConverter().toMessage(body, session);
444 }
445
446 // check if header have a type set, if so we force to use it
447 if (headers.containsKey(JmsConstants.JMS_MESSAGE_TYPE)) {
448 type = context.getTypeConverter().convertTo(JmsMessageType.class, headers.get(JmsConstants.JMS_MESSAGE_TYPE));
449 } else if (endpoint != null && endpoint.getConfiguration().getJmsMessageType() != null) {
450 // force a specific type from the endpoint configuration
451 type = endpoint.getConfiguration().getJmsMessageType();
452 } else {
453 // let body determine the type
454 if (body instanceof Node || body instanceof String) {
455 type = Text;
456 } else if (body instanceof byte[] || body instanceof GenericFile || body instanceof File || body instanceof Reader
457 || body instanceof InputStream || body instanceof ByteBuffer || body instanceof StreamCache) {
458 type = Bytes;
459 } else if (body instanceof Map) {
460 type = Map;
461 } else if (body instanceof Serializable) {
462 type = Object;
463 }
464 }
465
466 // create the JmsMessage based on the type
467 if (type != null) {
468 if (LOG.isTraceEnabled()) {
469 LOG.trace("Using JmsMessageType: " + type);
470 }
471
472 switch (type) {
473 case Text: {
474 TextMessage message = session.createTextMessage();
475 String payload = context.getTypeConverter().convertTo(String.class, exchange, body);
476 message.setText(payload);
477 return message;
478 }
479 case Bytes: {
480 BytesMessage message = session.createBytesMessage();
481 byte[] payload = context.getTypeConverter().convertTo(byte[].class, exchange, body);
482 message.writeBytes(payload);
483 return message;
484 }
485 case Map: {
486 MapMessage message = session.createMapMessage();
487 Map payload = context.getTypeConverter().convertTo(Map.class, exchange, body);
488 populateMapMessage(message, payload, context);
489 return message;
490 }
491 case Object:
492 Serializable payload;
493 try {
494 payload = context.getTypeConverter().mandatoryConvertTo(Serializable.class, exchange, body);
495 } catch (NoTypeConversionAvailableException e) {
496 // cannot convert to serializable then thrown an exception to avoid sending a null message
497 JMSException cause = new MessageFormatException(e.getMessage());
498 cause.initCause(e);
499 throw cause;
500 }
501 return session.createObjectMessage(payload);
502 default:
503 break;
504 }
505 }
506
507 // warn if the body could not be mapped
508 if (body != null && LOG.isWarnEnabled()) {
509 LOG.warn("Cannot determine specific JmsMessage type to use from body class."
510 + " Will use generic JmsMessage."
511 + " Body class: " + ObjectHelper.classCanonicalName(body)
512 + ". If you want to send a POJO then your class might need to implement java.io.Serializable"
513 + ", or you can force a specific type by setting the jmsMessageType option on the JMS endpoint.");
514 }
515
516 // return a default message
517 return session.createMessage();
518 }
519
520 /**
521 * Populates a {@link MapMessage} from a {@link Map} instance.
522 */
523 protected void populateMapMessage(MapMessage message, Map<?, ?> map, CamelContext context)
524 throws JMSException {
525 for (Object key : map.keySet()) {
526 String keyString = CamelContextHelper.convertTo(context, String.class, key);
527 if (keyString != null) {
528 message.setObject(keyString, map.get(key));
529 }
530 }
531 }
532
533 /**
534 * Extracts a {@link Map} from a {@link MapMessage}
535 */
536 public Map<String, Object> createMapFromMapMessage(MapMessage message) throws JMSException {
537 Map<String, Object> answer = new HashMap<String, Object>();
538 Enumeration names = message.getMapNames();
539 while (names.hasMoreElements()) {
540 String name = names.nextElement().toString();
541 Object value = message.getObject(name);
542 answer.put(name, value);
543 }
544 return answer;
545 }
546
547 /**
548 * Strategy to allow filtering of headers which are put on the JMS message
549 * <p/>
550 * <b>Note</b>: Currently only supports sending java identifiers as keys
551 */
552 protected boolean shouldOutputHeader(org.apache.camel.Message camelMessage, String headerName,
553 Object headerValue, Exchange exchange) {
554 return headerFilterStrategy == null
555 || !headerFilterStrategy.applyFilterToCamelHeaders(headerName, headerValue, exchange);
556 }
557
558 }