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    }