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.util;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.OutputStream;
023import java.io.Reader;
024import java.io.Writer;
025import java.util.Date;
026import java.util.List;
027import java.util.Map;
028import java.util.TreeMap;
029
030import javax.xml.transform.Source;
031
032import org.apache.camel.BytesSource;
033import org.apache.camel.Exchange;
034import org.apache.camel.Message;
035import org.apache.camel.MessageHistory;
036import org.apache.camel.StreamCache;
037import org.apache.camel.StringSource;
038import org.apache.camel.WrappedFile;
039import org.apache.camel.spi.ExchangeFormatter;
040import org.apache.camel.spi.HeaderFilterStrategy;
041
042/**
043 * Some helper methods when working with {@link org.apache.camel.Message}.
044 * 
045 * @version
046 */
047public final class MessageHelper {
048
049    private static final String MESSAGE_HISTORY_HEADER = "%-20s %-20s %-80s %-12s";
050    private static final String MESSAGE_HISTORY_OUTPUT = "[%-18.18s] [%-18.18s] [%-78.78s] [%10.10s]";
051
052    /**
053     * Utility classes should not have a public constructor.
054     */
055    private MessageHelper() {
056    }
057
058    /**
059     * Extracts the given body and returns it as a String, that can be used for
060     * logging etc.
061     * <p/>
062     * Will handle stream based bodies wrapped in StreamCache.
063     * 
064     * @param message the message with the body
065     * @return the body as String, can return <tt>null</null> if no body
066     */
067    public static String extractBodyAsString(Message message) {
068        if (message == null) {
069            return null;
070        }
071
072        // optimize if the body is a String type already
073        Object body = message.getBody();
074        if (body instanceof String) {
075            return (String) body;
076        }
077
078        // we need to favor using stream cache so the body can be re-read later
079        StreamCache newBody = message.getBody(StreamCache.class);
080        if (newBody != null) {
081            message.setBody(newBody);
082        }
083
084        Object answer = message.getBody(String.class);
085        if (answer == null) {
086            answer = message.getBody();
087        }
088
089        if (newBody != null) {
090            // Reset the InputStreamCache
091            newBody.reset();
092        }
093
094        return answer != null ? answer.toString() : null;
095    }
096
097    /**
098     * Gets the given body class type name as a String.
099     * <p/>
100     * Will skip java.lang. for the build in Java types.
101     * 
102     * @param message the message with the body
103     * @return the body type name as String, can return
104     *         <tt>null</null> if no body
105     */
106    public static String getBodyTypeName(Message message) {
107        if (message == null) {
108            return null;
109        }
110        String answer = ObjectHelper.classCanonicalName(message.getBody());
111        if (answer != null && answer.startsWith("java.lang.")) {
112            return answer.substring(10);
113        }
114        return answer;
115    }
116
117    /**
118     * If the message body contains a {@link StreamCache} instance, reset the
119     * cache to enable reading from it again.
120     * 
121     * @param message the message for which to reset the body
122     */
123    public static void resetStreamCache(Message message) {
124        if (message == null) {
125            return;
126        }
127        Object body = message.getBody();
128        if (body instanceof StreamCache) {
129            ((StreamCache) body).reset();
130        }
131    }
132
133    /**
134     * Returns the MIME content type on the message or <tt>null</tt> if none
135     * defined
136     */
137    public static String getContentType(Message message) {
138        return message.getHeader(Exchange.CONTENT_TYPE, String.class);
139    }
140
141    /**
142     * Returns the MIME content encoding on the message or <tt>null</tt> if none
143     * defined
144     */
145    public static String getContentEncoding(Message message) {
146        return message.getHeader(Exchange.CONTENT_ENCODING, String.class);
147    }
148
149    /**
150     * Extracts the body for logging purpose.
151     * <p/>
152     * Will clip the body if its too big for logging. Will prepend the message
153     * with <tt>Message: </tt>
154     * 
155     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_STREAMS
156     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_MAX_CHARS
157     * @param message the message
158     * @return the logging message
159     */
160    public static String extractBodyForLogging(Message message) {
161        return extractBodyForLogging(message, "Message: ");
162    }
163
164    /**
165     * Extracts the value for logging purpose.
166     * <p/>
167     * Will clip the value if its too big for logging.
168     *
169     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_STREAMS
170     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_MAX_CHARS
171     * @param value   the value
172     * @param message the message
173     * @return the logging message
174     */
175    public static String extractValueForLogging(Object value, Message message) {
176        boolean streams = false;
177        if (message.getExchange() != null) {
178            String globalOption = message.getExchange().getContext().getGlobalOption(Exchange.LOG_DEBUG_BODY_STREAMS);
179            if (globalOption != null) {
180                streams = message.getExchange().getContext().getTypeConverter().convertTo(Boolean.class, message.getExchange(), globalOption);
181            }
182        }
183
184        // default to 1000 chars
185        int maxChars = 1000;
186
187        if (message.getExchange() != null) {
188            String property = message.getExchange().getContext().getGlobalOption(Exchange.LOG_DEBUG_BODY_MAX_CHARS);
189            if (property != null) {
190                maxChars = message.getExchange().getContext().getTypeConverter().convertTo(Integer.class, property);
191            }
192        }
193
194        return extractValueForLogging(value, message, "", streams, false, maxChars);
195    }
196
197    /**
198     * Extracts the body for logging purpose.
199     * <p/>
200     * Will clip the body if its too big for logging.
201     *
202     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_STREAMS
203     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_MAX_CHARS
204     * @param message the message
205     * @param prepend a message to prepend
206     * @return the logging message
207     */
208    public static String extractBodyForLogging(Message message, String prepend) {
209        boolean streams = false;
210        if (message.getExchange() != null) {
211            String globalOption = message.getExchange().getContext().getGlobalOption(Exchange.LOG_DEBUG_BODY_STREAMS);
212            if (globalOption != null) {
213                streams = message.getExchange().getContext().getTypeConverter().convertTo(Boolean.class, message.getExchange(), globalOption);
214            }
215        }
216        return extractBodyForLogging(message, prepend, streams, false);
217    }
218
219    /**
220     * Extracts the body for logging purpose.
221     * <p/>
222     * Will clip the body if its too big for logging.
223     *
224     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_STREAMS
225     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_MAX_CHARS
226     * @param message the message
227     * @param prepend a message to prepend
228     * @param allowStreams whether or not streams is allowed
229     * @param allowFiles whether or not files is allowed (currently not in use)
230     * @return the logging message
231     */
232    public static String extractBodyForLogging(Message message, String prepend, boolean allowStreams, boolean allowFiles) {
233        // default to 1000 chars
234        int maxChars = 1000;
235
236        if (message.getExchange() != null) {
237            String globalOption = message.getExchange().getContext().getGlobalOption(Exchange.LOG_DEBUG_BODY_MAX_CHARS);
238            if (globalOption != null) {
239                maxChars = message.getExchange().getContext().getTypeConverter().convertTo(Integer.class, globalOption);
240            }
241        }
242
243        return extractBodyForLogging(message, prepend, allowStreams, allowFiles, maxChars);
244    }
245
246    /**
247     * Extracts the body for logging purpose.
248     * <p/>
249     * Will clip the body if its too big for logging.
250     * 
251     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_MAX_CHARS
252     * @param message the message
253     * @param prepend a message to prepend
254     * @param allowStreams whether or not streams is allowed
255     * @param allowFiles whether or not files is allowed (currently not in use)
256     * @param maxChars limit to maximum number of chars. Use 0 for not limit, and -1 for turning logging message body off.
257     * @return the logging message
258     */
259    public static String extractBodyForLogging(Message message, String prepend, boolean allowStreams, boolean allowFiles, int maxChars) {
260        return extractValueForLogging(message.getBody(), message, prepend, allowStreams, allowFiles, maxChars);
261    }
262
263    /**
264     * Extracts the value for logging purpose.
265     * <p/>
266     * Will clip the value if its too big for logging.
267     *
268     * @see org.apache.camel.Exchange#LOG_DEBUG_BODY_MAX_CHARS
269     * @param obj     the value
270     * @param message the message
271     * @param prepend a message to prepend
272     * @param allowStreams whether or not streams is allowed
273     * @param allowFiles whether or not files is allowed (currently not in use)
274     * @param maxChars limit to maximum number of chars. Use 0 for not limit, and -1 for turning logging message body off.
275     * @return the logging message
276     */
277    public static String extractValueForLogging(Object obj, Message message, String prepend, boolean allowStreams, boolean allowFiles, int maxChars) {
278        if (maxChars < 0) {
279            return prepend + "[Body is not logged]";
280        }
281
282        if (obj == null) {
283            return prepend + "[Body is null]";
284        }
285
286        if (!allowStreams) {
287            if (obj instanceof Source && !(obj instanceof StringSource || obj instanceof BytesSource)) {
288                // for Source its only StringSource or BytesSource that is okay as they are memory based
289                // all other kinds we should not touch the body
290                return prepend + "[Body is instance of java.xml.transform.Source]";
291            } else if (obj instanceof StreamCache) {
292                return prepend + "[Body is instance of org.apache.camel.StreamCache]";
293            } else if (obj instanceof InputStream) {
294                return prepend + "[Body is instance of java.io.InputStream]";
295            } else if (obj instanceof OutputStream) {
296                return prepend + "[Body is instance of java.io.OutputStream]";
297            } else if (obj instanceof Reader) {
298                return prepend + "[Body is instance of java.io.Reader]";
299            } else if (obj instanceof Writer) {
300                return prepend + "[Body is instance of java.io.Writer]";
301            } else if (obj instanceof WrappedFile || obj instanceof File) {
302                if (!allowFiles) {
303                    return prepend + "[Body is file based: " + obj + "]";
304                }
305            }
306        }
307
308        if (!allowFiles) {
309            if (obj instanceof WrappedFile || obj instanceof File) {
310                return prepend + "[Body is file based: " + obj + "]";
311            }
312        }
313
314        // is the body a stream cache or input stream
315        StreamCache cache = null;
316        InputStream is = null;
317        if (obj instanceof StreamCache) {
318            cache = (StreamCache)obj;
319            is = null;
320        } else if (obj instanceof InputStream) {
321            cache = null;
322            is = (InputStream) obj;
323        }
324
325        // grab the message body as a string
326        String body = null;
327        if (message.getExchange() != null) {
328            try {
329                body = message.getExchange().getContext().getTypeConverter().tryConvertTo(String.class, message.getExchange(), obj);
330            } catch (Throwable e) {
331                // ignore as the body is for logging purpose
332            }
333        }
334        if (body == null) {
335            try {
336                body = obj.toString();
337            } catch (Throwable e) {
338                // ignore as the body is for logging purpose
339            }
340        }
341
342        // reset stream cache after use
343        if (cache != null) {
344            cache.reset();
345        } else if (is != null && is.markSupported()) {
346            try {
347                is.reset();
348            } catch (IOException e) {
349                // ignore
350            }
351        }
352
353        if (body == null) {
354            return prepend + "[Body is null]";
355        }
356
357        // clip body if length enabled and the body is too big
358        if (maxChars > 0 && body.length() > maxChars) {
359            body = body.substring(0, maxChars) + "... [Body clipped after " + maxChars + " chars, total length is " + body.length() + "]";
360        }
361
362        return prepend + body;
363    }
364
365    /**
366     * Dumps the message as a generic XML structure.
367     * 
368     * @param message the message
369     * @return the XML
370     */
371    public static String dumpAsXml(Message message) {
372        return dumpAsXml(message, true);
373    }
374
375    /**
376     * Dumps the message as a generic XML structure.
377     * 
378     * @param message the message
379     * @param includeBody whether or not to include the message body
380     * @return the XML
381     */
382    public static String dumpAsXml(Message message, boolean includeBody) {
383        return dumpAsXml(message, includeBody, 0);
384    }
385
386    /**
387     * Dumps the message as a generic XML structure.
388     *
389     * @param message the message
390     * @param includeBody whether or not to include the message body
391     * @param indent number of spaces to indent
392     * @return the XML
393     */
394    public static String dumpAsXml(Message message, boolean includeBody, int indent) {
395        return dumpAsXml(message, includeBody, indent, false, true, 128 * 1024);
396    }
397
398    /**
399     * Dumps the message as a generic XML structure.
400     *
401     * @param message the message
402     * @param includeBody whether or not to include the message body
403     * @param indent number of spaces to indent
404     * @param allowStreams whether to include message body if they are stream based
405     * @param allowFiles whether to include message body if they are file based
406     * @param maxChars clip body after maximum chars (to avoid very big messages). Use 0 or negative value to not limit at all.
407     * @return the XML
408     */
409    public static String dumpAsXml(Message message, boolean includeBody, int indent, boolean allowStreams, boolean allowFiles, int maxChars) {
410        StringBuilder sb = new StringBuilder();
411
412        StringBuilder prefix = new StringBuilder();
413        for (int i = 0; i < indent; i++) {
414            prefix.append(" ");
415        }
416
417        // include exchangeId as attribute on the <message> tag
418        sb.append(prefix);
419        sb.append("<message exchangeId=\"").append(message.getExchange().getExchangeId()).append("\">\n");
420
421        // headers
422        if (message.hasHeaders()) {
423            sb.append(prefix);
424            sb.append("  <headers>\n");
425            // sort the headers so they are listed A..Z
426            Map<String, Object> headers = new TreeMap<>(message.getHeaders());
427            for (Map.Entry<String, Object> entry : headers.entrySet()) {
428                Object value = entry.getValue();
429                String type = ObjectHelper.classCanonicalName(value);
430                sb.append(prefix);
431                sb.append("    <header key=\"").append(entry.getKey()).append("\"");
432                if (type != null) {
433                    sb.append(" type=\"").append(type).append("\"");
434                }
435                sb.append(">");
436
437                // dump header value as XML, use Camel type converter to convert
438                // to String
439                if (value != null) {
440                    try {
441                        String xml = message.getExchange().getContext().getTypeConverter().tryConvertTo(String.class,
442                                message.getExchange(), value);
443                        if (xml != null) {
444                            // must always xml encode
445                            sb.append(StringHelper.xmlEncode(xml));
446                        }
447                    } catch (Throwable e) {
448                        // ignore as the body is for logging purpose
449                    }
450                }
451
452                sb.append("</header>\n");
453            }
454            sb.append(prefix);
455            sb.append("  </headers>\n");
456        }
457
458        if (includeBody) {
459            sb.append(prefix);
460            sb.append("  <body");
461            String type = ObjectHelper.classCanonicalName(message.getBody());
462            if (type != null) {
463                sb.append(" type=\"").append(type).append("\"");
464            }
465            sb.append(">");
466
467            String xml = extractBodyForLogging(message, "", allowStreams, allowFiles, maxChars);
468            if (xml != null) {
469                // must always xml encode
470                sb.append(StringHelper.xmlEncode(xml));
471            }
472
473            sb.append("</body>\n");
474        }
475
476        sb.append(prefix);
477        sb.append("</message>");
478        return sb.toString();
479    }
480
481    /**
482     * Copies the headers from the source to the target message.
483     * 
484     * @param source the source message
485     * @param target the target message
486     * @param override whether to override existing headers
487     */
488    public static void copyHeaders(Message source, Message target, boolean override) {
489        copyHeaders(source, target, null, override);
490    }
491    
492    /**
493     * Copies the headers from the source to the target message.
494     * 
495     * @param source the source message
496     * @param target the target message
497     * @param strategy the header filter strategy which could help us to filter the protocol message headers
498     * @param override whether to override existing headers
499     */
500    public static void copyHeaders(Message source, Message target, HeaderFilterStrategy strategy, boolean override) {
501        if (!source.hasHeaders()) {
502            return;
503        }
504
505        for (Map.Entry<String, Object> entry : source.getHeaders().entrySet()) {
506            String key = entry.getKey();
507            Object value = entry.getValue();
508
509            if (target.getHeader(key) == null || override) {
510                if (strategy == null) {
511                    target.setHeader(key, value);
512                } else if (!strategy.applyFilterToExternalHeaders(key, value, target.getExchange())) {
513                    // Just make sure we don't copy the protocol headers to target
514                    target.setHeader(key, value);
515                }
516            }
517        }
518    }
519
520    /**
521     * Dumps the {@link MessageHistory} from the {@link Exchange} in a human readable format.
522     *
523     * @param exchange           the exchange
524     * @param exchangeFormatter  if provided then information about the exchange is included in the dump
525     * @param logStackTrace      whether to include a header for the stacktrace, to be added (not included in this dump).
526     * @return a human readable message history as a table
527     */
528    public static String dumpMessageHistoryStacktrace(Exchange exchange, ExchangeFormatter exchangeFormatter, boolean logStackTrace) {
529        // must not cause new exceptions so run this in a try catch block
530        try {
531            return doDumpMessageHistoryStacktrace(exchange, exchangeFormatter, logStackTrace);
532        } catch (Throwable e) {
533            // ignore as the body is for logging purpose
534            return "";
535        }
536    }
537
538    @SuppressWarnings("unchecked")
539    public static String doDumpMessageHistoryStacktrace(Exchange exchange, ExchangeFormatter exchangeFormatter, boolean logStackTrace) {
540        List<MessageHistory> list = exchange.getProperty(Exchange.MESSAGE_HISTORY, List.class);
541        if (list == null || list.isEmpty()) {
542            return null;
543        }
544
545        StringBuilder sb = new StringBuilder();
546        sb.append("\n");
547        sb.append("Message History\n");
548        sb.append("---------------------------------------------------------------------------------------------------------------------------------------\n");
549        String goMessageHistoryHeaeder = exchange.getContext().getGlobalOption(Exchange.MESSAGE_HISTORY_HEADER_FORMAT);
550        sb.append(String.format(
551                         goMessageHistoryHeaeder == null ? MESSAGE_HISTORY_HEADER : goMessageHistoryHeaeder,
552                         "RouteId", "ProcessorId", "Processor", "Elapsed (ms)"));
553        sb.append("\n");
554
555        // add incoming origin of message on the top
556        String routeId = exchange.getFromRouteId();
557        String id = routeId;
558        String label = "";
559        if (exchange.getFromEndpoint() != null) {
560            label = URISupport.sanitizeUri(exchange.getFromEndpoint().getEndpointUri());
561        }
562        long elapsed = 0;
563        Date created = exchange.getCreated();
564        if (created != null) {
565            elapsed = new StopWatch(created).taken();
566        }
567
568        String goMessageHistoryOutput = exchange.getContext().getGlobalOption(Exchange.MESSAGE_HISTORY_OUTPUT_FORMAT);
569        goMessageHistoryOutput = goMessageHistoryOutput == null ? MESSAGE_HISTORY_OUTPUT : goMessageHistoryOutput;
570        sb.append(String.format(goMessageHistoryOutput, routeId, id, label, elapsed));
571        sb.append("\n");
572
573        // and then each history
574        for (MessageHistory history : list) {
575            routeId = history.getRouteId() != null ? history.getRouteId() : "";
576            id = history.getNode().getId();
577            // we need to avoid leak the sensible information here
578            // the sanitizeUri takes a very long time for very long string and the format cuts this to
579            // 78 characters, anyway. Cut this to 100 characters. This will give enough space for removing
580            // characters in the sanitizeUri method and will be reasonably fast
581            label =  URISupport.sanitizeUri(StringHelper.limitLength(history.getNode().getLabel(), 100));
582            elapsed = history.getElapsed();
583
584            sb.append(String.format(goMessageHistoryOutput, routeId, id, label, elapsed));
585            sb.append("\n");
586        }
587
588        if (exchangeFormatter != null) {
589            sb.append("\nExchange\n");
590            sb.append("---------------------------------------------------------------------------------------------------------------------------------------\n");
591            sb.append(exchangeFormatter.format(exchange));
592            sb.append("\n");
593        }
594
595        if (logStackTrace) {
596            sb.append("\nStacktrace\n");
597            sb.append("---------------------------------------------------------------------------------------------------------------------------------------");
598        }
599        return sb.toString();
600    }
601
602}