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 }