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