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