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 */
017package org.apache.camel.impl;
018
019import java.io.File;
020import java.io.Serializable;
021import java.math.BigDecimal;
022import java.math.BigInteger;
023import java.util.Date;
024import java.util.LinkedHashMap;
025import java.util.Map;
026
027import org.apache.camel.Exchange;
028import org.apache.camel.RuntimeExchangeException;
029import org.apache.camel.WrappedFile;
030import org.apache.camel.util.ObjectHelper;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034/**
035 * Holder object for sending an exchange over a remote wire as a serialized object.
036 * This is usually configured using the <tt>transferExchange=true</tt> option on the endpoint.
037 * <br/>
038 * <b>Note:</b> Message body of type {@link File} or {@link WrappedFile} is <b>not</b> supported and
039 * a {@link RuntimeExchangeException} is thrown.
040 * <br/>
041 * As opposed to normal usage where only the body part of the exchange is transferred over the wire,
042 * this holder object serializes the following fields over the wire:
043 * <ul>
044 * <li>exchangeId</li>
045 * <li>in body</li>
046 * <li>out body</li>
047 * <li>fault body </li>
048 * <li>exchange properties</li>
049 * <li>exception</li>
050 * </ul>
051 * And the following headers is transferred if their values are of primitive types, String or Number based.
052 * <ul>
053 * <li>in headers</li>
054 * <li>out headers</li>
055 * <li>fault headers</li>
056 * </ul>
057 * The body is serialized and stored as serialized bytes. The header and exchange properties only include
058 * primitive, String, and Number types (and Exception types for exchange properties). Any other type is skipped.
059 * <br/>
060 * Any message body object that is not serializable will be skipped and Camel will log this at <tt>WARN</tt> level.
061 * And any message header values that is not a primitive value will be skipped and Camel will log this at <tt>DEBUG</tt> level.
062 *
063 * @version 
064 */
065public class DefaultExchangeHolder implements Serializable {
066
067    private static final long serialVersionUID = 2L;
068    private static final Logger LOG = LoggerFactory.getLogger(DefaultExchangeHolder.class);
069
070    private String exchangeId;
071    private Object inBody;
072    private Object outBody;
073    private Boolean inFaultFlag = Boolean.FALSE;
074    private Boolean outFaultFlag = Boolean.FALSE;
075    private Map<String, Object> inHeaders;
076    private Map<String, Object> outHeaders;
077    private Map<String, Object> properties;
078    private Exception exception;
079
080    /**
081     * Creates a payload object with the information from the given exchange.
082     *
083     * @param exchange the exchange, must <b>not</b> be <tt>null</tt>
084     * @return the holder object with information copied form the exchange
085     */
086    public static DefaultExchangeHolder marshal(Exchange exchange) {
087        return marshal(exchange, true);
088    }
089
090    /**
091     * Creates a payload object with the information from the given exchange.
092     *
093     * @param exchange the exchange, must <b>not</b> be <tt>null</tt>
094     * @param includeProperties whether or not to include exchange properties
095     * @return the holder object with information copied form the exchange
096     */
097    public static DefaultExchangeHolder marshal(Exchange exchange, boolean includeProperties) {
098        ObjectHelper.notNull(exchange, "exchange");
099
100        // we do not support files
101        Object body = exchange.getIn().getBody();
102        if (body instanceof WrappedFile || body instanceof File) {
103            throw new RuntimeExchangeException("Message body of type " + body.getClass().getCanonicalName() + " is not supported by this marshaller.", exchange);
104        }
105
106        DefaultExchangeHolder payload = new DefaultExchangeHolder();
107
108        payload.exchangeId = exchange.getExchangeId();
109        payload.inBody = checkSerializableBody("in body", exchange, exchange.getIn().getBody());
110        payload.safeSetInHeaders(exchange);
111        if (exchange.hasOut()) {
112            payload.outBody = checkSerializableBody("out body", exchange, exchange.getOut().getBody());
113            payload.outFaultFlag = exchange.getOut().isFault();
114            payload.safeSetOutHeaders(exchange);
115        } else {
116            payload.inFaultFlag = exchange.getIn().isFault();
117        }
118        if (includeProperties) {
119            payload.safeSetProperties(exchange);
120        }
121        payload.exception = exchange.getException();
122
123        return payload;
124    }
125
126    /**
127     * Transfers the information from the payload to the exchange.
128     *
129     * @param exchange the exchange to set values from the payload, must <b>not</b> be <tt>null</tt>
130     * @param payload  the payload with the values, must <b>not</b> be <tt>null</tt>
131     */
132    public static void unmarshal(Exchange exchange, DefaultExchangeHolder payload) {
133        ObjectHelper.notNull(exchange, "exchange");
134        ObjectHelper.notNull(payload, "payload");
135
136        exchange.setExchangeId(payload.exchangeId);
137        exchange.getIn().setBody(payload.inBody);
138        if (payload.inHeaders != null) {
139            exchange.getIn().setHeaders(payload.inHeaders);
140        }
141        if (payload.inFaultFlag != null) {
142            exchange.getIn().setFault(payload.inFaultFlag);
143        }
144        if (payload.outBody != null) {
145            exchange.getOut().setBody(payload.outBody);
146            if (payload.outHeaders != null) {
147                exchange.getOut().setHeaders(payload.outHeaders);
148            }
149            if (payload.outFaultFlag != null) {
150                exchange.getOut().setFault(payload.outFaultFlag);
151            }
152        }
153        if (payload.properties != null) {
154            for (String key : payload.properties.keySet()) {
155                exchange.setProperty(key, payload.properties.get(key));
156            }
157        }
158        exchange.setException(payload.exception);
159    }
160
161    /**
162     * Adds a property to the payload.
163     * <p/>
164     * This can be done in special situations where additional information must be added which was not provided
165     * from the source.
166     *
167     * @param payload the serialized payload
168     * @param key the property key to add
169     * @param property the property value to add
170     */
171    public static void addProperty(DefaultExchangeHolder payload, String key, Serializable property) {
172        if (key == null || property == null) {
173            return;
174        }
175        if (payload.properties == null) {
176            payload.properties = new LinkedHashMap<String, Object>();
177        }
178        payload.properties.put(key, property);
179    }
180
181    public String toString() {
182        StringBuilder sb = new StringBuilder("DefaultExchangeHolder[exchangeId=").append(exchangeId);
183        sb.append("inBody=").append(inBody).append(", outBody=").append(outBody);
184        sb.append(", inHeaders=").append(inHeaders).append(", outHeaders=").append(outHeaders);
185        sb.append(", properties=").append(properties).append(", exception=").append(exception);
186        return sb.append(']').toString();
187    }
188
189    private Map<String, Object> safeSetInHeaders(Exchange exchange) {
190        if (exchange.getIn().hasHeaders()) {
191            Map<String, Object> map = checkValidHeaderObjects("in headers", exchange, exchange.getIn().getHeaders());
192            if (map != null && !map.isEmpty()) {
193                inHeaders = new LinkedHashMap<String, Object>(map);
194            }
195        }
196        return null;
197    }
198
199    private Map<String, Object> safeSetOutHeaders(Exchange exchange) {
200        if (exchange.hasOut() && exchange.getOut().hasHeaders()) {
201            Map<String, Object> map = checkValidHeaderObjects("out headers", exchange, exchange.getOut().getHeaders());
202            if (map != null && !map.isEmpty()) {
203                outHeaders = new LinkedHashMap<String, Object>(map);
204            }
205        }
206        return null;
207    }
208
209    private Map<String, Object> safeSetProperties(Exchange exchange) {
210        if (exchange.hasProperties()) {
211            Map<String, Object> map = checkValidExchangePropertyObjects("properties", exchange, exchange.getProperties());
212            if (map != null && !map.isEmpty()) {
213                properties = new LinkedHashMap<String, Object>(map);
214            }
215        }
216        return null;
217    }
218
219    private static Object checkSerializableBody(String type, Exchange exchange, Object object) {
220        if (object == null) {
221            return null;
222        }
223
224        Serializable converted = exchange.getContext().getTypeConverter().convertTo(Serializable.class, exchange, object);
225        if (converted != null) {
226            return converted;
227        } else {
228            LOG.warn("Exchange " + type + " containing object: " + object + " of type: " + object.getClass().getCanonicalName() + " cannot be serialized, it will be excluded by the holder.");
229            return null;
230        }
231    }
232
233    private static Map<String, Object> checkValidHeaderObjects(String type, Exchange exchange, Map<String, Object> map) {
234        if (map == null) {
235            return null;
236        }
237
238        Map<String, Object> result = new LinkedHashMap<String, Object>();
239        for (Map.Entry<String, Object> entry : map.entrySet()) {
240
241            // silently skip any values which is null
242            if (entry.getValue() == null) {
243                continue;
244            }
245
246            Object value = getValidHeaderValue(entry.getKey(), entry.getValue());
247            if (value != null) {
248                Serializable converted = exchange.getContext().getTypeConverter().convertTo(Serializable.class, exchange, value);
249                if (converted != null) {
250                    result.put(entry.getKey(), converted);
251                } else {
252                    logCannotSerializeObject(type, entry.getKey(), entry.getValue());
253                }
254            } else {
255                logInvalidHeaderValue(type, entry.getKey(), entry.getValue());
256            }
257        }
258
259        return result;
260    }
261
262    private static Map<String, Object> checkValidExchangePropertyObjects(String type, Exchange exchange, Map<String, Object> map) {
263        if (map == null) {
264            return null;
265        }
266
267        Map<String, Object> result = new LinkedHashMap<String, Object>();
268        for (Map.Entry<String, Object> entry : map.entrySet()) {
269
270            // silently skip any values which is null
271            if (entry.getValue() == null) {
272                continue;
273            }
274
275            Object value = getValidExchangePropertyValue(entry.getKey(), entry.getValue());
276            if (value != null) {
277                Serializable converted = exchange.getContext().getTypeConverter().convertTo(Serializable.class, exchange, value);
278                if (converted != null) {
279                    result.put(entry.getKey(), converted);
280                } else {
281                    logCannotSerializeObject(type, entry.getKey(), entry.getValue());
282                }
283            } else {
284                logInvalidExchangePropertyValue(type, entry.getKey(), entry.getValue());
285            }
286        }
287
288        return result;
289    }
290
291    /**
292     * We only want to store header values of primitive and String related types.
293     * <p/>
294     * This default implementation will allow:
295     * <ul>
296     *   <li>any primitives and their counter Objects (Integer, Double etc.)</li>
297     *   <li>String and any other literals, Character, CharSequence</li>
298     *   <li>Boolean</li>
299     *   <li>Number</li>
300     *   <li>java.util.Date</li>
301     * </ul>
302     *
303     * @param headerName   the header name
304     * @param headerValue  the header value
305     * @return  the value to use, <tt>null</tt> to ignore this header
306     */
307    protected static Object getValidHeaderValue(String headerName, Object headerValue) {
308        if (headerValue instanceof String) {
309            return headerValue;
310        } else if (headerValue instanceof BigInteger) {
311            return headerValue;
312        } else if (headerValue instanceof BigDecimal) {
313            return headerValue;
314        } else if (headerValue instanceof Number) {
315            return headerValue;
316        } else if (headerValue instanceof Character) {
317            return headerValue;
318        } else if (headerValue instanceof CharSequence) {
319            return headerValue.toString();
320        } else if (headerValue instanceof Boolean) {
321            return headerValue;
322        } else if (headerValue instanceof Date) {
323            return headerValue;
324        }
325        return null;
326    }
327
328    /**
329     * We only want to store exchange property values of primitive and String related types, and
330     * as well any caught exception that Camel routing engine has caught.
331     * <p/>
332     * This default implementation will allow the same values as {@link #getValidHeaderValue(String, Object)}
333     * and in addition any value of type {@link Throwable}.
334     *
335     * @param propertyName   the property name
336     * @param propertyValue  the property value
337     * @return  the value to use, <tt>null</tt> to ignore this header
338     */
339    protected static Object getValidExchangePropertyValue(String propertyName, Object propertyValue) {
340        // for exchange properties we also allow exception to be transferred so people can store caught exception
341        if (propertyValue instanceof Throwable) {
342            return propertyValue;
343        }
344        return getValidHeaderValue(propertyName, propertyValue);
345    }
346
347    private static void logCannotSerializeObject(String type, String key, Object value) {
348        if (key.startsWith("Camel")) {
349            // log Camel at DEBUG level
350            if (LOG.isDebugEnabled()) {
351                LOG.debug("Exchange {} containing key: {} with object: {} of type: {} cannot be serialized, it will be excluded by the holder."
352                        , new Object[]{type, key, value, ObjectHelper.classCanonicalName(value)});
353            }
354        } else {
355            // log regular at WARN level
356            LOG.warn("Exchange {} containing key: {} with object: {} of type: {} cannot be serialized, it will be excluded by the holder."
357                    , new Object[]{type, key, value, ObjectHelper.classCanonicalName(value)});
358        }
359    }
360
361    private static void logInvalidHeaderValue(String type, String key, Object value) {
362        if (LOG.isDebugEnabled()) {
363            LOG.debug("Exchange {} containing key: {} with object: {} of type: {} is not valid header type, it will be excluded by the holder."
364                      , new Object[]{type, key, value, ObjectHelper.classCanonicalName(value)});
365        }
366    }
367
368    private static void logInvalidExchangePropertyValue(String type, String key, Object value) {
369        if (LOG.isDebugEnabled()) {
370            LOG.debug("Exchange {} containing key: {} with object: {} of type: {} is not valid exchange property type, it will be excluded by the holder."
371                      , new Object[]{type, key, value, ObjectHelper.classCanonicalName(value)});
372        }
373    }
374
375}