1   /* 
2    * Copyright (c) 2004-2007 QOS.ch
3    * All rights reserved.
4    * 
5    * Permission is hereby granted, free  of charge, to any person obtaining
6    * a  copy  of this  software  and  associated  documentation files  (the
7    * "Software"), to  deal in  the Software without  restriction, including
8    * without limitation  the rights to  use, copy, modify,  merge, publish,
9    * distribute,  sublicense, and/or sell  copies of  the Software,  and to
10   * permit persons to whom the Software  is furnished to do so, subject to
11   * the following conditions:
12   * 
13   * The  above  copyright  notice  and  this permission  notice  shall  be
14   * included in all copies or substantial portions of the Software.
15   * 
16   * THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
17   * EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
18   * MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
19   * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20   * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21   * OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION
22   * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23   */
24  
25  package org.slf4j.helpers;
26  
27  import java.util.HashMap;
28  import java.util.Map;
29  
30  // contributors: lizongbo: proposed special treatment of array parameter values
31  // Jörn Huxhorn: pointed out double[] omission, suggested deep array copy
32  /**
33   * Formats messages according to very simple substitution rules. Substitutions
34   * can be made 1, 2 or more arguments.
35   * <p>
36   * For example,
37   * 
38   * <pre>
39   * MessageFormatter.format(&quot;Hi {}.&quot;, &quot;there&quot;)
40   * </pre>
41   * 
42   * will return the string "Hi there.".
43   * <p>
44   * The {} pair is called the <em>formatting anchor</em>. It serves to
45   * designate the location where arguments need to be substituted within the
46   * message pattern.
47   * <p>
48   * In case your message contains the '{' or the '}' character, you do not have
49   * to do anything special unless the '}' character immediately follows '{'. For
50   * example,
51   * 
52   * <pre>
53   * MessageFormatter.format(&quot;Set {1,2,3} is not equal to {}.&quot;, &quot;1,2&quot;);
54   * </pre>
55   * 
56   * will return the string "Set {1,2,3} is not equal to 1,2.".
57   * 
58   * <p>
59   * If for whatever reason you need to place the string "{}" in the message
60   * without its <em>formatting anchor</em> meaning, then you need to escape the
61   * '{' character with '\', that is the backslash character. Only the '{'
62   * character should be escaped. There is no need to escape the '}' character.
63   * For example,
64   * 
65   * <pre>
66   * MessageFormatter.format(&quot;Set \\{} is not equal to {}.&quot;, &quot;1,2&quot;);
67   * </pre>
68   * 
69   * will return the string "Set {} is not equal to 1,2.".
70   * 
71   * <p>
72   * The escaping behavior just described can be overridden by escaping the escape
73   * character '\'. Calling
74   * 
75   * <pre>
76   * MessageFormatter.format(&quot;File name is C:\\\\{}.&quot;, &quot;file.zip&quot;);
77   * </pre>
78   * 
79   * will return the string "File name is C:\file.zip".
80   * 
81   * <p>
82   * See {@link #format(String, Object)}, {@link #format(String, Object, Object)}
83   * and {@link #arrayFormat(String, Object[])} methods for more details.
84   * 
85   * @author Ceki G&uuml;lc&uuml;
86   */
87  final public class MessageFormatter {
88    static final char DELIM_START = '{';
89    static final char DELIM_STOP = '}';
90    static final String DELIM_STR = "{}";
91    private static final char ESCAPE_CHAR = '\\';
92  
93    /**
94     * Performs single argument substitution for the 'messagePattern' passed as
95     * parameter.
96     * <p>
97     * For example,
98     * 
99     * <pre>
100    * MessageFormatter.format(&quot;Hi {}.&quot;, &quot;there&quot;);
101    * </pre>
102    * 
103    * will return the string "Hi there.".
104    * <p>
105    * 
106    * @param messagePattern
107    *                The message pattern which will be parsed and formatted
108    * @param argument
109    *                The argument to be substituted in place of the formatting
110    *                anchor
111    * @return The formatted message
112    */
113   final public static String format(String messagePattern, Object arg) {
114     return arrayFormat(messagePattern, new Object[] { arg });
115   }
116 
117   /**
118    * 
119    * Performs a two argument substitution for the 'messagePattern' passed as
120    * parameter.
121    * <p>
122    * For example,
123    * 
124    * <pre>
125    * MessageFormatter.format(&quot;Hi {}. My name is {}.&quot;, &quot;Alice&quot;, &quot;Bob&quot;);
126    * </pre>
127    * 
128    * will return the string "Hi Alice. My name is Bob.".
129    * 
130    * @param messagePattern
131    *                The message pattern which will be parsed and formatted
132    * @param arg1
133    *                The argument to be substituted in place of the first
134    *                formatting anchor
135    * @param arg2
136    *                The argument to be substituted in place of the second
137    *                formatting anchor
138    * @return The formatted message
139    */
140   final public static String format(final String messagePattern, Object arg1,
141       Object arg2) {
142     return arrayFormat(messagePattern, new Object[] { arg1, arg2 });
143   }
144 
145   /**
146    * Same principle as the {@link #format(String, Object)} and
147    * {@link #format(String, Object, Object)} methods except that any number of
148    * arguments can be passed in an array.
149    * 
150    * @param messagePattern
151    *                The message pattern which will be parsed and formatted
152    * @param argArray
153    *                An array of arguments to be substituted in place of
154    *                formatting anchors
155    * @return The formatted message
156    */
157   final public static String arrayFormat(final String messagePattern,
158       final Object[] argArray) {
159     if (messagePattern == null) {
160       return null;
161     }
162     if (argArray == null) {
163       return messagePattern;
164     }
165     int i = 0;
166     int j;
167     StringBuffer sbuf = new StringBuffer(messagePattern.length() + 50);
168 
169     for (int L = 0; L < argArray.length; L++) {
170 
171       j = messagePattern.indexOf(DELIM_STR, i);
172 
173       if (j == -1) {
174         // no more variables
175         if (i == 0) { // this is a simple string
176           return messagePattern;
177         } else { // add the tail string which contains no variables and return
178           // the result.
179           sbuf.append(messagePattern.substring(i, messagePattern.length()));
180           return sbuf.toString();
181         }
182       } else {
183         if (isEscapedDelimeter(messagePattern, j)) {
184           if (!isDoubleEscaped(messagePattern, j)) {
185             L--; // DELIM_START was escaped, thus should not be incremented
186             sbuf.append(messagePattern.substring(i, j - 1));
187             sbuf.append(DELIM_START);
188             i = j + 1;
189           } else {
190             // The escape character preceding the delimiter start is
191             // itself escaped: "abc x:\\{}"
192             // we have to consume one backward slash
193             sbuf.append(messagePattern.substring(i, j - 1));
194             deeplyAppendParameter(sbuf, argArray[L], new HashMap());
195             i = j + 2;
196           }
197         } else {
198           // normal case
199           sbuf.append(messagePattern.substring(i, j));
200           deeplyAppendParameter(sbuf, argArray[L], new HashMap());
201           i = j + 2;
202         }
203       }
204     }
205     // append the characters following the last {} pair.
206     sbuf.append(messagePattern.substring(i, messagePattern.length()));
207     return sbuf.toString();
208   }
209 
210   final static boolean isEscapedDelimeter(String messagePattern,
211       int delimeterStartIndex) {
212 
213     if (delimeterStartIndex == 0) {
214       return false;
215     }
216     char potentialEscape = messagePattern.charAt(delimeterStartIndex - 1);
217     if (potentialEscape == ESCAPE_CHAR) {
218       return true;
219     } else {
220       return false;
221     }
222   }
223 
224   final static boolean isDoubleEscaped(String messagePattern,
225       int delimeterStartIndex) {
226     if (delimeterStartIndex >= 2
227         && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR) {
228       return true;
229     } else {
230       return false;
231     }
232   }
233 
234   // special treatment of array values was suggested by 'lizongbo'
235   private static void deeplyAppendParameter(StringBuffer sbuf, Object o,
236       Map seenMap) {
237     if (o == null) {
238       sbuf.append("null");
239       return;
240     }
241     if (!o.getClass().isArray()) {
242       safeObjectAppend(sbuf, o);
243     } else {
244       // check for primitive array types because they
245       // unfortunately cannot be cast to Object[]
246       if (o instanceof boolean[]) {
247         booleanArrayAppend(sbuf, (boolean[]) o);
248       } else if (o instanceof byte[]) {
249         byteArrayAppend(sbuf, (byte[]) o);
250       } else if (o instanceof char[]) {
251         charArrayAppend(sbuf, (char[]) o);
252       } else if (o instanceof short[]) {
253         shortArrayAppend(sbuf, (short[]) o);
254       } else if (o instanceof int[]) {
255         intArrayAppend(sbuf, (int[]) o);
256       } else if (o instanceof long[]) {
257         longArrayAppend(sbuf, (long[]) o);
258       } else if (o instanceof float[]) {
259         floatArrayAppend(sbuf, (float[]) o);
260       } else if (o instanceof double[]) {
261         doubleArrayAppend(sbuf, (double[]) o);
262       } else {
263         objectArrayAppend(sbuf, (Object[]) o, seenMap);
264       }
265     }
266   }
267 
268   private static void safeObjectAppend(StringBuffer sbuf, Object o) {
269     try {
270       String oAsString = o.toString();
271       sbuf.append(oAsString);
272     } catch( Throwable t) {
273       System.err.println("SLF4J: Failed toString() invocation on an object of type ["+o.getClass().getName()+"]");
274       t.printStackTrace();
275       sbuf.append("[FAILED toString()]");
276     }
277 
278   }
279 
280   private static void objectArrayAppend(StringBuffer sbuf, Object[] a,
281       Map seenMap) {
282     sbuf.append('[');
283     if (!seenMap.containsKey(a)) {
284       seenMap.put(a, null);
285       final int len = a.length;
286       for (int i = 0; i < len; i++) {
287         deeplyAppendParameter(sbuf, a[i], seenMap);
288         if (i != len - 1)
289           sbuf.append(", ");
290       }
291       // allow repeats in siblings
292       seenMap.remove(a);
293     } else {
294       sbuf.append("...");
295     }
296     sbuf.append(']');
297   }
298 
299   private static void booleanArrayAppend(StringBuffer sbuf, boolean[] a) {
300     sbuf.append('[');
301     final int len = a.length;
302     for (int i = 0; i < len; i++) {
303       sbuf.append(a[i]);
304       if (i != len - 1)
305         sbuf.append(", ");
306     }
307     sbuf.append(']');
308   }
309 
310   private static void byteArrayAppend(StringBuffer sbuf, byte[] a) {
311     sbuf.append('[');
312     final int len = a.length;
313     for (int i = 0; i < len; i++) {
314       sbuf.append(a[i]);
315       if (i != len - 1)
316         sbuf.append(", ");
317     }
318     sbuf.append(']');
319   }
320 
321   private static void charArrayAppend(StringBuffer sbuf, char[] a) {
322     sbuf.append('[');
323     final int len = a.length;
324     for (int i = 0; i < len; i++) {
325       sbuf.append(a[i]);
326       if (i != len - 1)
327         sbuf.append(", ");
328     }
329     sbuf.append(']');
330   }
331 
332   private static void shortArrayAppend(StringBuffer sbuf, short[] a) {
333     sbuf.append('[');
334     final int len = a.length;
335     for (int i = 0; i < len; i++) {
336       sbuf.append(a[i]);
337       if (i != len - 1)
338         sbuf.append(", ");
339     }
340     sbuf.append(']');
341   }
342 
343   private static void intArrayAppend(StringBuffer sbuf, int[] a) {
344     sbuf.append('[');
345     final int len = a.length;
346     for (int i = 0; i < len; i++) {
347       sbuf.append(a[i]);
348       if (i != len - 1)
349         sbuf.append(", ");
350     }
351     sbuf.append(']');
352   }
353 
354   private static void longArrayAppend(StringBuffer sbuf, long[] a) {
355     sbuf.append('[');
356     final int len = a.length;
357     for (int i = 0; i < len; i++) {
358       sbuf.append(a[i]);
359       if (i != len - 1)
360         sbuf.append(", ");
361     }
362     sbuf.append(']');
363   }
364 
365   private static void floatArrayAppend(StringBuffer sbuf, float[] a) {
366     sbuf.append('[');
367     final int len = a.length;
368     for (int i = 0; i < len; i++) {
369       sbuf.append(a[i]);
370       if (i != len - 1)
371         sbuf.append(", ");
372     }
373     sbuf.append(']');
374   }
375 
376   private static void doubleArrayAppend(StringBuffer sbuf, double[] a) {
377     sbuf.append('[');
378     final int len = a.length;
379     for (int i = 0; i < len; i++) {
380       sbuf.append(a[i]);
381       if (i != len - 1)
382         sbuf.append(", ");
383     }
384     sbuf.append(']');
385   }
386 }