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