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