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}