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.impl;
018
019import java.util.ArrayList;
020import java.util.List;
021import java.util.Scanner;
022
023import org.apache.logging.log4j.core.pattern.JAnsiTextRenderer;
024import org.apache.logging.log4j.core.pattern.PlainTextRenderer;
025import org.apache.logging.log4j.core.pattern.TextRenderer;
026import org.apache.logging.log4j.core.util.Loader;
027import org.apache.logging.log4j.core.util.Patterns;
028import org.apache.logging.log4j.status.StatusLogger;
029import org.apache.logging.log4j.util.Strings;
030
031/**
032 * Contains options which control how a {@link Throwable} pattern is formatted.
033 */
034public final class ThrowableFormatOptions {
035
036    private static final int DEFAULT_LINES = Integer.MAX_VALUE;
037
038    /**
039     * Default instance of {@code ThrowableFormatOptions}.
040     */
041    protected static final ThrowableFormatOptions DEFAULT = new ThrowableFormatOptions();
042
043    /**
044     * Format the whole stack trace.
045     */
046    private static final String FULL = "full";
047
048    /**
049     * Do not format the exception.
050     */
051    private static final String NONE = "none";
052
053    /**
054     * Format only the first line of the throwable.
055     */
056    private static final String SHORT = "short";
057
058    /**
059     * ANSI renderer
060     */
061    private final TextRenderer textRenderer;
062
063    /**
064     * The number of lines to write.
065     */
066    private final int lines;
067
068    /**
069     * The stack trace separator.
070     */
071    private final String separator;
072
073    private final String suffix;
074
075    /**
076     * The list of packages to filter.
077     */
078    private final List<String> ignorePackages;
079
080    public static final String CLASS_NAME = "short.className";
081    public static final String METHOD_NAME = "short.methodName";
082    public static final String LINE_NUMBER = "short.lineNumber";
083    public static final String FILE_NAME = "short.fileName";
084    public static final String MESSAGE = "short.message";
085    public static final String LOCALIZED_MESSAGE = "short.localizedMessage";
086
087    /**
088     * Constructs the options for printing stack trace.
089     *
090     * @param lines
091     *            The number of lines.
092     * @param separator
093     *            The stack trace separator.
094     * @param ignorePackages
095     *            The packages to filter.
096     * @param textRenderer
097     *            The ANSI renderer
098     * @param suffix
099     */
100    protected ThrowableFormatOptions(final int lines, final String separator, final List<String> ignorePackages,
101            final TextRenderer textRenderer, final String suffix) {
102        this.lines = lines;
103        this.separator = separator == null ? Strings.LINE_SEPARATOR : separator;
104        this.ignorePackages = ignorePackages;
105        this.textRenderer = textRenderer == null ? PlainTextRenderer.getInstance() : textRenderer;
106        this.suffix = suffix;
107    }
108
109    /**
110     * Constructs the options for printing stack trace.
111     *
112     * @param packages
113     *            The packages to filter.
114     */
115    protected ThrowableFormatOptions(final List<String> packages) {
116        this(DEFAULT_LINES, null, packages, null, null);
117    }
118
119    /**
120     * Constructs the options for printing stack trace.
121     */
122    protected ThrowableFormatOptions() {
123        this(DEFAULT_LINES, null, null, null, null);
124    }
125
126    /**
127     * Returns the number of lines to write.
128     *
129     * @return The number of lines to write.
130     */
131    public int getLines() {
132        return this.lines;
133    }
134
135    /**
136     * Returns the stack trace separator.
137     *
138     * @return The stack trace separator.
139     */
140    public String getSeparator() {
141        return this.separator;
142    }
143
144    /**
145     * Returns the message rendered.
146     *
147     * @return the message rendered.
148     */
149    public TextRenderer getTextRenderer() {
150        return textRenderer;
151    }
152
153    /**
154     * Returns the list of packages to ignore (filter out).
155     *
156     * @return The list of packages to ignore (filter out).
157     */
158    public List<String> getIgnorePackages() {
159        return this.ignorePackages;
160    }
161
162    /**
163     * Determines if all lines should be printed.
164     *
165     * @return true for all lines, false otherwise.
166     */
167    public boolean allLines() {
168        return this.lines == DEFAULT_LINES;
169    }
170
171    /**
172     * Determines if any lines should be printed.
173     *
174     * @return true for any lines, false otherwise.
175     */
176    public boolean anyLines() {
177        return this.lines > 0;
178    }
179
180    /**
181     * Returns the minimum between the lines and the max lines.
182     *
183     * @param maxLines
184     *            The maximum number of lines.
185     * @return The number of lines to print.
186     */
187    public int minLines(final int maxLines) {
188        return this.lines > maxLines ? maxLines : this.lines;
189    }
190
191    /**
192     * Determines if there are any packages to filter.
193     *
194     * @return true if there are packages, false otherwise.
195     */
196    public boolean hasPackages() {
197        return this.ignorePackages != null && !this.ignorePackages.isEmpty();
198    }
199
200    /**
201     * {@inheritDoc}
202     */
203    @Override
204    public String toString() {
205        final StringBuilder s = new StringBuilder();
206        s.append('{')
207                .append(allLines() ? FULL : this.lines == 2 ? SHORT : anyLines() ? String.valueOf(this.lines) : NONE)
208                .append('}');
209        s.append("{separator(").append(this.separator).append(")}");
210        if (hasPackages()) {
211            s.append("{filters(");
212            for (final String p : this.ignorePackages) {
213                s.append(p).append(',');
214            }
215            s.deleteCharAt(s.length() - 1);
216            s.append(")}");
217        }
218        return s.toString();
219    }
220
221    /**
222     * Creates a new instance based on the array of options.
223     *
224     * @param options
225     *            The array of options.
226     * @return A new initialized instance.
227     */
228    public static ThrowableFormatOptions newInstance(String[] options) {
229        if (options == null || options.length == 0) {
230            return DEFAULT;
231        }
232        // NOTE: The following code is present for backward compatibility
233        // and was copied from Extended/RootThrowablePatternConverter.
234        // This supports a single option with the format:
235        // %xEx{["none"|"short"|"full"|depth],[filters(packages)}
236        // However, the convention for multiple options should be:
237        // %xEx{["none"|"short"|"full"|depth]}[{filters(packages)}]
238        if (options.length == 1 && Strings.isNotEmpty(options[0])) {
239            final String[] opts = options[0].split(Patterns.COMMA_SEPARATOR, 2);
240            final String first = opts[0].trim();
241            try (final Scanner scanner = new Scanner(first)) {
242                if (opts.length > 1 && (first.equalsIgnoreCase(FULL) || first.equalsIgnoreCase(SHORT)
243                        || first.equalsIgnoreCase(NONE) || scanner.hasNextInt())) {
244                    options = new String[] { first, opts[1].trim() };
245                }
246            }
247        }
248
249        int lines = DEFAULT.lines;
250        String separator = DEFAULT.separator;
251        List<String> packages = DEFAULT.ignorePackages;
252        TextRenderer ansiRenderer = DEFAULT.textRenderer;
253        String suffix = DEFAULT.getSuffix();
254        for (final String rawOption : options) {
255            if (rawOption != null) {
256                final String option = rawOption.trim();
257                if (option.isEmpty()) {
258                    // continue;
259                } else if (option.startsWith("separator(") && option.endsWith(")")) {
260                    separator = option.substring("separator(".length(), option.length() - 1);
261                } else if (option.startsWith("filters(") && option.endsWith(")")) {
262                    final String filterStr = option.substring("filters(".length(), option.length() - 1);
263                    if (filterStr.length() > 0) {
264                        final String[] array = filterStr.split(Patterns.COMMA_SEPARATOR);
265                        if (array.length > 0) {
266                            packages = new ArrayList<>(array.length);
267                            for (String token : array) {
268                                token = token.trim();
269                                if (token.length() > 0) {
270                                    packages.add(token);
271                                }
272                            }
273                        }
274                    }
275                } else if (option.equalsIgnoreCase(NONE)) {
276                    lines = 0;
277                } else if (option.equalsIgnoreCase(SHORT) || option.equalsIgnoreCase(CLASS_NAME)
278                        || option.equalsIgnoreCase(METHOD_NAME) || option.equalsIgnoreCase(LINE_NUMBER)
279                        || option.equalsIgnoreCase(FILE_NAME) || option.equalsIgnoreCase(MESSAGE)
280                        || option.equalsIgnoreCase(LOCALIZED_MESSAGE)) {
281                    lines = 2;
282                } else if (option.startsWith("ansi(") && option.endsWith(")") || option.equals("ansi")) {
283                    if (Loader.isJansiAvailable()) {
284                        final String styleMapStr = option.equals("ansi") ? Strings.EMPTY
285                                : option.substring("ansi(".length(), option.length() - 1);
286                        ansiRenderer = new JAnsiTextRenderer(new String[] { null, styleMapStr },
287                                JAnsiTextRenderer.DefaultExceptionStyleMap);
288                    } else {
289                        StatusLogger.getLogger().warn(
290                                "You requested ANSI exception rendering but JANSI is not on the classpath. Please see https://logging.apache.org/log4j/2.x/runtime-dependencies.html");
291                    }
292                } else if (option.startsWith("S(") && option.endsWith(")")){
293                    suffix = option.substring("S(".length(), option.length() - 1);
294                } else if (option.startsWith("suffix(") && option.endsWith(")")){
295                    suffix = option.substring("suffix(".length(), option.length() - 1);
296                } else if (!option.equalsIgnoreCase(FULL)) {
297                    lines = Integer.parseInt(option);
298                }
299            }
300        }
301        return new ThrowableFormatOptions(lines, separator, packages, ansiRenderer, suffix);
302    }
303
304    public String getSuffix() {
305        return suffix;
306    }
307
308}