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}