View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.pattern;
18  
19  import static org.fusesource.jansi.AnsiRenderer.Code.BG_RED;
20  import static org.fusesource.jansi.AnsiRenderer.Code.BOLD;
21  import static org.fusesource.jansi.AnsiRenderer.Code.RED;
22  import static org.fusesource.jansi.AnsiRenderer.Code.WHITE;
23  import static org.fusesource.jansi.AnsiRenderer.Code.YELLOW;
24  
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.Locale;
28  import java.util.Map;
29  
30  import org.apache.logging.log4j.status.StatusLogger;
31  import org.fusesource.jansi.Ansi;
32  import org.fusesource.jansi.AnsiRenderer;
33  import org.fusesource.jansi.AnsiRenderer.Code;
34  
35  /**
36   * Renders an input as ANSI escaped output.
37   *
38   * Uses the JAnsi rendering syntax as the default to render a message into an ANSI escaped string.
39   *
40   * The default syntax for embedded ANSI codes is:
41   *
42   * <pre>
43   *   &#64;|<em>code</em>(,<em>code</em>)* <em>text</em>|@
44   * </pre>
45   *
46   * For example, to render the message {@code "Hello"} in green, use:
47   *
48   * <pre>
49   *   &#64;|green Hello|@
50   * </pre>
51   *
52   * To render the message {@code "Hello"} in bold and red, use:
53   *
54   * <pre>
55   *   &#64;|bold,red Warning!|@
56   * </pre>
57   *
58   * You can also define custom style names in the configuration with the syntax:
59   *
60   * <pre>
61   * %message{ansi}{StyleName=value(,value)*( StyleName=value(,value)*)*}%n
62   * </pre>
63   *
64   * For example:
65   *
66   * <pre>
67   * %message{ansi}{WarningStyle=red,bold KeyStyle=white ValueStyle=blue}%n
68   * </pre>
69   *
70   * The call site can look like this:
71   *
72   * <pre>
73   * logger.info("@|KeyStyle {}|@ = @|ValueStyle {}|@", entry.getKey(), entry.getValue());
74   * </pre>
75   *
76   * Note: This class originally copied and then heavily modified code from JAnsi's AnsiRenderer (which is licensed as
77   * Apache 2.0.)
78   *
79   * @see AnsiRenderer
80   */
81  public final class JAnsiTextRenderer implements TextRenderer {
82  
83      public static final Map<String, Code[]> DefaultExceptionStyleMap;
84      static final Map<String, Code[]> DefaultMessageStyleMap;
85      private static final Map<String, Map<String, Code[]>> PrefedinedStyleMaps;
86  
87      private static void put(final Map<String, Code[]> map, final String name, final Code... codes) {
88          map.put(name, codes);
89      }
90  
91      static {
92          final Map<String, Map<String, Code[]>> tempPreDefs = new HashMap<>();
93          // Default style: Spock
94          {
95              // TODO Should the keys be in an enum?
96              final Map<String, Code[]> map = new HashMap<>();
97              put(map, "Prefix", WHITE);
98              put(map, "Name", BG_RED, WHITE);
99              put(map, "NameMessageSeparator", BG_RED, WHITE);
100             put(map, "Message", BG_RED, WHITE, BOLD);
101             put(map, "At", WHITE);
102             put(map, "CauseLabel", WHITE);
103             put(map, "Text", WHITE);
104             put(map, "More", WHITE);
105             put(map, "Suppressed", WHITE);
106             // StackTraceElement
107             put(map, "StackTraceElement.ClassName", YELLOW);
108             put(map, "StackTraceElement.ClassMethodSeparator", YELLOW);
109             put(map, "StackTraceElement.MethodName", YELLOW);
110             put(map, "StackTraceElement.NativeMethod", YELLOW);
111             put(map, "StackTraceElement.FileName", RED);
112             put(map, "StackTraceElement.LineNumber", RED);
113             put(map, "StackTraceElement.Container", RED);
114             put(map, "StackTraceElement.ContainerSeparator", WHITE);
115             put(map, "StackTraceElement.UnknownSource", RED);
116             // ExtraClassInfo
117             put(map, "ExtraClassInfo.Inexact", YELLOW);
118             put(map, "ExtraClassInfo.Container", YELLOW);
119             put(map, "ExtraClassInfo.ContainerSeparator", YELLOW);
120             put(map, "ExtraClassInfo.Location", YELLOW);
121             put(map, "ExtraClassInfo.Version", YELLOW);
122             // Save
123             DefaultExceptionStyleMap = Collections.unmodifiableMap(map);
124             tempPreDefs.put("Spock", DefaultExceptionStyleMap);
125         }
126         // Style: Kirk
127         {
128             // TODO Should the keys be in an enum?
129             final Map<String, Code[]> map = new HashMap<>();
130             put(map, "Prefix", WHITE);
131             put(map, "Name", BG_RED, YELLOW, BOLD);
132             put(map, "NameMessageSeparator", BG_RED, YELLOW);
133             put(map, "Message", BG_RED, WHITE, BOLD);
134             put(map, "At", WHITE);
135             put(map, "CauseLabel", WHITE);
136             put(map, "Text", WHITE);
137             put(map, "More", WHITE);
138             put(map, "Suppressed", WHITE);
139             // StackTraceElement
140             put(map, "StackTraceElement.ClassName", BG_RED, WHITE);
141             put(map, "StackTraceElement.ClassMethodSeparator", BG_RED, YELLOW);
142             put(map, "StackTraceElement.MethodName", BG_RED, YELLOW);
143             put(map, "StackTraceElement.NativeMethod", BG_RED, YELLOW);
144             put(map, "StackTraceElement.FileName", RED);
145             put(map, "StackTraceElement.LineNumber", RED);
146             put(map, "StackTraceElement.Container", RED);
147             put(map, "StackTraceElement.ContainerSeparator", WHITE);
148             put(map, "StackTraceElement.UnknownSource", RED);
149             // ExtraClassInfo
150             put(map, "ExtraClassInfo.Inexact", YELLOW);
151             put(map, "ExtraClassInfo.Container", WHITE);
152             put(map, "ExtraClassInfo.ContainerSeparator", WHITE);
153             put(map, "ExtraClassInfo.Location", YELLOW);
154             put(map, "ExtraClassInfo.Version", YELLOW);
155             // Save
156             tempPreDefs.put("Kirk", Collections.unmodifiableMap(map));
157         }
158         {
159             final Map<String, Code[]> temp = new HashMap<>();
160             // TODO
161             DefaultMessageStyleMap = Collections.unmodifiableMap(temp);
162         }
163         PrefedinedStyleMaps = Collections.unmodifiableMap(tempPreDefs);
164     }
165 
166     private final String beginToken;
167     private final int beginTokenLen;
168     private final String endToken;
169     private final int endTokenLen;
170     private final Map<String, Code[]> styleMap;
171 
172     public JAnsiTextRenderer(final String[] formats, final Map<String, Code[]> defaultStyleMap) {
173         String tempBeginToken = AnsiRenderer.BEGIN_TOKEN;
174         String tempEndToken = AnsiRenderer.END_TOKEN;
175         Map<String, Code[]> map;
176         if (formats.length > 1) {
177             final String allStylesStr = formats[1];
178             // Style def split
179             final String[] allStyleAssignmentsArr = allStylesStr.split(" ");
180             map = new HashMap<>(allStyleAssignmentsArr.length + defaultStyleMap.size());
181             map.putAll(defaultStyleMap);
182             for (final String styleAssignmentStr : allStyleAssignmentsArr) {
183                 final String[] styleAssignmentArr = styleAssignmentStr.split("=");
184                 if (styleAssignmentArr.length != 2) {
185                     StatusLogger.getLogger().warn("{} parsing style \"{}\", expected format: StyleName=Code(,Code)*",
186                             getClass().getSimpleName(), styleAssignmentStr);
187                 } else {
188                     final String styleName = styleAssignmentArr[0];
189                     final String codeListStr = styleAssignmentArr[1];
190                     final String[] codeNames = codeListStr.split(",");
191                     if (codeNames.length == 0) {
192                         StatusLogger.getLogger().warn(
193                                 "{} parsing style \"{}\", expected format: StyleName=Code(,Code)*",
194                                 getClass().getSimpleName(), styleAssignmentStr);
195                     } else {
196                         switch (styleName) {
197                         case "BeginToken":
198                             tempBeginToken = codeNames[0];
199                             break;
200                         case "EndToken":
201                             tempEndToken = codeNames[0];
202                             break;
203                         case "StyleMapName":
204                             final String predefinedMapName = codeNames[0];
205                             final Map<String, Code[]> predefinedMap = PrefedinedStyleMaps.get(predefinedMapName);
206                             if (predefinedMap != null) {
207                                 map.putAll(predefinedMap);
208                             } else {
209                                 StatusLogger.getLogger().warn("Unknown predefined map name {}, pick one of {}",
210                                         predefinedMapName, null);
211                             }
212                             break;
213                         default:
214                             final Code[] codes = new Code[codeNames.length];
215                             for (int i = 0; i < codes.length; i++) {
216                                 codes[i] = toCode(codeNames[i]);
217                             }
218                             map.put(styleName, codes);
219                         }
220                     }
221                 }
222             }
223         } else {
224             map = defaultStyleMap;
225         }
226         styleMap = map;
227         beginToken = tempBeginToken;
228         endToken = tempEndToken;
229         beginTokenLen = tempBeginToken.length();
230         endTokenLen = tempEndToken.length();
231     }
232 
233     public Map<String, Code[]> getStyleMap() {
234         return styleMap;
235     }
236 
237     private void render(final Ansi ansi, final Code code) {
238         if (code.isColor()) {
239             if (code.isBackground()) {
240                 ansi.bg(code.getColor());
241             } else {
242                 ansi.fg(code.getColor());
243             }
244         } else if (code.isAttribute()) {
245             ansi.a(code.getAttribute());
246         }
247     }
248 
249     private void render(final Ansi ansi, final Code... codes) {
250         for (final Code code : codes) {
251             render(ansi, code);
252         }
253     }
254 
255     /**
256      * Renders the given text with the given names which can be ANSI code names or Log4j style names.
257      *
258      * @param text
259      *            The text to render
260      * @param names
261      *            ANSI code names or Log4j style names.
262      * @return A rendered string containing ANSI codes.
263      */
264     private String render(final String text, final String... names) {
265         final Ansi ansi = Ansi.ansi();
266         for (final String name : names) {
267             final Code[] codes = styleMap.get(name);
268             if (codes != null) {
269                 render(ansi, codes);
270             } else {
271                 render(ansi, toCode(name));
272             }
273         }
274         return ansi.a(text).reset().toString();
275     }
276 
277     // EXACT COPY OF StringBuilder version of the method but typed as String for input
278     @Override
279     public void render(final String input, final StringBuilder output, final String styleName)
280             throws IllegalArgumentException {
281         output.append(render(input, styleName));
282     }
283 
284     @Override
285     public void render(final StringBuilder input, final StringBuilder output) throws IllegalArgumentException {
286         int i = 0;
287         int j, k;
288 
289         while (true) {
290             j = input.indexOf(beginToken, i);
291             if (j == -1) {
292                 if (i == 0) {
293                     output.append(input);
294                     return;
295                 }
296                 output.append(input.substring(i, input.length()));
297                 return;
298             }
299             output.append(input.substring(i, j));
300             k = input.indexOf(endToken, j);
301 
302             if (k == -1) {
303                 output.append(input);
304                 return;
305             }
306             j += beginTokenLen;
307             final String spec = input.substring(j, k);
308 
309             final String[] items = spec.split(AnsiRenderer.CODE_TEXT_SEPARATOR, 2);
310             if (items.length == 1) {
311                 output.append(input);
312                 return;
313             }
314             final String replacement = render(items[1], items[0].split(","));
315 
316             output.append(replacement);
317 
318             i = k + endTokenLen;
319         }
320     }
321 
322     private Code toCode(final String name) {
323         return Code.valueOf(name.toUpperCase(Locale.ENGLISH));
324     }
325 
326     @Override
327     public String toString() {
328         return "JAnsiMessageRenderer [beginToken=" + beginToken + ", beginTokenLen=" + beginTokenLen + ", endToken="
329                 + endToken + ", endTokenLen=" + endTokenLen + ", styleMap=" + styleMap + "]";
330     }
331 
332 }