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.logging.log4j.message; 018 019 import java.io.Serializable; 020 import java.text.SimpleDateFormat; 021 import java.util.Arrays; 022 import java.util.Collection; 023 import java.util.Date; 024 import java.util.HashSet; 025 import java.util.Map; 026 import java.util.Set; 027 028 /** 029 * Handles messages that consist of a format string containing '{}' to represent each replaceable token, and 030 * the parameters. 031 * <p/> 032 * This class was originally written for Lillith (http://mac.freshmeat.net/projects/lilith-viewer) by 033 * Joern Huxhorn where it is licensed under the LGPL. It has been relicensed here with his permission 034 * providing that this attribution remain. 035 */ 036 public class ParameterizedMessage implements Message, Serializable { 037 038 /** 039 * Prefix for recursion. 040 */ 041 public static final String RECURSION_PREFIX = "[..."; 042 /** 043 * Suffix for recursion. 044 */ 045 public static final String RECURSION_SUFFIX = "...]"; 046 047 /** 048 * Prefix for errors. 049 */ 050 public static final String ERROR_PREFIX = "[!!!"; 051 /** 052 * Separator for errors. 053 */ 054 public static final String ERROR_SEPARATOR = "=>"; 055 /** 056 * Separator for error messages. 057 */ 058 public static final String ERROR_MSG_SEPARATOR = ":"; 059 /** 060 * Suffix for errors. 061 */ 062 public static final String ERROR_SUFFIX = "!!!]"; 063 064 private static final long serialVersionUID = -665975803997290697L; 065 066 private static final int HASHVAL = 31; 067 068 private static final char DELIM_START = '{'; 069 private static final char DELIM_STOP = '}'; 070 private static final char ESCAPE_CHAR = '\\'; 071 072 private final String messagePattern; 073 private String[] stringArgs; 074 private transient Object[] argArray; 075 private transient String formattedMessage; 076 private transient Throwable throwable; 077 078 /** 079 * Create the parameterizedMessage. 080 * @param messagePattern The message "format" string. This will be a String containing "{}" placeholders 081 * where parameters should be substituted. 082 * @param stringArgs The arguments for substitution. 083 * @param throwable A Throwable. 084 */ 085 public ParameterizedMessage(String messagePattern, String[] stringArgs, Throwable throwable) { 086 this.messagePattern = messagePattern; 087 this.stringArgs = stringArgs; 088 this.throwable = throwable; 089 } 090 091 public ParameterizedMessage(String messagePattern, Object[] arguments, Throwable throwable) { 092 this.messagePattern = messagePattern; 093 this.throwable = throwable; 094 if (arguments != null) { 095 parseArguments(arguments); 096 } 097 } 098 099 /** 100 * <p>This method returns a ParameterizedMessage which contains the arguments converted to String 101 * as well as an optional Throwable.</p> 102 * <p/> 103 * <p>If the last argument is a Throwable and is NOT used up by a placeholder in the message pattern it is returned 104 * in ParameterizedMessage.getThrowable() and won't be contained in the created String[].<br/> 105 * If it is used up ParameterizedMessage.getThrowable() will return null even if the last argument was a 106 * Throwable!</p> 107 * 108 * @param messagePattern the message pattern that to be checked for placeholders. 109 * @param arguments the argument array to be converted. 110 */ 111 public ParameterizedMessage(String messagePattern, Object[] arguments) { 112 this.messagePattern = messagePattern; 113 if (arguments != null) { 114 parseArguments(arguments); 115 } 116 } 117 118 /** 119 * Constructor with a pattern and a single parameter. 120 * @param messagePattern The message pattern. 121 * @param arg The parameter. 122 */ 123 public ParameterizedMessage(String messagePattern, Object arg) { 124 this(messagePattern, new Object[]{arg}); 125 } 126 127 /** 128 * Constructor with a pattern and two parameters. 129 * @param messagePattern The message pattern. 130 * @param arg1 The first parameter. 131 * @param arg2 The second parameter. 132 */ 133 public ParameterizedMessage(String messagePattern, Object arg1, Object arg2) { 134 this(messagePattern, new Object[]{arg1, arg2}); 135 } 136 137 private void parseArguments(Object[] arguments) { 138 int argsCount = countArgumentPlaceholders(messagePattern); 139 int resultArgCount = arguments.length; 140 if (argsCount < arguments.length) { 141 if (throwable == null && arguments[arguments.length - 1] instanceof Throwable) { 142 throwable = (Throwable) arguments[arguments.length - 1]; 143 resultArgCount--; 144 } 145 } 146 argArray = new Object[resultArgCount]; 147 for (int i = 0; i < resultArgCount; ++i) { 148 argArray[i] = arguments[i]; 149 } 150 151 if (argsCount == 1 && throwable == null && arguments.length > 1) { 152 // special case 153 stringArgs = new String[1]; 154 stringArgs[0] = deepToString(arguments); 155 } else { 156 stringArgs = new String[resultArgCount]; 157 for (int i = 0; i < stringArgs.length; i++) { 158 stringArgs[i] = deepToString(arguments[i]); 159 } 160 } 161 } 162 163 /** 164 * Returns the formatted message. 165 * @return the formatted message. 166 */ 167 public String getFormattedMessage() { 168 if (formattedMessage == null) { 169 formattedMessage = formatMessage(messagePattern, stringArgs); 170 } 171 return formattedMessage; 172 } 173 174 /** 175 * Returns the message pattern. 176 * @return the message pattern. 177 */ 178 public String getFormat() { 179 return messagePattern; 180 } 181 182 /** 183 * Returns the message parameters. 184 * @return the message parameters. 185 */ 186 public Object[] getParameters() { 187 if (argArray != null) { 188 return argArray; 189 } 190 return stringArgs; 191 } 192 193 /** 194 * Returns the Throwable that was given as the last argument, if any. 195 * It will not survive serialization. The Throwable exists as part of the message 196 * primarily so that it can be extracted from the end of the list of parameters 197 * and then be added to the LogEvent. As such, the Throwable in the event should 198 * not be used once the LogEvent has been constructed. 199 * 200 * @return the Throwable, if any. 201 */ 202 public Throwable getThrowable() { 203 return throwable; 204 } 205 206 protected String formatMessage(String msgPattern, String[] sArgs) { 207 return format(msgPattern, sArgs); 208 } 209 210 @Override 211 public boolean equals(Object o) { 212 if (this == o) { 213 return true; 214 } 215 if (o == null || getClass() != o.getClass()) { 216 return false; 217 } 218 219 ParameterizedMessage that = (ParameterizedMessage) o; 220 221 if (messagePattern != null ? !messagePattern.equals(that.messagePattern) : that.messagePattern != null) { 222 return false; 223 } 224 if (!Arrays.equals(stringArgs, that.stringArgs)) { 225 return false; 226 } 227 //if (throwable != null ? !throwable.equals(that.throwable) : that.throwable != null) return false; 228 229 return true; 230 } 231 232 @Override 233 public int hashCode() { 234 int result = messagePattern != null ? messagePattern.hashCode() : 0; 235 result = HASHVAL * result + (stringArgs != null ? Arrays.hashCode(stringArgs) : 0); 236 return result; 237 } 238 239 /** 240 * Replace placeholders in the given messagePattern with arguments. 241 * 242 * @param messagePattern the message pattern containing placeholders. 243 * @param arguments the arguments to be used to replace placeholders. 244 * @return the formatted message. 245 */ 246 public static String format(String messagePattern, Object[] arguments) { 247 if (messagePattern == null || arguments == null || arguments.length == 0) { 248 return messagePattern; 249 } 250 251 StringBuilder result = new StringBuilder(); 252 int escapeCounter = 0; 253 int currentArgument = 0; 254 for (int i = 0; i < messagePattern.length(); i++) { 255 char curChar = messagePattern.charAt(i); 256 if (curChar == ESCAPE_CHAR) { 257 escapeCounter++; 258 } else { 259 if (curChar == DELIM_START) { 260 if (i < messagePattern.length() - 1) { 261 if (messagePattern.charAt(i + 1) == DELIM_STOP) { 262 // write escaped escape chars 263 int escapedEscapes = escapeCounter / 2; 264 for (int j = 0; j < escapedEscapes; j++) { 265 result.append(ESCAPE_CHAR); 266 } 267 268 if (escapeCounter % 2 == 1) { 269 // i.e. escaped 270 // write escaped escape chars 271 result.append(DELIM_START); 272 result.append(DELIM_STOP); 273 } else { 274 // unescaped 275 if (currentArgument < arguments.length) { 276 result.append(arguments[currentArgument]); 277 } else { 278 result.append(DELIM_START).append(DELIM_STOP); 279 } 280 currentArgument++; 281 } 282 i++; 283 escapeCounter = 0; 284 continue; 285 } 286 } 287 } 288 // any other char beside ESCAPE or DELIM_START/STOP-combo 289 // write unescaped escape chars 290 if (escapeCounter > 0) { 291 for (int j = 0; j < escapeCounter; j++) { 292 result.append(ESCAPE_CHAR); 293 } 294 escapeCounter = 0; 295 } 296 result.append(curChar); 297 } 298 } 299 return result.toString(); 300 } 301 302 /** 303 * Counts the number of unescaped placeholders in the given messagePattern. 304 * 305 * @param messagePattern the message pattern to be analyzed. 306 * @return the number of unescaped placeholders. 307 */ 308 public static int countArgumentPlaceholders(String messagePattern) { 309 if (messagePattern == null) { 310 return 0; 311 } 312 313 int delim = messagePattern.indexOf(DELIM_START); 314 315 if (delim == -1) { 316 // special case, no placeholders at all. 317 return 0; 318 } 319 int result = 0; 320 boolean isEscaped = false; 321 for (int i = 0; i < messagePattern.length(); i++) { 322 char curChar = messagePattern.charAt(i); 323 if (curChar == ESCAPE_CHAR) { 324 isEscaped = !isEscaped; 325 } else if (curChar == DELIM_START) { 326 if (!isEscaped) { 327 if (i < messagePattern.length() - 1) { 328 if (messagePattern.charAt(i + 1) == DELIM_STOP) { 329 result++; 330 i++; 331 } 332 } 333 } 334 isEscaped = false; 335 } else { 336 isEscaped = false; 337 } 338 } 339 return result; 340 } 341 342 /** 343 * This method performs a deep toString of the given Object. 344 * Primitive arrays are converted using their respective Arrays.toString methods while 345 * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could 346 * contain themselves. 347 * <p/> 348 * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a 349 * behavior. They only check if the container is directly contained in itself, but not if a contained container 350 * contains the original one. Because of that, Arrays.toString(Object[]) isn't safe either. 351 * Confusing? Just read the last paragraph again and check the respective toString() implementation. 352 * <p/> 353 * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o) 354 * would produce a relatively hard-to-debug StackOverflowError. 355 * @param o The object. 356 * @return The String representation. 357 */ 358 public static String deepToString(Object o) { 359 if (o == null) { 360 return null; 361 } 362 if (o instanceof String) { 363 return (String) o; 364 } 365 StringBuilder str = new StringBuilder(); 366 Set<String> dejaVu = new HashSet<String>(); // that's actually a neat name ;) 367 recursiveDeepToString(o, str, dejaVu); 368 return str.toString(); 369 } 370 371 /** 372 * This method performs a deep toString of the given Object. 373 * Primitive arrays are converted using their respective Arrays.toString methods while 374 * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could 375 * contain themselves. 376 * <p/> 377 * dejaVu is used in case of those container types to prevent an endless recursion. 378 * <p/> 379 * It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a 380 * behavior. 381 * They only check if the container is directly contained in itself, but not if a contained container contains the 382 * original one. Because of that, Arrays.toString(Object[]) isn't safe either. 383 * Confusing? Just read the last paragraph again and check the respective toString() implementation. 384 * <p/> 385 * This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o) 386 * would produce a relatively hard-to-debug StackOverflowError. 387 * 388 * @param o the Object to convert into a String 389 * @param str the StringBuilder that o will be appended to 390 * @param dejaVu a list of container identities that were already used. 391 */ 392 private static void recursiveDeepToString(Object o, StringBuilder str, Set<String> dejaVu) { 393 if (o == null) { 394 str.append("null"); 395 return; 396 } 397 if (o instanceof String) { 398 str.append(o); 399 return; 400 } 401 402 Class<?> oClass = o.getClass(); 403 if (oClass.isArray()) { 404 if (oClass == byte[].class) { 405 str.append(Arrays.toString((byte[]) o)); 406 } else if (oClass == short[].class) { 407 str.append(Arrays.toString((short[]) o)); 408 } else if (oClass == int[].class) { 409 str.append(Arrays.toString((int[]) o)); 410 } else if (oClass == long[].class) { 411 str.append(Arrays.toString((long[]) o)); 412 } else if (oClass == float[].class) { 413 str.append(Arrays.toString((float[]) o)); 414 } else if (oClass == double[].class) { 415 str.append(Arrays.toString((double[]) o)); 416 } else if (oClass == boolean[].class) { 417 str.append(Arrays.toString((boolean[]) o)); 418 } else if (oClass == char[].class) { 419 str.append(Arrays.toString((char[]) o)); 420 } else { 421 // special handling of container Object[] 422 String id = identityToString(o); 423 if (dejaVu.contains(id)) { 424 str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX); 425 } else { 426 dejaVu.add(id); 427 Object[] oArray = (Object[]) o; 428 str.append("["); 429 boolean first = true; 430 for (Object current : oArray) { 431 if (first) { 432 first = false; 433 } else { 434 str.append(", "); 435 } 436 recursiveDeepToString(current, str, new HashSet<String>(dejaVu)); 437 } 438 str.append("]"); 439 } 440 //str.append(Arrays.deepToString((Object[]) o)); 441 } 442 } else if (o instanceof Map) { 443 // special handling of container Map 444 String id = identityToString(o); 445 if (dejaVu.contains(id)) { 446 str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX); 447 } else { 448 dejaVu.add(id); 449 Map<?, ?> oMap = (Map<?, ?>) o; 450 str.append("{"); 451 boolean isFirst = true; 452 for (Object o1 : oMap.entrySet()) { 453 Map.Entry<?, ?> current = (Map.Entry<?, ?>) o1; 454 if (isFirst) { 455 isFirst = false; 456 } else { 457 str.append(", "); 458 } 459 Object key = current.getKey(); 460 Object value = current.getValue(); 461 recursiveDeepToString(key, str, new HashSet<String>(dejaVu)); 462 str.append("="); 463 recursiveDeepToString(value, str, new HashSet<String>(dejaVu)); 464 } 465 str.append("}"); 466 } 467 } else if (o instanceof Collection) { 468 // special handling of container Collection 469 String id = identityToString(o); 470 if (dejaVu.contains(id)) { 471 str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX); 472 } else { 473 dejaVu.add(id); 474 Collection<?> oCol = (Collection<?>) o; 475 str.append("["); 476 boolean isFirst = true; 477 for (Object anOCol : oCol) { 478 if (isFirst) { 479 isFirst = false; 480 } else { 481 str.append(", "); 482 } 483 recursiveDeepToString(anOCol, str, new HashSet<String>(dejaVu)); 484 } 485 str.append("]"); 486 } 487 } else if (o instanceof Date) { 488 Date date = (Date) o; 489 SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); 490 // I'll leave it like this for the moment... this could probably be optimized using ThreadLocal... 491 str.append(format.format(date)); 492 } else { 493 // it's just some other Object, we can only use toString(). 494 try { 495 str.append(o.toString()); 496 } catch (Throwable t) { 497 str.append(ERROR_PREFIX); 498 str.append(identityToString(o)); 499 str.append(ERROR_SEPARATOR); 500 String msg = t.getMessage(); 501 String className = t.getClass().getName(); 502 str.append(className); 503 if (!className.equals(msg)) { 504 str.append(ERROR_MSG_SEPARATOR); 505 str.append(msg); 506 } 507 str.append(ERROR_SUFFIX); 508 } 509 } 510 } 511 512 /** 513 * This method returns the same as if Object.toString() would not have been 514 * overridden in obj. 515 * <p/> 516 * Note that this isn't 100% secure as collisions can always happen with hash codes. 517 * <p/> 518 * Copied from Object.hashCode(): 519 * As much as is reasonably practical, the hashCode method defined by 520 * class <tt>Object</tt> does return distinct integers for distinct 521 * objects. (This is typically implemented by converting the internal 522 * address of the object into an integer, but this implementation 523 * technique is not required by the 524 * Java<font size="-2"><sup>TM</sup></font> 525 * programming language.) 526 * 527 * @param obj the Object that is to be converted into an identity string. 528 * @return the identity string as also defined in Object.toString() 529 */ 530 public static String identityToString(Object obj) { 531 if (obj == null) { 532 return null; 533 } 534 return obj.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(obj)); 535 } 536 537 @Override 538 public String toString() { 539 return "ParameterizedMessage[messagePattern=" + messagePattern + ", stringArgs=" + 540 Arrays.toString(stringArgs) + ", throwable=" + throwable + "]"; 541 } 542 }