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