001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.core.pattern;
018
019import static org.fusesource.jansi.AnsiRenderer.Code.BG_RED;
020import static org.fusesource.jansi.AnsiRenderer.Code.BOLD;
021import static org.fusesource.jansi.AnsiRenderer.Code.RED;
022import static org.fusesource.jansi.AnsiRenderer.Code.WHITE;
023import static org.fusesource.jansi.AnsiRenderer.Code.YELLOW;
024
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.Locale;
028import java.util.Map;
029
030import org.apache.logging.log4j.status.StatusLogger;
031import org.fusesource.jansi.Ansi;
032import org.fusesource.jansi.AnsiRenderer;
033import org.fusesource.jansi.AnsiRenderer.Code;
034
035/**
036 * Renders an input as ANSI escaped output.
037 *
038 * Uses the JAnsi rendering syntax as the default to render a message into an ANSI escaped string.
039 *
040 * The default syntax for embedded ANSI codes is:
041 *
042 * <pre>
043 *   &#64;|<em>code</em>(,<em>code</em>)* <em>text</em>|@
044 * </pre>
045 *
046 * For example, to render the message {@code "Hello"} in green, use:
047 *
048 * <pre>
049 *   &#64;|green Hello|@
050 * </pre>
051 *
052 * To render the message {@code "Hello"} in bold and red, use:
053 *
054 * <pre>
055 *   &#64;|bold,red Warning!|@
056 * </pre>
057 *
058 * You can also define custom style names in the configuration with the syntax:
059 *
060 * <pre>
061 * %message{ansi}{StyleName=value(,value)*( StyleName=value(,value)*)*}%n
062 * </pre>
063 *
064 * For example:
065 *
066 * <pre>
067 * %message{ansi}{WarningStyle=red,bold KeyStyle=white ValueStyle=blue}%n
068 * </pre>
069 *
070 * The call site can look like this:
071 *
072 * <pre>
073 * logger.info("@|KeyStyle {}|@ = @|ValueStyle {}|@", entry.getKey(), entry.getValue());
074 * </pre>
075 *
076 * Note: This class originally copied and then heavily modified code from JAnsi's AnsiRenderer (which is licensed as
077 * Apache 2.0.)
078 *
079 * @see AnsiRenderer
080 */
081public final class JAnsiTextRenderer implements TextRenderer {
082
083    public static final Map<String, Code[]> DefaultExceptionStyleMap;
084    static final Map<String, Code[]> DefaultMessageStyleMap;
085    private static final Map<String, Map<String, Code[]>> PrefedinedStyleMaps;
086
087    private static void put(final Map<String, Code[]> map, final String name, final Code... codes) {
088        map.put(name, codes);
089    }
090
091    static {
092        final Map<String, Map<String, Code[]>> tempPreDefs = new HashMap<>();
093        // Default style: Spock
094        {
095            // TODO Should the keys be in an enum?
096            final Map<String, Code[]> map = new HashMap<>();
097            put(map, "Prefix", WHITE);
098            put(map, "Name", BG_RED, WHITE);
099            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}