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