View Javadoc
1   /*
2    * Copyright (c) 2001-2017, Zoltan Farkas All Rights Reserved.
3    *
4    * This library is free software; you can redistribute it and/or
5    * modify it under the terms of the GNU Lesser General Public
6    * License as published by the Free Software Foundation; either
7    * version 2.1 of the License, or (at your option) any later version.
8    *
9    * This library is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public
15   * License along with this program; if not, write to the Free Software
16   * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
17   *
18   * Additionally licensed with:
19   *
20   * Licensed under the Apache License, Version 2.0 (the "License");
21   * you may not use this file except in compliance with the License.
22   * You may obtain a copy of the License at
23   *
24   *      http://www.apache.org/licenses/LICENSE-2.0
25   *
26   * Unless required by applicable law or agreed to in writing, software
27   * distributed under the License is distributed on an "AS IS" BASIS,
28   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29   * See the License for the specific language governing permissions and
30   * limitations under the License.
31   */
32  package org.spf4j.base;
33  
34  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
35  import gnu.trove.set.hash.THashSet;
36  import java.io.IOException;
37  import java.io.UncheckedIOException;
38  import java.util.Arrays;
39  import java.util.Set;
40  import javax.annotation.Nonnull;
41  import org.spf4j.io.ObjectAppenderSupplier;
42  
43  /**
44   * A more flexible implementation of the SLF4j message formatter (org.slf4j.helpers.MessageFormatter). the following
45   * improvements:
46   *
47   * 1) Allow to format to a procvided destination (Appendable) allowing you to reduce the amount of garbage generated in
48   * a custom formatter... 2) Lets you know which arguments have been used in the message allowing you to implement extra
49   * logic to handle the unused ones 3) Lets you plug custom formatters for argument types. (you can get better
50   * performance and more flexibility) 4) Processing arguments that are arrays is sligtly faster than the slf4j formatter.
51   *
52   * @author zoly
53   */
54  public final class Slf4jMessageFormatter {
55  
56    private static final char DELIM_START = '{';
57    private static final String DELIM_STR = "{}";
58    private static final char ESCAPE_CHAR = '\\';
59  
60  
61    public interface ErrorHandler {
62      void accept(Object obj, Appendable sbuf, Throwable t) throws IOException;
63    }
64  
65  
66  
67    private Slf4jMessageFormatter() {
68    }
69  
70  
71    @SuppressWarnings("checkstyle:regexp")
72    public static void exHandle(final Object obj, final Appendable sbuf, final Throwable t) throws IOException {
73      String className = obj.getClass().getName();
74      synchronized (System.err) {
75        System.err.print("SPF4J: Failed toString() invocation on an object of type [");
76        System.err.print(className);
77        System.err.println(']');
78      }
79      Throwables.writeTo(t, System.err, Throwables.PackageDetail.SHORT);
80      sbuf.append("[FAILED toString() for ");
81      sbuf.append(className);
82      sbuf.append(']');
83    }
84  
85    public static String toString(@Nonnull final String messagePattern,
86            final Object... argArray) {
87      StringBuilder sb = new StringBuilder(messagePattern.length() + argArray.length * 8);
88      try {
89        int nrUsed = format(sb, messagePattern, argArray);
90        if (nrUsed != argArray.length) {
91          throw new IllegalArgumentException("Invalid format "
92                  + messagePattern + ", params " + Arrays.toString(argArray));
93        }
94      } catch (IOException ex) {
95       throw new UncheckedIOException(ex);
96      }
97      return sb.toString();
98    }
99  
100 
101   /**
102    * Slf4j message formatter.
103    *
104    * @param to Appendable to put formatted message to.
105    * @param messagePattern see org.slf4j.helpers.MessageFormatter for format.
106    * @param argArray the message arguments.
107    * @return the number of arguments used in the message.
108    * @throws IOException
109    */
110   public static int format(@Nonnull final Appendable to, @Nonnull final String messagePattern,
111           final Object... argArray) throws IOException {
112     return format(to, messagePattern, ObjectAppenderSupplier.TO_STRINGER, argArray);
113   }
114 
115   /**
116    * Slf4j message formatter.
117    *
118    * @param to Appendable to put formatted message to.
119    * @param appSupplier a supplier that will provide the serialization method for a particular argument type.
120    * @param messagePattern see org.slf4j.helpers.MessageFormatter for format.
121    * @param argArray the message arguments.
122    * @return the number of arguments used in the message.
123    * @throws IOException
124    */
125   public static int format(@Nonnull final Appendable to,
126           @Nonnull final ObjectAppenderSupplier appSupplier, @Nonnull final String messagePattern,
127           final Object... argArray) throws IOException {
128     return format(to, messagePattern, appSupplier, argArray);
129   }
130 
131   /**
132    * slf4j message formatter.
133    *
134    * @param to Appendable to put formatted message to.
135    * @param messagePattern see org.slf4j.helpers.MessageFormatter for format.
136    * @param appSupplier a supplier that will provide the serialization method for a particular argument type.
137    * @param argArray the message arguments.
138    * @return the number of arguments used in the message.
139    * @throws IOException something wend wrong while writing to the appendable.
140    */
141   public static int format(@Nonnull final Appendable to, @Nonnull final String messagePattern,
142           @Nonnull final ObjectAppenderSupplier appSupplier, final Object... argArray) throws IOException {
143     return format(0, to, messagePattern, appSupplier, argArray);
144   }
145 
146   /**
147    * Slf4j message formatter.
148    *
149    * @param to Appendable to put formatted message to.
150    * @param messagePattern see org.slf4j.helpers.MessageFormatter for format.
151    * @param appSupplier a supplier that will provide the serialization method for a particular argument type.
152    * @param firstArgIdx the index of the first parameter.
153    * @param argArray the message arguments.
154    * @return the index of the last arguments used in the message + 1.
155    * @throws IOException something wend wrong while writing to the appendable.
156    */
157   public static int format(final int firstArgIdx, @Nonnull final Appendable to, @Nonnull final String messagePattern,
158           @Nonnull final ObjectAppenderSupplier appSupplier, final Object... argArray) throws IOException {
159     return format(Slf4jMessageFormatter::exHandle, firstArgIdx, to, messagePattern, appSupplier, argArray);
160   }
161 
162 
163   public static int getFormatParameterNumber(@Nonnull final String messagePattern) {
164     int nrParams = 0;
165     int i = 0;
166     int j;
167     while ((j = messagePattern.indexOf(DELIM_STR, i)) >= 0) {
168         if (isEscapedDelimeter(messagePattern, j)) {
169           if (isDoubleEscaped(messagePattern, j)) {
170             // The escape character preceding the delimiter start is
171             // itself escaped: "abc x:\\{}"
172             // we have to consume one backward slash
173             nrParams++;
174             i = j + 2;
175           } else {
176             i = j + 1;
177           }
178         } else {
179           // normal case
180           nrParams++;
181           i = j + 2;
182         }
183     }
184     return nrParams;
185   }
186 
187 
188 
189 
190   /**
191    * Slf4j message formatter.
192    *
193    * @param safe - if true recoverable exHandle will be caught when writing arguments, and a error will be appended
194  instead.
195    * @param to Appendable to put formatted message to.
196    * @param messagePattern see org.slf4j.helpers.MessageFormatter for format.
197    * @param appSupplier a supplier that will provide the serialization method for a particular argument type.
198    * @param firstArgIdx the index of the first parameter.
199    * @param argArray the message arguments.
200    * @return the index of the last arguments used in the message + 1.
201    * @throws IOException something wend wrong while writing to the appendable.
202    */
203   public static int format(final ErrorHandler exHandler, final int firstArgIdx,
204           @Nonnull final Appendable to, @Nonnull final String messagePattern,
205           @Nonnull final ObjectAppenderSupplier appSupplier, final Object... argArray)
206           throws IOException {
207     int i = 0;
208     final int len = argArray.length;
209     int k = firstArgIdx;
210     for (; k < len; k++) {
211       int j = messagePattern.indexOf(DELIM_STR, i);
212       if (j == -1) {
213         // no more variables
214         break;
215       } else {
216         if (isEscapedDelimeter(messagePattern, j)) {
217           if (isDoubleEscaped(messagePattern, j)) {
218             // The escape character preceding the delimiter start is
219             // itself escaped: "abc x:\\{}"
220             // we have to consume one backward slash
221             to.append(messagePattern, i, j - 1);
222             deeplyAppendParameter(exHandler, to, argArray[k], new THashSet<>(), appSupplier);
223             i = j + 2;
224           } else {
225             k--; // DELIM_START was escaped, thus should not be incremented
226             to.append(messagePattern, i, j - 1);
227             to.append(DELIM_START);
228             i = j + 1;
229           }
230         } else {
231           // normal case
232           to.append(messagePattern, i, j);
233           deeplyAppendParameter(exHandler, to, argArray[k], new THashSet<>(), appSupplier);
234           i = j + 2;
235         }
236       }
237     }
238     // append the characters following the last {} pair.
239     to.append(messagePattern, i, messagePattern.length());
240     return k;
241   }
242 
243   private static boolean isEscapedDelimeter(final String messagePattern, final int delimeterStartIndex) {
244     if (delimeterStartIndex == 0) {
245       return false;
246     }
247     return messagePattern.charAt(delimeterStartIndex - 1) == ESCAPE_CHAR;
248   }
249 
250   private static boolean isDoubleEscaped(final String messagePattern, final int delimeterStartIndex) {
251     return delimeterStartIndex >= 2 && messagePattern.charAt(delimeterStartIndex - 2) == ESCAPE_CHAR;
252   }
253 
254   // special treatment of array values was suggested by 'lizongbo'
255   @SuppressFBWarnings("ITC_INHERITANCE_TYPE_CHECKING")
256   private static void deeplyAppendParameter(final ErrorHandler exHandler, final Appendable sbuf, final Object o,
257           final Set<Object[]> seen, final ObjectAppenderSupplier appSupplier) throws IOException {
258     if (o == null) {
259       sbuf.append("null");
260       return;
261     }
262     if (!o.getClass().isArray()) {
263       safeObjectAppend(exHandler, sbuf, o, appSupplier);
264     } else {
265       // check for primitive array types because they
266       // unfortunately cannot be cast to Object[]
267       if (o instanceof boolean[]) {
268         booleanArrayAppend(sbuf, (boolean[]) o);
269       } else if (o instanceof byte[]) {
270         byteArrayAppend(sbuf, (byte[]) o);
271       } else if (o instanceof char[]) {
272         charArrayAppend(sbuf, (char[]) o);
273       } else if (o instanceof short[]) {
274         shortArrayAppend(sbuf, (short[]) o);
275       } else if (o instanceof int[]) {
276         intArrayAppend(sbuf, (int[]) o);
277       } else if (o instanceof long[]) {
278         longArrayAppend(sbuf, (long[]) o);
279       } else if (o instanceof float[]) {
280         floatArrayAppend(sbuf, (float[]) o);
281       } else if (o instanceof double[]) {
282         doubleArrayAppend(sbuf, (double[]) o);
283       } else {
284         objectArrayAppend(exHandler, sbuf, (Object[]) o, seen, appSupplier);
285       }
286     }
287   }
288 
289   @SuppressWarnings("unchecked")
290   public static void safeObjectAppend(final ErrorHandler exHandler, final Appendable sbuf, final Object obj,
291           final ObjectAppenderSupplier appSupplier) throws IOException {
292     try {
293       appSupplier.get((Class) obj.getClass()).append(obj, sbuf, appSupplier);
294     } catch (IOException | RuntimeException | StackOverflowError t) {
295       exHandler.accept(obj, sbuf, t);
296     }
297 
298   }
299 
300   @SuppressFBWarnings("ABC_ARRAY_BASED_COLLECTIONS")
301   private static void objectArrayAppend(final ErrorHandler exHandler, final Appendable sbuf,
302           final Object[] a, final Set<Object[]> seen,
303           final ObjectAppenderSupplier appSupplier) throws IOException {
304     sbuf.append('[');
305     if (seen.add(a)) {
306       final int len = a.length;
307       if (len > 0) {
308         deeplyAppendParameter(exHandler, sbuf, a[0], seen, appSupplier);
309         for (int i = 1; i < len; i++) {
310           sbuf.append(", ");
311           deeplyAppendParameter(exHandler, sbuf, a[i], seen, appSupplier);
312         }
313       }
314       // allow repeats in siblings
315       seen.remove(a);
316     } else {
317       sbuf.append("...");
318     }
319     sbuf.append(']');
320   }
321 
322   private static void booleanArrayAppend(final Appendable sbuf, final boolean[] a) throws IOException {
323     sbuf.append('[');
324     final int len = a.length;
325     if (len > 0) {
326       sbuf.append(Boolean.toString(a[0]));
327       for (int i = 1; i < len; i++) {
328         sbuf.append(", ");
329         sbuf.append(Boolean.toString(a[i]));
330       }
331     }
332     sbuf.append(']');
333   }
334 
335   private static void byteArrayAppend(final Appendable sbuf, final byte[] a) throws IOException {
336     sbuf.append('[');
337     final int len = a.length;
338     if (len > 0) {
339       sbuf.append(Byte.toString(a[0]));
340       for (int i = 1; i < len; i++) {
341         sbuf.append(", ");
342         sbuf.append(Byte.toString(a[i]));
343       }
344     }
345     sbuf.append(']');
346   }
347 
348   private static void charArrayAppend(final Appendable sbuf, final char[] a) throws IOException {
349     sbuf.append('[');
350     final int len = a.length;
351     if (len > 0) {
352       sbuf.append(a[0]);
353       for (int i = 1; i < len; i++) {
354         sbuf.append(", ");
355         sbuf.append(a[i]);
356       }
357     }
358     sbuf.append(']');
359   }
360 
361   private static void shortArrayAppend(final Appendable sbuf, final short[] a) throws IOException {
362     sbuf.append('[');
363     final int len = a.length;
364     if (len > 0) {
365       sbuf.append(Short.toString(a[0]));
366       for (int i = 1; i < len; i++) {
367         sbuf.append(", ");
368         sbuf.append(Short.toString(a[i]));
369       }
370     }
371     sbuf.append(']');
372   }
373 
374   private static void intArrayAppend(final Appendable sbuf, final int[] a) throws IOException {
375     sbuf.append('[');
376     final int len = a.length;
377     if (len > 0) {
378       sbuf.append(Integer.toString(a[0]));
379       for (int i = 1; i < len; i++) {
380         sbuf.append(", ");
381         sbuf.append(Integer.toString(a[i]));
382       }
383     }
384     sbuf.append(']');
385   }
386 
387   private static void longArrayAppend(final Appendable sbuf, final long[] a) throws IOException {
388     sbuf.append('[');
389     final int len = a.length;
390     if (len > 0) {
391       sbuf.append(Long.toString(a[0]));
392       for (int i = 1; i < len; i++) {
393         sbuf.append(", ");
394         sbuf.append(Long.toString(a[i]));
395       }
396     }
397     sbuf.append(']');
398   }
399 
400   private static void floatArrayAppend(final Appendable sbuf, final float[] a) throws IOException {
401     sbuf.append('[');
402     final int len = a.length;
403     if (len > 0) {
404       sbuf.append(Float.toString(a[0]));
405       for (int i = 1; i < len; i++) {
406         sbuf.append(", ");
407         sbuf.append(Float.toString(a[i]));
408       }
409     }
410     sbuf.append(']');
411   }
412 
413   private static void doubleArrayAppend(final Appendable sbuf, final double[] a) throws IOException {
414     sbuf.append('[');
415     final int len = a.length;
416     if (len > 0) {
417       sbuf.append(Double.toString(a[0]));
418       for (int i = 1; i < len; i++) {
419         sbuf.append(", ");
420         sbuf.append(Double.toString(a[i]));
421       }
422     }
423     sbuf.append(']');
424   }
425 
426 }