View Javadoc
1   /*
2    * This file is part of Waarp Project (named also Waarp or GG).
3    *
4    *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
5    *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
6    * individual contributors.
7    *
8    *  All Waarp Project is free software: you can redistribute it and/or
9    * modify it under the terms of the GNU General Public License as published by
10   * the Free Software Foundation, either version 3 of the License, or (at your
11   * option) any later version.
12   *
13   * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
14   * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15   * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16   *
17   *  You should have received a copy of the GNU General Public License along with
18   * Waarp . If not, see <http://www.gnu.org/licenses/>.
19   */
20  
21  /*
22   * Copyright 2013 The Netty Project
23   * The Netty Project licenses this file to you under the Apache License,
24   * version 2.0 (the "License"); you may not use this file except in compliance
25   * with the License. You may obtain a copy of the License at:
26   * http://www.apache.org/licenses/LICENSE-2.0
27   * Unless required by applicable law or agreed to in writing, software
28   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
29   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
30   * License for the specific language governing permissions and limitations
31   * under the License.
32   */
33  /**
34   * Copyright (c) 2004-2011 QOS.ch
35   * All rights reserved.
36   * <p>
37   * Permission is hereby granted, free of charge, to any person obtaining
38   * a copy of this software and associated documentation files (the
39   * "Software"), to deal in the Software without restriction, including
40   * without limitation the rights to use, copy, modify, merge, publish,
41   * distribute, sublicense, and/or sell copies of the Software, and to
42   * permit persons to whom the Software is furnished to do so, subject to
43   * the following conditions:
44   * <p>
45   * The above copyright notice and this permission notice shall be
46   * included in all copies or substantial portions of the Software.
47   * <p>
48   * THE SOFTWARE IS PROVIDED "AS  IS", WITHOUT WARRANTY OF ANY KIND,
49   * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
50   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
51   * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
52   * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
53   * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
54   * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
55   */
56  package org.waarp.common.logging;
57  
58  import org.waarp.common.utility.ParametersChecker;
59  
60  import java.text.MessageFormat;
61  import java.util.HashMap;
62  import java.util.Map;
63  
64  // contributors: lizongbo: proposed special treatment of array parameter values
65  // Joern Huxhorn: pointed out double[] omission, suggested deep array copy
66  
67  /**
68   * Formats messages according to very simple substitution rules. Substitutions
69   * can be made 1, 2 or more
70   * arguments.
71   * <p/>
72   * <p/>
73   * For example,
74   * <p/>
75   *
76   * <pre>
77   * MessageFormatter.format(&quot;Hi {}.&quot;, &quot;there&quot;)
78   * </pre>
79   * <p/>
80   * will return the string "Hi there.".
81   * <p/>
82   * The {} pair is called the <em>formatting anchor</em>. It serves to designate
83   * the location where arguments
84   * need to be substituted within the message pattern.
85   * <p/>
86   * In case your message contains the '{' or the '}' character, you do not have
87   * to do anything special unless
88   * the '}' character immediately follows '{'. For example,
89   * <p/>
90   *
91   * <pre>
92   * MessageFormatter.format(&quot;Set {1,2,3} is not equal to {}.&quot;, &quot;1,2&quot;);
93   * </pre>
94   * <p/>
95   * will return the string "Set {1,2,3} is not equal to 1,2.".
96   * <p/>
97   * <p/>
98   * If for whatever reason you need to place the string "{}" in the message
99   * without its <em>formatting
100  * anchor</em> meaning, then you need to escape the '{' character with '\',
101  * that
102  * is the backslash character.
103  * Only the '{' character should be escaped. There is no need to escape the '}'
104  * character. For example,
105  * <p/>
106  *
107  * <pre>
108  * MessageFormatter.format(&quot;Set \\{} is not equal to {}.&quot;, &quot;1,2&quot;);
109  * </pre>
110  * <p/>
111  * will return the string "Set {} is not equal to 1,2.".
112  * <p/>
113  * <p/>
114  * The escaping behavior just described can be overridden by escaping the
115  * escape
116  * character '\'. Calling
117  * <p/>
118  *
119  * <pre>
120  * MessageFormatter.format(&quot;File name is C:\\\\{}.&quot;, &quot;file.zip&quot;);
121  * </pre>
122  * <p/>
123  * will return the string "File name is C:\file.zip".
124  * <p/>
125  * <p/>
126  * The formatting conventions are different than those of {@link MessageFormat}
127  * which ships with the Java
128  * platform. This is justified by the fact that SLF4J's implementation is 10
129  * times faster than that of
130  * {@link MessageFormat}. This local performance difference is both measurable
131  * and significant in the larger
132  * context of the complete logging processing chain.
133  * <p/>
134  * <p/>
135  * See also {@link #format(String, Object)}, {@link #format(String, Object,
136  * Object)} and
137  * {@link #arrayFormat(String, Object[])} methods for more details.
138  */
139 final class MessageFormatter {
140 
141   static final char DELIM_START = '{';
142   static final char DELIM_STOP = '}';
143   static final String DELIM_STR = "{}";
144   private static final char ESCAPE_CHAR = '\\';
145 
146   /**
147    * Performs single argument substitution for the 'messagePattern' passed as
148    * parameter.
149    * <p/>
150    * For example,
151    * <p/>
152    *
153    * <pre>
154    * MessageFormatter.format(&quot;Hi {}.&quot;, &quot;there&quot;);
155    * </pre>
156    * <p/>
157    * will return the string "Hi there.".
158    * <p/>
159    *
160    * @param messagePattern The message pattern which will be parsed
161    *     and
162    *     formatted
163    * @param arg The argument to be substituted in place of the
164    *     formatting
165    *     anchor
166    *
167    * @return The formatted message
168    */
169   static FormattingTuple format(final String messagePattern, final Object arg) {
170     return arrayFormat(messagePattern, new Object[] { arg });
171   }
172 
173   /**
174    * Performs a two argument substitution for the 'messagePattern' passed as
175    * parameter.
176    * <p/>
177    * For example,
178    * <p/>
179    *
180    * <pre>
181    * MessageFormatter.format(&quot;Hi {}. My name is {}.&quot;, &quot;Alice&quot;, &quot;Bob&quot;);
182    * </pre>
183    * <p/>
184    * will return the string "Hi Alice. My name is Bob.".
185    *
186    * @param messagePattern The message pattern which will be parsed
187    *     and
188    *     formatted
189    * @param argA The argument to be substituted in place of the first
190    *     formatting anchor
191    * @param argB The argument to be substituted in place of the second
192    *     formatting anchor
193    *
194    * @return The formatted message
195    */
196   static FormattingTuple format(final String messagePattern, final Object argA,
197                                 final Object argB) {
198     return arrayFormat(messagePattern, new Object[] { argA, argB });
199   }
200 
201   static Throwable getThrowableCandidate(final Object[] argArray) {
202     if (argArray == null || argArray.length == 0) {
203       return null;
204     }
205 
206     final Object lastEntry = argArray[argArray.length - 1];
207     if (lastEntry instanceof Throwable) {
208       return (Throwable) lastEntry;
209     }
210     return null;
211   }
212 
213   /**
214    * Same principle as the {@link #format(String, Object)} and {@link
215    * #format(String, Object, Object)} methods
216    * except that any number of arguments can be passed in an array.
217    *
218    * @param messagePattern The message pattern which will be parsed
219    *     and
220    *     formatted
221    * @param argArray An array of arguments to be substituted in place
222    *     of
223    *     formatting anchors
224    *
225    * @return The formatted message
226    */
227   static FormattingTuple arrayFormat(final String messagePattern,
228                                      final Object[] argArray) {
229 
230     final Throwable throwableCandidate = getThrowableCandidate(argArray);
231 
232     if (messagePattern == null) {
233       return new FormattingTuple(null, argArray, throwableCandidate);
234     }
235 
236     if (argArray == null) {
237       return new FormattingTuple(messagePattern);
238     }
239 
240     int i = 0;
241     int j;
242     final StringBuilder sbuild =
243         new StringBuilder(messagePattern.length() + 50);
244 
245     int l;
246     for (l = 0; l < argArray.length; l++) {
247 
248       j = messagePattern.indexOf(DELIM_STR, i);
249 
250       if (j == -1) {
251         // no more variables
252         if (i == 0) {
253           // this is a simple string
254           return new FormattingTuple(messagePattern, argArray,
255                                      throwableCandidate);
256         } else {
257           // add the tail string which contains no variables and return the result.
258           sbuild.append(messagePattern.substring(i));
259           return new FormattingTuple(sbuild.toString(), argArray,
260                                      throwableCandidate);
261         }
262       } else {
263         if (isEscapedDelimeter(messagePattern, j)) {
264           if (!isDoubleEscaped(messagePattern, j)) {
265             l--;
266             // DELIM_START was escaped, thus should not be incremented
267             sbuild.append(messagePattern, i, j - 1).append(DELIM_START);
268             i = j + 1;
269           } else {
270             // The escape character preceding the delimiter start is
271             // itself escaped: "abc x:\\{}"
272             // we have to consume one backward slash
273             sbuild.append(messagePattern, i, j - 1);
274             deeplyAppendParameter(sbuild, argArray[l],
275                                   new HashMap<Object[], Void>());
276             i = j + 2;
277           }
278         } else {
279           // normal case
280           sbuild.append(messagePattern, i, j);
281           deeplyAppendParameter(sbuild, argArray[l],
282                                 new HashMap<Object[], Void>());
283           i = j + 2;
284         }
285       }
286     }
287     // append the characters following the last {} pair.
288     sbuild.append(messagePattern.substring(i));
289     if (l < argArray.length - 1) {
290       return new FormattingTuple(sbuild.toString(), argArray,
291                                  throwableCandidate);
292     } else {
293       return new FormattingTuple(sbuild.toString(), argArray, null);
294     }
295   }
296 
297   static boolean isEscapedDelimeter(final String messagePattern,
298                                     final int delimeterStartIndex) {
299     ParametersChecker.checkParameterNullOnly("Must not be null",
300                                              messagePattern);
301     if (delimeterStartIndex == 0) {
302       return false;
303     }
304     return messagePattern.charAt(delimeterStartIndex - 1) == ESCAPE_CHAR;
305   }
306 
307   static boolean isDoubleEscaped(final String messagePattern,
308                                  final int delimeterStartIndex) {
309     ParametersChecker.checkParameterNullOnly("Must not be null",
310                                              messagePattern);
311     return delimeterStartIndex >= 2 &&
312            delimeterStartIndex - 2 > messagePattern.length() &&
313            messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR;
314   }
315 
316   // special treatment of array values was suggested by 'lizongbo'
317   static void deeplyAppendParameter(final StringBuilder sbuild, final Object o,
318                                     final Map<Object[], Void> seenMap) {
319     if (o == null) {
320       sbuild.append("null");
321       return;
322     }
323     if (!o.getClass().isArray()) {
324       safeObjectAppend(sbuild, o);
325     } else {
326       // check for primitive array types because they
327       // unfortunately cannot be cast to Object[]
328       if (o instanceof boolean[]) {
329         booleanArrayAppend(sbuild, (boolean[]) o);
330       } else if (o instanceof byte[]) {
331         byteArrayAppend(sbuild, (byte[]) o);
332       } else if (o instanceof char[]) {
333         charArrayAppend(sbuild, (char[]) o);
334       } else if (o instanceof short[]) {
335         shortArrayAppend(sbuild, (short[]) o);
336       } else if (o instanceof int[]) {
337         intArrayAppend(sbuild, (int[]) o);
338       } else if (o instanceof long[]) {
339         longArrayAppend(sbuild, (long[]) o);
340       } else if (o instanceof float[]) {
341         floatArrayAppend(sbuild, (float[]) o);
342       } else if (o instanceof double[]) {
343         doubleArrayAppend(sbuild, (double[]) o);
344       } else {
345         objectArrayAppend(sbuild, (Object[]) o, seenMap);
346       }
347     }
348   }
349 
350   private static void safeObjectAppend(final StringBuilder sbuild,
351                                        final Object o) {
352     try {
353       final String oAsString = o.toString();
354       sbuild.append(oAsString);
355     } catch (final Exception t) {
356       SysErrLogger.FAKE_LOGGER.ignoreLog(t);
357       SysErrLogger.FAKE_LOGGER.syserr(
358           "SLF4J: Failed toString() invocation on an object of type [" +
359           o.getClass().getName() + ']' + t.getMessage());
360       sbuild.append("[FAILED toString()]");
361     }
362   }
363 
364   private static void objectArrayAppend(final StringBuilder sbuild,
365                                         final Object[] a,
366                                         final Map<Object[], Void> seenMap) {
367     sbuild.append('[');
368     if (!seenMap.containsKey(a)) {
369       seenMap.put(a, null);
370       final int len = a.length;
371       for (int i = 0; i < len; i++) {
372         deeplyAppendParameter(sbuild, a[i], seenMap);
373         if (i != len - 1) {
374           sbuild.append(", ");
375         }
376       }
377       // allow repeats in siblings
378       seenMap.remove(a);
379     } else {
380       sbuild.append("...");
381     }
382     sbuild.append(']');
383   }
384 
385   private static void booleanArrayAppend(final StringBuilder sbuild,
386                                          final boolean[] a) {
387     sbuild.append('[');
388     final int len = a.length;
389     for (int i = 0; i < len; i++) {
390       sbuild.append(a[i]);
391       if (i != len - 1) {
392         sbuild.append(", ");
393       }
394     }
395     sbuild.append(']');
396   }
397 
398   private static void byteArrayAppend(final StringBuilder sbuild,
399                                       final byte[] a) {
400     sbuild.append('[');
401     final int len = a.length;
402     for (int i = 0; i < len; i++) {
403       sbuild.append(a[i]);
404       if (i != len - 1) {
405         sbuild.append(", ");
406       }
407     }
408     sbuild.append(']');
409   }
410 
411   private static void charArrayAppend(final StringBuilder sbuild,
412                                       final char[] a) {
413     sbuild.append('[');
414     final int len = a.length;
415     for (int i = 0; i < len; i++) {
416       sbuild.append(a[i]);
417       if (i != len - 1) {
418         sbuild.append(", ");
419       }
420     }
421     sbuild.append(']');
422   }
423 
424   private static void shortArrayAppend(final StringBuilder sbuild,
425                                        final short[] a) {
426     sbuild.append('[');
427     final int len = a.length;
428     for (int i = 0; i < len; i++) {
429       sbuild.append(a[i]);
430       if (i != len - 1) {
431         sbuild.append(", ");
432       }
433     }
434     sbuild.append(']');
435   }
436 
437   private static void intArrayAppend(final StringBuilder sbuild,
438                                      final int[] a) {
439     sbuild.append('[');
440     final int len = a.length;
441     for (int i = 0; i < len; i++) {
442       sbuild.append(a[i]);
443       if (i != len - 1) {
444         sbuild.append(", ");
445       }
446     }
447     sbuild.append(']');
448   }
449 
450   private static void longArrayAppend(final StringBuilder sbuild,
451                                       final long[] a) {
452     sbuild.append('[');
453     final int len = a.length;
454     for (int i = 0; i < len; i++) {
455       sbuild.append(a[i]);
456       if (i != len - 1) {
457         sbuild.append(", ");
458       }
459     }
460     sbuild.append(']');
461   }
462 
463   private static void floatArrayAppend(final StringBuilder sbuild,
464                                        final float[] a) {
465     sbuild.append('[');
466     final int len = a.length;
467     for (int i = 0; i < len; i++) {
468       sbuild.append(a[i]);
469       if (i != len - 1) {
470         sbuild.append(", ");
471       }
472     }
473     sbuild.append(']');
474   }
475 
476   private static void doubleArrayAppend(final StringBuilder sbuild,
477                                         final double[] a) {
478     sbuild.append('[');
479     final int len = a.length;
480     for (int i = 0; i < len; i++) {
481       sbuild.append(a[i]);
482       if (i != len - 1) {
483         sbuild.append(", ");
484       }
485     }
486     sbuild.append(']');
487   }
488 
489   private MessageFormatter() {
490   }
491 }