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.util.Constants;
024import org.apache.logging.log4j.core.util.Patterns;
025import org.apache.logging.log4j.util.Strings;
026
027/**
028 * Contains options which control how a {@link Throwable} pattern is formatted.
029 */
030public final class ThrowableFormatOptions {
031
032    private static final int DEFAULT_LINES = Integer.MAX_VALUE;
033
034    /**
035     * Default instance of {@code ThrowableFormatOptions}.
036     */
037    protected static final ThrowableFormatOptions DEFAULT = new ThrowableFormatOptions();
038
039    /**
040     * Format the whole stack trace.
041     */
042    private static final String FULL = "full";
043
044    /**
045     * Do not format the exception.
046     */
047    private static final String NONE = "none";
048
049    /**
050     * Format only the first line of the throwable.
051     */
052    private static final String SHORT = "short";
053
054    /**
055     * The number of lines to write.
056     */
057    private final int lines;
058
059    /**
060     * The stack trace separator.
061     */
062    private final String separator;
063
064    /**
065     * The list of packages to filter.
066     */
067    private final List<String> packages;
068
069    public static final String CLASS_NAME = "short.className";
070    public static final String METHOD_NAME = "short.methodName";
071    public static final String LINE_NUMBER = "short.lineNumber";
072    public static final String FILE_NAME = "short.fileName";
073    public static final String MESSAGE = "short.message";
074    public static final String LOCALIZED_MESSAGE = "short.localizedMessage";
075
076    /**
077     * Construct the options for printing stack trace.
078     * @param lines The number of lines.
079     * @param separator The stack trace separator.
080     * @param packages The packages to filter.
081     */
082    protected ThrowableFormatOptions(final int lines, final String separator, final List<String> packages) {
083        this.lines = lines;
084        this.separator = separator == null ? Constants.LINE_SEPARATOR : separator;
085        this.packages = packages;
086    }
087
088    /**
089     * Construct the options for printing stack trace.
090     * @param packages The packages to filter.
091     */
092    protected ThrowableFormatOptions(final List<String> packages) {
093        this(DEFAULT_LINES, null, packages);
094    }
095
096    /**
097     * Construct the options for printing stack trace.
098     */
099    protected ThrowableFormatOptions() {
100        this(DEFAULT_LINES, null, null);
101    }
102
103    /**
104     * Returns the number of lines to write.
105     * @return The number of lines to write.
106     */
107    public int getLines() {
108        return this.lines;
109    }
110
111    /**
112     * Returns the stack trace separator.
113     * @return The stack trace separator.
114     */
115    public String getSeparator() {
116        return this.separator;
117    }
118
119    /**
120     * Returns the list of packages to filter.
121     * @return The list of packages to filter.
122     */
123    public List<String> getPackages() {
124        return this.packages;
125    }
126
127    /**
128     * Determines if all lines should be printed.
129     * @return true for all lines, false otherwise.
130     */
131    public boolean allLines() {
132        return this.lines == DEFAULT_LINES;
133    }
134
135    /**
136     * Determines if any lines should be printed.
137     * @return true for any lines, false otherwise.
138     */
139    public boolean anyLines() {
140        return this.lines > 0;
141    }
142
143    /**
144     * Returns the minimum between the lines and the max lines.
145     * @param maxLines The maximum number of lines.
146     * @return The number of lines to print.
147     */
148    public int minLines(final int maxLines) {
149        return this.lines > maxLines ? maxLines : this.lines;
150    }
151
152    /**
153     * Determines if there are any packages to filter.
154     * @return true if there are packages, false otherwise.
155     */
156    public boolean hasPackages() {
157        return this.packages != null && !this.packages.isEmpty();
158    }
159
160    /**
161     * {@inheritDoc}
162     */
163    @Override
164    public String toString() {
165        final StringBuilder s = new StringBuilder();
166        s.append('{').append(allLines() ? FULL : this.lines == 2 ? SHORT : anyLines() ? String.valueOf(this.lines) : NONE).append('}');
167        s.append("{separator(").append(this.separator).append(")}");
168        if (hasPackages()) {
169            s.append("{filters(");
170            for (final String p : this.packages) {
171                s.append(p).append(',');
172            }
173            s.deleteCharAt(s.length() - 1);
174            s.append(")}");
175        }
176        return s.toString();
177    }
178
179    /**
180     * Create a new instance based on the array of options.
181     * @param options The array of options.
182     */
183    public static ThrowableFormatOptions newInstance(String[] options) {
184        if (options == null || options.length == 0) {
185            return DEFAULT;
186        }
187        // NOTE: The following code is present for backward compatibility
188        // and was copied from Extended/RootThrowablePatternConverter.
189        // This supports a single option with the format:
190        //     %xEx{["none"|"short"|"full"|depth],[filters(packages)}
191        // However, the convention for multiple options should be:
192        //     %xEx{["none"|"short"|"full"|depth]}[{filters(packages)}]
193        if (options.length == 1 && Strings.isNotEmpty(options[0])) {
194            final String[] opts = options[0].split(Patterns.COMMA_SEPARATOR, 2);
195            final String first = opts[0].trim();
196            try (final Scanner scanner = new Scanner(first)) {
197                if (opts.length > 1
198                        && (first.equalsIgnoreCase(FULL) || first.equalsIgnoreCase(SHORT)
199                                || first.equalsIgnoreCase(NONE) || scanner.hasNextInt())) {
200                    options = new String[] {
201                            first,
202                            opts[1].trim() };
203                }
204            }
205        }
206
207        int lines = DEFAULT.lines;
208        String separator = DEFAULT.separator;
209        List<String> packages = DEFAULT.packages;
210        for (final String rawOption : options) {
211            if (rawOption != null) {
212                final String option = rawOption.trim();
213                if (option.isEmpty()) {
214                    // continue;
215                } else if (option.startsWith("separator(") && option.endsWith(")")) {
216                    separator = option.substring("separator(".length(), option.length() - 1);
217                } else if (option.startsWith("filters(") && option.endsWith(")")) {
218                    final String filterStr = option.substring("filters(".length(), option.length() - 1);
219                    if (filterStr.length() > 0) {
220                        final String[] array = filterStr.split(Patterns.COMMA_SEPARATOR);
221                        if (array.length > 0) {
222                            packages = new ArrayList<>(array.length);
223                            for (String token : array) {
224                                token = token.trim();
225                                if (token.length() > 0) {
226                                    packages.add(token);
227                                }
228                            }
229                        }
230                    }
231                } else if (option.equalsIgnoreCase(NONE)) {
232                    lines = 0;
233                } else if (option.equalsIgnoreCase(SHORT) || option.equalsIgnoreCase(CLASS_NAME) ||
234                        option.equalsIgnoreCase(METHOD_NAME) || option.equalsIgnoreCase(LINE_NUMBER) ||
235                        option.equalsIgnoreCase(FILE_NAME) || option.equalsIgnoreCase(MESSAGE) ||
236                        option.equalsIgnoreCase(LOCALIZED_MESSAGE)) {
237                    lines = 2;
238                } else if (!option.equalsIgnoreCase(FULL)) {
239                    lines = Integer.parseInt(option);
240                }
241            }
242        }
243        return new ThrowableFormatOptions(lines, separator, packages);
244    }
245}