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.impl;
18  
19  import java.util.ArrayList;
20  import java.util.List;
21  import java.util.Scanner;
22  
23  import org.apache.logging.log4j.core.pattern.JAnsiTextRenderer;
24  import org.apache.logging.log4j.core.pattern.TextRenderer;
25  import org.apache.logging.log4j.core.pattern.PlainTextRenderer;
26  import org.apache.logging.log4j.core.util.Loader;
27  import org.apache.logging.log4j.core.util.Patterns;
28  import org.apache.logging.log4j.status.StatusLogger;
29  import org.apache.logging.log4j.util.Strings;
30  
31  /**
32   * Contains options which control how a {@link Throwable} pattern is formatted.
33   */
34  public final class ThrowableFormatOptions {
35  
36      private static final int DEFAULT_LINES = Integer.MAX_VALUE;
37  
38      /**
39       * Default instance of {@code ThrowableFormatOptions}.
40       */
41      protected static final ThrowableFormatOptions DEFAULT = new ThrowableFormatOptions();
42  
43      /**
44       * Format the whole stack trace.
45       */
46      private static final String FULL = "full";
47  
48      /**
49       * Do not format the exception.
50       */
51      private static final String NONE = "none";
52  
53      /**
54       * Format only the first line of the throwable.
55       */
56      private static final String SHORT = "short";
57  
58      /**
59       * ANSI renderer
60       */
61      private final TextRenderer textRenderer;
62  
63      /**
64       * The number of lines to write.
65       */
66      private final int lines;
67  
68      /**
69       * The stack trace separator.
70       */
71      private final String separator;
72  
73      /**
74       * The list of packages to filter.
75       */
76      private final List<String> ignorePackages;
77  
78      public static final String CLASS_NAME = "short.className";
79      public static final String METHOD_NAME = "short.methodName";
80      public static final String LINE_NUMBER = "short.lineNumber";
81      public static final String FILE_NAME = "short.fileName";
82      public static final String MESSAGE = "short.message";
83      public static final String LOCALIZED_MESSAGE = "short.localizedMessage";
84  
85      /**
86       * Constructs the options for printing stack trace.
87       * 
88       * @param lines
89       *            The number of lines.
90       * @param separator
91       *            The stack trace separator.
92       * @param ignorePackages
93       *            The packages to filter.
94       * @param textRenderer
95       *            The ANSI renderer
96       */
97      protected ThrowableFormatOptions(final int lines, final String separator, final List<String> ignorePackages,
98              final TextRenderer textRenderer) {
99          this.lines = lines;
100         this.separator = separator == null ? Strings.LINE_SEPARATOR : separator;
101         this.ignorePackages = ignorePackages;
102         this.textRenderer = textRenderer == null ? PlainTextRenderer.getInstance() : textRenderer;
103     }
104 
105     /**
106      * Constructs the options for printing stack trace.
107      * 
108      * @param packages
109      *            The packages to filter.
110      */
111     protected ThrowableFormatOptions(final List<String> packages) {
112         this(DEFAULT_LINES, null, packages, null);
113     }
114 
115     /**
116      * Constructs the options for printing stack trace.
117      */
118     protected ThrowableFormatOptions() {
119         this(DEFAULT_LINES, null, null, null);
120     }
121 
122     /**
123      * Returns the number of lines to write.
124      * 
125      * @return The number of lines to write.
126      */
127     public int getLines() {
128         return this.lines;
129     }
130 
131     /**
132      * Returns the stack trace separator.
133      * 
134      * @return The stack trace separator.
135      */
136     public String getSeparator() {
137         return this.separator;
138     }
139 
140     /**
141      * Returns the message rendered.
142      * 
143      * @return the message rendered.
144      */
145     public TextRenderer getTextRenderer() {
146         return textRenderer;
147     }
148 
149     /**
150      * Returns the list of packages to ignore (filter out).
151      * 
152      * @return The list of packages to ignore (filter out).
153      */
154     public List<String> getIgnorePackages() {
155         return this.ignorePackages;
156     }
157 
158     /**
159      * Determines if all lines should be printed.
160      * 
161      * @return true for all lines, false otherwise.
162      */
163     public boolean allLines() {
164         return this.lines == DEFAULT_LINES;
165     }
166 
167     /**
168      * Determines if any lines should be printed.
169      * 
170      * @return true for any lines, false otherwise.
171      */
172     public boolean anyLines() {
173         return this.lines > 0;
174     }
175 
176     /**
177      * Returns the minimum between the lines and the max lines.
178      * 
179      * @param maxLines
180      *            The maximum number of lines.
181      * @return The number of lines to print.
182      */
183     public int minLines(final int maxLines) {
184         return this.lines > maxLines ? maxLines : this.lines;
185     }
186 
187     /**
188      * Determines if there are any packages to filter.
189      * 
190      * @return true if there are packages, false otherwise.
191      */
192     public boolean hasPackages() {
193         return this.ignorePackages != null && !this.ignorePackages.isEmpty();
194     }
195 
196     /**
197      * {@inheritDoc}
198      */
199     @Override
200     public String toString() {
201         final StringBuilder s = new StringBuilder();
202         s.append('{')
203                 .append(allLines() ? FULL : this.lines == 2 ? SHORT : anyLines() ? String.valueOf(this.lines) : NONE)
204                 .append('}');
205         s.append("{separator(").append(this.separator).append(")}");
206         if (hasPackages()) {
207             s.append("{filters(");
208             for (final String p : this.ignorePackages) {
209                 s.append(p).append(',');
210             }
211             s.deleteCharAt(s.length() - 1);
212             s.append(")}");
213         }
214         return s.toString();
215     }
216 
217     /**
218      * Creates a new instance based on the array of options.
219      * 
220      * @param options
221      *            The array of options.
222      * @return A new initialized instance.
223      */
224     public static ThrowableFormatOptions newInstance(String[] options) {
225         if (options == null || options.length == 0) {
226             return DEFAULT;
227         }
228         // NOTE: The following code is present for backward compatibility
229         // and was copied from Extended/RootThrowablePatternConverter.
230         // This supports a single option with the format:
231         // %xEx{["none"|"short"|"full"|depth],[filters(packages)}
232         // However, the convention for multiple options should be:
233         // %xEx{["none"|"short"|"full"|depth]}[{filters(packages)}]
234         if (options.length == 1 && Strings.isNotEmpty(options[0])) {
235             final String[] opts = options[0].split(Patterns.COMMA_SEPARATOR, 2);
236             final String first = opts[0].trim();
237             try (final Scanner scanner = new Scanner(first)) {
238                 if (opts.length > 1 && (first.equalsIgnoreCase(FULL) || first.equalsIgnoreCase(SHORT)
239                         || first.equalsIgnoreCase(NONE) || scanner.hasNextInt())) {
240                     options = new String[] { first, opts[1].trim() };
241                 }
242             }
243         }
244 
245         int lines = DEFAULT.lines;
246         String separator = DEFAULT.separator;
247         List<String> packages = DEFAULT.ignorePackages;
248         TextRenderer ansiRenderer = DEFAULT.textRenderer;
249         for (final String rawOption : options) {
250             if (rawOption != null) {
251                 final String option = rawOption.trim();
252                 if (option.isEmpty()) {
253                     // continue;
254                 } else if (option.startsWith("separator(") && option.endsWith(")")) {
255                     separator = option.substring("separator(".length(), option.length() - 1);
256                 } else if (option.startsWith("filters(") && option.endsWith(")")) {
257                     final String filterStr = option.substring("filters(".length(), option.length() - 1);
258                     if (filterStr.length() > 0) {
259                         final String[] array = filterStr.split(Patterns.COMMA_SEPARATOR);
260                         if (array.length > 0) {
261                             packages = new ArrayList<>(array.length);
262                             for (String token : array) {
263                                 token = token.trim();
264                                 if (token.length() > 0) {
265                                     packages.add(token);
266                                 }
267                             }
268                         }
269                     }
270                 } else if (option.equalsIgnoreCase(NONE)) {
271                     lines = 0;
272                 } else if (option.equalsIgnoreCase(SHORT) || option.equalsIgnoreCase(CLASS_NAME)
273                         || option.equalsIgnoreCase(METHOD_NAME) || option.equalsIgnoreCase(LINE_NUMBER)
274                         || option.equalsIgnoreCase(FILE_NAME) || option.equalsIgnoreCase(MESSAGE)
275                         || option.equalsIgnoreCase(LOCALIZED_MESSAGE)) {
276                     lines = 2;
277                 } else if (option.startsWith("ansi(") && option.endsWith(")") || option.equals("ansi")) {
278                     if (Loader.isJansiAvailable()) {
279                         final String styleMapStr = option.equals("ansi") ? Strings.EMPTY
280                                 : option.substring("ansi(".length(), option.length() - 1);
281                         ansiRenderer = new JAnsiTextRenderer(new String[] { null, styleMapStr },
282                                 JAnsiTextRenderer.DefaultExceptionStyleMap);
283                     } else {
284                         StatusLogger.getLogger().warn(
285                                 "You requested ANSI exception rendering but JANSI is not on the classpath. Please see https://logging.apache.org/log4j/2.x/runtime-dependencies.html");
286                     }
287                 } else if (!option.equalsIgnoreCase(FULL)) {
288                     lines = Integer.parseInt(option);
289                 }
290             }
291         }
292         return new ThrowableFormatOptions(lines, separator, packages, ansiRenderer);
293     }
294 
295 }