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 }