1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache license, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the license for the specific language governing permissions and
15 * limitations under the license.
16 */
17 package org.apache.logging.log4j.message;
18
19 import java.text.SimpleDateFormat;
20 import java.util.Arrays;
21 import java.util.Collection;
22 import java.util.Date;
23 import java.util.HashSet;
24 import java.util.Map;
25 import java.util.Set;
26
27 /**
28 * Handles messages that consist of a format string containing '{}' to represent each replaceable token, and
29 * the parameters.
30 * <p>
31 * This class was originally written for <a href="http://lilithapp.com/">Lilith</a> by Joern Huxhorn where it is
32 * licensed under the LGPL. It has been relicensed here with his permission providing that this attribution remain.
33 * </p>
34 */
35 public class ParameterizedMessage implements Message {
36
37 /**
38 * Prefix for recursion.
39 */
40 public static final String RECURSION_PREFIX = "[...";
41 /**
42 * Suffix for recursion.
43 */
44 public static final String RECURSION_SUFFIX = "...]";
45
46 /**
47 * Prefix for errors.
48 */
49 public static final String ERROR_PREFIX = "[!!!";
50 /**
51 * Separator for errors.
52 */
53 public static final String ERROR_SEPARATOR = "=>";
54 /**
55 * Separator for error messages.
56 */
57 public static final String ERROR_MSG_SEPARATOR = ":";
58 /**
59 * Suffix for errors.
60 */
61 public static final String ERROR_SUFFIX = "!!!]";
62
63 private static final long serialVersionUID = -665975803997290697L;
64
65 private static final int HASHVAL = 31;
66
67 private static final char DELIM_START = '{';
68 private static final char DELIM_STOP = '}';
69 private static final char ESCAPE_CHAR = '\\';
70
71 private final String messagePattern;
72 private final String[] stringArgs;
73 private transient Object[] argArray;
74 private transient String formattedMessage;
75 private transient Throwable throwable;
76
77 /**
78 * Creates a parameterized message.
79 * @param messagePattern The message "format" string. This will be a String containing "{}" placeholders
80 * where parameters should be substituted.
81 * @param stringArgs The arguments for substitution.
82 * @param throwable A Throwable.
83 */
84 public ParameterizedMessage(final String messagePattern, final String[] stringArgs, final Throwable throwable) {
85 this.messagePattern = messagePattern;
86 this.stringArgs = stringArgs;
87 this.throwable = throwable;
88 }
89
90 /**
91 * Creates a parameterized message.
92 * @param messagePattern The message "format" string. This will be a String containing "{}" placeholders
93 * where parameters should be substituted.
94 * @param objectArgs The arguments for substitution.
95 * @param throwable A Throwable.
96 */
97 public ParameterizedMessage(final String messagePattern, final Object[] objectArgs, final Throwable throwable) {
98 this.messagePattern = messagePattern;
99 this.throwable = throwable;
100 this.stringArgs = argumentsToStrings(objectArgs);
101 }
102
103 /**
104 * Constructs a ParameterizedMessage which contains the arguments converted to String as well as an optional
105 * Throwable.
106 *
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 {@link #getThrowable()} and won't be contained in the created String[].
109 * If it is used up {@link #getThrowable()} will return null even if the last argument was a Throwable!</p>
110 *
111 * @param messagePattern the message pattern that to be checked for placeholders.
112 * @param arguments the argument array to be converted.
113 */
114 public ParameterizedMessage(final String messagePattern, final Object[] arguments) {
115 this.messagePattern = messagePattern;
116 this.stringArgs = argumentsToStrings(arguments);
117 }
118
119 /**
120 * Constructor with a pattern and a single parameter.
121 * @param messagePattern The message pattern.
122 * @param arg The parameter.
123 */
124 public ParameterizedMessage(final String messagePattern, final Object arg) {
125 this(messagePattern, new Object[]{arg});
126 }
127
128 /**
129 * Constructor with a pattern and two parameters.
130 * @param messagePattern The message pattern.
131 * @param arg1 The first parameter.
132 * @param arg2 The second parameter.
133 */
134 public ParameterizedMessage(final String messagePattern, final Object arg1, final Object arg2) {
135 this(messagePattern, new Object[]{arg1, arg2});
136 }
137
138 private String[] argumentsToStrings(final Object[] arguments) {
139 if (arguments == null) {
140 return null;
141 }
142 final int argsCount = countArgumentPlaceholders(messagePattern);
143 int resultArgCount = arguments.length;
144 if (argsCount < arguments.length && throwable == null && arguments[arguments.length - 1] instanceof Throwable) {
145 throwable = (Throwable) arguments[arguments.length - 1];
146 resultArgCount--;
147 }
148 argArray = new Object[resultArgCount];
149 System.arraycopy(arguments, 0, argArray, 0, resultArgCount);
150
151 String[] strArgs;
152 if (argsCount == 1 && throwable == null && arguments.length > 1) {
153 // special case
154 strArgs = new String[1];
155 strArgs[0] = deepToString(arguments);
156 } else {
157 strArgs = new String[resultArgCount];
158 for (int i = 0; i < strArgs.length; i++) {
159 strArgs[i] = deepToString(arguments[i]);
160 }
161 }
162 return strArgs;
163 }
164
165 /**
166 * Returns the formatted message.
167 * @return the formatted message.
168 */
169 @Override
170 public String getFormattedMessage() {
171 if (formattedMessage == null) {
172 formattedMessage = formatMessage(messagePattern, stringArgs);
173 }
174 return formattedMessage;
175 }
176
177 /**
178 * Returns the message pattern.
179 * @return the message pattern.
180 */
181 @Override
182 public String getFormat() {
183 return messagePattern;
184 }
185
186 /**
187 * Returns the message parameters.
188 * @return the message parameters.
189 */
190 @Override
191 public Object[] getParameters() {
192 if (argArray != null) {
193 return argArray;
194 }
195 return stringArgs;
196 }
197
198 /**
199 * Returns the Throwable that was given as the last argument, if any.
200 * It will not survive serialization. The Throwable exists as part of the message
201 * primarily so that it can be extracted from the end of the list of parameters
202 * and then be added to the LogEvent. As such, the Throwable in the event should
203 * not be used once the LogEvent has been constructed.
204 *
205 * @return the Throwable, if any.
206 */
207 @Override
208 public Throwable getThrowable() {
209 return throwable;
210 }
211
212 protected String formatMessage(final String msgPattern, final String[] sArgs) {
213 return format(msgPattern, sArgs);
214 }
215
216 @Override
217 public boolean equals(final Object o) {
218 if (this == o) {
219 return true;
220 }
221 if (o == null || getClass() != o.getClass()) {
222 return false;
223 }
224
225 final ParameterizedMessage that = (ParameterizedMessage) o;
226
227 if (messagePattern != null ? !messagePattern.equals(that.messagePattern) : that.messagePattern != null) {
228 return false;
229 }
230 if (!Arrays.equals(stringArgs, that.stringArgs)) {
231 return false;
232 }
233 //if (throwable != null ? !throwable.equals(that.throwable) : that.throwable != null) return false;
234
235 return true;
236 }
237
238 @Override
239 public int hashCode() {
240 int result = messagePattern != null ? messagePattern.hashCode() : 0;
241 result = HASHVAL * result + (stringArgs != null ? Arrays.hashCode(stringArgs) : 0);
242 return result;
243 }
244
245 /**
246 * Replace placeholders in the given messagePattern with arguments.
247 *
248 * @param messagePattern the message pattern containing placeholders.
249 * @param arguments the arguments to be used to replace placeholders.
250 * @return the formatted message.
251 */
252 public static String format(final String messagePattern, final Object[] arguments) {
253 if (messagePattern == null || arguments == null || arguments.length == 0) {
254 return messagePattern;
255 }
256
257 final StringBuilder result = new StringBuilder();
258 int escapeCounter = 0;
259 int currentArgument = 0;
260 for (int i = 0; i < messagePattern.length(); i++) {
261 final char curChar = messagePattern.charAt(i);
262 if (curChar == ESCAPE_CHAR) {
263 escapeCounter++;
264 } else {
265 if (curChar == DELIM_START && i < messagePattern.length() - 1
266 && messagePattern.charAt(i + 1) == DELIM_STOP) {
267 // write escaped escape chars
268 final int escapedEscapes = escapeCounter / 2;
269 for (int j = 0; j < escapedEscapes; j++) {
270 result.append(ESCAPE_CHAR);
271 }
272
273 if (escapeCounter % 2 == 1) {
274 // i.e. escaped
275 // write escaped escape chars
276 result.append(DELIM_START);
277 result.append(DELIM_STOP);
278 } else {
279 // unescaped
280 if (currentArgument < arguments.length) {
281 result.append(arguments[currentArgument]);
282 } else {
283 result.append(DELIM_START).append(DELIM_STOP);
284 }
285 currentArgument++;
286 }
287 i++;
288 escapeCounter = 0;
289 continue;
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 && i < messagePattern.length() - 1 && messagePattern.charAt(i + 1) == DELIM_STOP) {
330 result++;
331 i++;
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 * <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 * </p>
356 * @param o The object.
357 * @return The String representation.
358 */
359 public static String deepToString(final Object o) {
360 if (o == null) {
361 return null;
362 }
363 if (o instanceof String) {
364 return (String) o;
365 }
366 final StringBuilder str = new StringBuilder();
367 final Set<String> dejaVu = new HashSet<String>(); // that's actually a neat name ;)
368 recursiveDeepToString(o, str, dejaVu);
369 return str.toString();
370 }
371
372 /**
373 * This method performs a deep toString of the given Object.
374 * Primitive arrays are converted using their respective Arrays.toString methods while
375 * special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
376 * contain themselves.
377 * <p>
378 * dejaVu is used in case of those container types to prevent an endless recursion.
379 * </p>
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 * <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 * </p>
391 *
392 * @param o the Object to convert into a String
393 * @param str the StringBuilder that o will be appended to
394 * @param dejaVu a list of container identities that were already used.
395 */
396 private static void recursiveDeepToString(final Object o, final StringBuilder str, final Set<String> dejaVu) {
397 if (o == null) {
398 str.append("null");
399 return;
400 }
401 if (o instanceof String) {
402 str.append(o);
403 return;
404 }
405
406 final Class<?> oClass = o.getClass();
407 if (oClass.isArray()) {
408 if (oClass == byte[].class) {
409 str.append(Arrays.toString((byte[]) o));
410 } else if (oClass == short[].class) {
411 str.append(Arrays.toString((short[]) o));
412 } else if (oClass == int[].class) {
413 str.append(Arrays.toString((int[]) o));
414 } else if (oClass == long[].class) {
415 str.append(Arrays.toString((long[]) o));
416 } else if (oClass == float[].class) {
417 str.append(Arrays.toString((float[]) o));
418 } else if (oClass == double[].class) {
419 str.append(Arrays.toString((double[]) o));
420 } else if (oClass == boolean[].class) {
421 str.append(Arrays.toString((boolean[]) o));
422 } else if (oClass == char[].class) {
423 str.append(Arrays.toString((char[]) o));
424 } else {
425 // special handling of container Object[]
426 final String id = identityToString(o);
427 if (dejaVu.contains(id)) {
428 str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
429 } else {
430 dejaVu.add(id);
431 final Object[] oArray = (Object[]) o;
432 str.append('[');
433 boolean first = true;
434 for (final Object current : oArray) {
435 if (first) {
436 first = false;
437 } else {
438 str.append(", ");
439 }
440 recursiveDeepToString(current, str, new HashSet<String>(dejaVu));
441 }
442 str.append(']');
443 }
444 //str.append(Arrays.deepToString((Object[]) o));
445 }
446 } else if (o instanceof Map) {
447 // special handling of container Map
448 final String id = identityToString(o);
449 if (dejaVu.contains(id)) {
450 str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
451 } else {
452 dejaVu.add(id);
453 final Map<?, ?> oMap = (Map<?, ?>) o;
454 str.append('{');
455 boolean isFirst = true;
456 for (final Object o1 : oMap.entrySet()) {
457 final Map.Entry<?, ?> current = (Map.Entry<?, ?>) o1;
458 if (isFirst) {
459 isFirst = false;
460 } else {
461 str.append(", ");
462 }
463 final Object key = current.getKey();
464 final Object value = current.getValue();
465 recursiveDeepToString(key, str, new HashSet<String>(dejaVu));
466 str.append('=');
467 recursiveDeepToString(value, str, new HashSet<String>(dejaVu));
468 }
469 str.append('}');
470 }
471 } else if (o instanceof Collection) {
472 // special handling of container Collection
473 final String id = identityToString(o);
474 if (dejaVu.contains(id)) {
475 str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
476 } else {
477 dejaVu.add(id);
478 final Collection<?> oCol = (Collection<?>) o;
479 str.append('[');
480 boolean isFirst = true;
481 for (final Object anOCol : oCol) {
482 if (isFirst) {
483 isFirst = false;
484 } else {
485 str.append(", ");
486 }
487 recursiveDeepToString(anOCol, str, new HashSet<String>(dejaVu));
488 }
489 str.append(']');
490 }
491 } else if (o instanceof Date) {
492 final Date date = (Date) o;
493 final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
494 // I'll leave it like this for the moment... this could probably be optimized using ThreadLocal...
495 str.append(format.format(date));
496 } else {
497 // it's just some other Object, we can only use toString().
498 try {
499 str.append(o.toString());
500 } catch (final Throwable t) {
501 str.append(ERROR_PREFIX);
502 str.append(identityToString(o));
503 str.append(ERROR_SEPARATOR);
504 final String msg = t.getMessage();
505 final String className = t.getClass().getName();
506 str.append(className);
507 if (!className.equals(msg)) {
508 str.append(ERROR_MSG_SEPARATOR);
509 str.append(msg);
510 }
511 str.append(ERROR_SUFFIX);
512 }
513 }
514 }
515
516 /**
517 * This method returns the same as if Object.toString() would not have been
518 * overridden in obj.
519 * <p>
520 * Note that this isn't 100% secure as collisions can always happen with hash codes.
521 * </p>
522 * <p>
523 * Copied from Object.hashCode():
524 * </p>
525 * <blockquote>
526 * As much as is reasonably practical, the hashCode method defined by
527 * class {@code Object} does return distinct integers for distinct
528 * objects. (This is typically implemented by converting the internal
529 * address of the object into an integer, but this implementation
530 * technique is not required by the Java™ programming language.)
531 * </blockquote>
532 *
533 * @param obj the Object that is to be converted into an identity string.
534 * @return the identity string as also defined in Object.toString()
535 */
536 public static String identityToString(final Object obj) {
537 if (obj == null) {
538 return null;
539 }
540 return obj.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(obj));
541 }
542
543 @Override
544 public String toString() {
545 return "ParameterizedMessage[messagePattern=" + messagePattern + ", stringArgs=" +
546 Arrays.toString(stringArgs) + ", throwable=" + throwable + ']';
547 }
548 }