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}