src.main.java.com.mgnt.utils.TextUtils Maven / Gradle / Ivy
package com.mgnt.utils;
import com.mgnt.utils.entities.TimeInterval;
import com.mgnt.utils.textutils.InvalidVersionFormatException;
import com.mgnt.utils.textutils.Version;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.concurrent.TimeUnit;
/**
* This class provides various utilities for work with String that represents some other type. In current version this class provides methods for
* converting a String into its numeric value of various types (Integer, Float, Byte, Double, Long, Short). There are several methods for retrieving
* Exception stacktrace as a String in full or shortened version. Shortened version of the stacktrace will contain concise information focusing on
* specific package or subpackage while removing long parts of irrelevant stacktrace. This could be very useful for logging in web-based architecture
* where stacktrace may contain long parts of server provided classes trace that could be eliminated with the methods of this class while retaining
* important parts of the stacktrace relating to user's packages. Also this class provides methods that work with textual representation of versions.
* Valid version is a String of the following format:
*
*
* X[.X[.X[...]]]
*
*
*
* where X is a zero or positive integer not larger than 2147483647. Leading or trailing white spaces in this string are permitted and are ignored.
* Examples of valid versions are: "1.6", "58", " 7.34.17 " etc. (Note that last example contains both leading and trailing white spaces and it is
* still a valid version)
*
* Another useful feature is parsing String to time interval. It parses Strings with numerical value and optional time unit
* suffix (for example string "38s" will be parsed as 38 seconds, "24m" - 24 minutes "4h" - 4 hours, "3d" - 3 days and
* "45" as 45 milliseconds.) This method may be very useful for parsing time interval properties such as timeouts or waiting
* periods from configuration files.
*
*
* Also in this class there is a method that converts String to preserve indentation formatting for html without use of escape
* characters. It converts a String in such a way that its spaces are not modified by HTML renderer i.e. it replaces
* regular space characters with non-breaking spaces known as ' ' but they look in your source as regular space
* ' ' and not as ' ' It also replaces new line character with '<br>'.
*
*
* Note that this class has a loose dependency on slf4J library. If in the project some other compatible logging library is present
* (such as Log4J) this class will still work without any ill effects
*
*
* @author Michael Gantman
*/
public class TextUtils {
private static final Logger logger = LoggerFactory.getLogger(TextUtils.class);
protected static final TimeUnit DEFAULT_TIMEOUT_TIME_UNIT = TimeUnit.MILLISECONDS;
protected static final String SECONDS_SUFFIX = "s";
protected static final String MINUTES_SUFFIX = "m";
protected static final String HOURS_SUFFIX = "h";
protected static final String DAYS_SUFFIX = "d";
private static final long INITIAL_PARSING_VALUE = -1L;
/*
* Strings defined bellow are for the use of methods getStacktrace() of this class
*/
private static String RELEVANT_PACKAGE = null;
private static final String STANDARD_STAKTRACE_PREFIX = "at ";
private static final String SKIPPING_LINES_STRING = "\t...";
private static final String CAUSE_STAKTRACE_PREFIX = "Caused by:";
private static final String SUPPRESED_STAKTRACE_PREFIX = "Suppressed:";
private static final String RELEVANT_PACKAGE_SYSTEM_EVIRONMENT_VARIABLE = "MGNT_RELEVANT_PACKAGE";
private static final String RELEVANT_PACKAGE_SYSTEM_PROPERTY = "mgnt.relevant.package";
private static final String HTML_NON_BREAKING_SPACE_CHARACTER = StringUnicodeEncoderDecoder.decodeUnicodeSequenceToString("\\u00A0");
private static final String HTML_NEW_LINE = "
";
static {
initRelevantPackageFromSystemProperty();
}
private static void initRelevantPackageFromSystemProperty() {
String relevantPackage = System.getProperty(RELEVANT_PACKAGE_SYSTEM_PROPERTY);
if(StringUtils.isBlank(relevantPackage)) {
relevantPackage = System.getenv(RELEVANT_PACKAGE_SYSTEM_EVIRONMENT_VARIABLE);
}
if(StringUtils.isNotBlank(relevantPackage)) {
setRelevantPackage(relevantPackage);
}
}
/**
* This method compares 2 Strings as versions
*
* @param ver1 String that contains version for comparison
* @param ver2 String that contains version for comparison
* @return negative integer (-1) if the first version is lesser then the second, 0 if the versions are equal and positive integer (1) if the first
* version is greater than second
* @throws com.mgnt.utils.textutils.InvalidVersionFormatException
* if any of the String instances is not a valid Version
*/
public static int compareVersions(String ver1, String ver2) throws InvalidVersionFormatException {
return new Version(ver1).compareTo(new Version(ver2));
}
/**
* This method compares a Version to a String that represents a version
*
* @param ver1 Version for comparison
* @param ver2 String that contains version for comparison
* @return negative integer (-1) if the first version is lesser then the second, 0 if the versions are equal and positive integer (1) if the first
* version is greater than second
* @throws com.mgnt.utils.textutils.InvalidVersionFormatException
* if the String parameter is not a valid Version
*/
public static int compareVersions(Version ver1, String ver2) throws InvalidVersionFormatException {
return ver1.compareTo(new Version(ver2));
}
/**
* This method compares a String that represents a version to a Version
*
* @param ver1 String that contains version for comparison
* @param ver2 Version for comparison
* @return negative integer (-1) if the first version is lesser then the second, 0 if the versions are equal and positive integer (1) if the first
* version is greater than second
* @throws com.mgnt.utils.textutils.InvalidVersionFormatException
* if the String parameter is not a valid Version
*/
public static int compareVersions(String ver1, Version ver2) throws InvalidVersionFormatException {
return new Version(ver1).compareTo(ver2);
}
/**
* This method compares two Version instances
*
* @param ver1 Version for comparison
* @param ver2 Version for comparison
* @return negative integer (-1) if the first version is lesser then the second, 0 if the versions are equal and positive integer (1) if the first
* version is greater than second
* @throws com.mgnt.utils.textutils.InvalidVersionFormatException
* if the String parameter is not a valid Version
*/
public static int compareVersions(Version ver1, Version ver2) {
return ver1.compareTo(ver2);
}
/**
* This method parses a String to its Numeric value. If parsing does not succeed because the String is not of appropriate number format the
* default numeric value is returned and appropriate error message is printed into log. Also if the String is blank or null the default value is
* returned and appropriate error message is printed into log. Note that logging relies on slf4j being present and appropriately configured by
* project that uses this utility. If {@code nullOrEmptyStringErrorMessage} or {@code numberFormatErrorMessage} parameters are null or empty
* Strings than the correlating error will not be printed.
*
* @param num CharSequence to be parsed
* @param defaultValue value that will be returned by this method if parsing of the String failed
* @param nullOrEmptyStringErrorMessage String that holds an error message that will printed into log if parameter {@code num} is null or blank
* @param numberFormatErrorMessage String that holds an error message that will printed into log if parameter {@code num} is not in appropriate format
* @return numeric value parsed from the String
*/
public static int parseStringToInt(CharSequence num, int defaultValue,
String nullOrEmptyStringErrorMessage, String numberFormatErrorMessage) {
Integer result = null;
if (num == null || "".equals(num.toString())) {
if (nullOrEmptyStringErrorMessage != null && !"".equals(nullOrEmptyStringErrorMessage)) {
logger.warn(nullOrEmptyStringErrorMessage);
}
result = defaultValue;
} else {
try {
result = Integer.parseInt(num.toString());
} catch (NumberFormatException nfe) {
if (numberFormatErrorMessage != null && !"".equals(numberFormatErrorMessage)) {
warn(numberFormatErrorMessage, nfe);
}
result = defaultValue;
}
}
return result;
}
/**
* This method parses a String to Numeric value. If parsing does not succeed because the String is not of appropriate number format the default
* numeric value is returned and appropriate error message is printed into log. Also if the String is blank or null the default value is returned
* and appropriate error message is printed into log. Note that logging relies on slf4j being present and appropriately configured by project that
* uses this utility. If {@code nullOrEmptyStringErrorMessage} or {@code numberFormatErrorMessage} parameters are null or empty Strings than the
* correlating error will not be printed.
*
* @param num CharSequence to be parsed
* @param defaultValue value that will be returned by this method if parsing of the String failed
* @param nullOrEmptyStringErrorMessage String that holds an error message that will printed into log if parameter {@code num} is null or blank
* @param numberFormatErrorMessage String that holds an error message that will printed into log if parameter {@code num} is not in appropriate format
* @return numeric value parsed from the String
*/
public static float parseStringToFloat(CharSequence num, float defaultValue,
String nullOrEmptyStringErrorMessage, String numberFormatErrorMessage) {
Float result = null;
if (num == null || "".equals(num.toString())) {
if (nullOrEmptyStringErrorMessage != null && !"".equals(nullOrEmptyStringErrorMessage)) {
logger.warn(nullOrEmptyStringErrorMessage);
}
result = defaultValue;
} else {
try {
result = Float.parseFloat(num.toString());
} catch (NumberFormatException nfe) {
if (numberFormatErrorMessage != null && !"".equals(numberFormatErrorMessage)) {
warn(numberFormatErrorMessage, nfe);
}
result = defaultValue;
}
}
return result;
}
/**
* This method parses a String to Numeric value. If parsing does not succeed because the String is not of appropriate number format the default
* numeric value is returned and appropriate error message is printed into log. Also if the String is blank or null the default value is returned
* and appropriate error message is printed into log. Note that logging relies on slf4j being present and appropriately configured by project that
* uses this utility. If {@code nullOrEmptyStringErrorMessage} or {@code numberFormatErrorMessage} parameters are null or empty Strings than the
* correlating error will not be printed.
*
* @param num CharSequence to be parsed
* @param defaultValue value that will be returned by this method if parsing of the String failed
* @param nullOrEmptyStringErrorMessage String that holds an error message that will printed into log if parameter {@code num} is null or blank
* @param numberFormatErrorMessage String that holds an error message that will printed into log if parameter {@code num} is not in appropriate format
* @return numeric value parsed from the String
*/
public static Byte parseStringToByte(CharSequence num, byte defaultValue,
String nullOrEmptyStringErrorMessage, String numberFormatErrorMessage) {
Byte result = null;
if (num == null || "".equals(num.toString())) {
if (nullOrEmptyStringErrorMessage != null && !"".equals(nullOrEmptyStringErrorMessage)) {
logger.warn(nullOrEmptyStringErrorMessage);
}
result = defaultValue;
} else {
try {
result = Byte.parseByte(num.toString());
} catch (NumberFormatException nfe) {
if (numberFormatErrorMessage != null && !"".equals(numberFormatErrorMessage)) {
warn(numberFormatErrorMessage, nfe);
}
result = defaultValue;
}
}
return result;
}
/**
* This method parses a String to Numeric value. If parsing does not succeed because the String is not of appropriate number format the default
* numeric value is returned and appropriate error message is printed into log. Also if the String is blank or null the default value is returned
* and appropriate error message is printed into log. Note that logging relies on slf4j being present and appropriately configured by project that
* uses this utility. If {@code nullOrEmptyStringErrorMessage} or {@code numberFormatErrorMessage} parameters are null or empty Strings than the
* correlating error will not be printed.
*
* @param num CharSequence to be parsed
* @param defaultValue value that will be returned by this method if parsing of the String failed
* @param nullOrEmptyStringErrorMessage String that holds an error message that will printed into log if parameter {@code num} is null or blank
* @param numberFormatErrorMessage String that holds an error message that will printed into log if parameter {@code num} is not in appropriate format
* @return numeric value parsed from the String
*/
public static double parseStringToDouble(CharSequence num, double defaultValue,
String nullOrEmptyStringErrorMessage, String numberFormatErrorMessage) {
Double result = null;
if (num == null || "".equals(num.toString())) {
if (nullOrEmptyStringErrorMessage != null && !"".equals(nullOrEmptyStringErrorMessage)) {
logger.warn(nullOrEmptyStringErrorMessage);
}
result = defaultValue;
} else {
try {
result = Double.parseDouble(num.toString());
} catch (NumberFormatException nfe) {
if (numberFormatErrorMessage != null && !"".equals(numberFormatErrorMessage)) {
warn(numberFormatErrorMessage, nfe);
}
result = defaultValue;
}
}
return result;
}
/**
* This method parses a String to Numeric value. If parsing does not succeed because the String is not of appropriate number format the default
* numeric value is returned and appropriate error message is printed into log. Also if the String is blank or null the default value is returned
* and appropriate error message is printed into log. Note that logging relies on slf4j being present and appropriately configured by project that
* uses this utility. If {@code nullOrEmptyStringErrorMessage} or {@code numberFormatErrorMessage} parameters are null or empty Strings than the
* correlating error will not be printed.
*
* @param num CharSequence to be parsed
* @param defaultValue value that will be returned by this method if parsing of the String failed
* @param nullOrEmptyStringErrorMessage String that holds an error message that will printed into log if parameter {@code num} is null or blank
* @param numberFormatErrorMessage String that holds an error message that will printed into log if parameter {@code num} is not in appropriate format
* @return numeric value parsed from the String
*/
public static long parseStringToLong(CharSequence num, long defaultValue,
String nullOrEmptyStringErrorMessage, String numberFormatErrorMessage) {
Long result = null;
if (num == null || "".equals(num.toString())) {
if (nullOrEmptyStringErrorMessage != null && !"".equals(nullOrEmptyStringErrorMessage)) {
logger.warn(nullOrEmptyStringErrorMessage);
}
result = defaultValue;
} else {
try {
result = Long.parseLong(num.toString());
} catch (NumberFormatException nfe) {
if (numberFormatErrorMessage != null && !"".equals(numberFormatErrorMessage)) {
warn(numberFormatErrorMessage, nfe);
}
result = defaultValue;
}
}
return result;
}
/**
* This method parses a String to Numeric value. If parsing does not succeed because the String is not of appropriate number format the default
* numeric value is returned and appropriate error message is printed into log. Also if the String is blank or null the default value is returned
* and appropriate error message is printed into log. Note that logging relies on slf4j being present and appropriately configured by project that
* uses this utility. If {@code nullOrEmptyStringErrorMessage} or {@code numberFormatErrorMessage} parameters are null or empty Strings than the
* correlating error will not be printed.
*
* @param num CharSequence to be parsed
* @param defaultValue value that will be returned by this method if parsing of the String failed
* @param nullOrEmptyStringErrorMessage String that holds an error message that will printed into log if parameter {@code num} is null or blank
* @param numberFormatErrorMessage String that holds an error message that will printed into log if parameter {@code num} is not in appropriate format
* @return numeric value parsed from the String
*/
public static Short parseStringToShort(CharSequence num, short defaultValue,
String nullOrEmptyStringErrorMessage, String numberFormatErrorMessage) {
Short result = null;
if (num == null || "".equals(num.toString())) {
if (nullOrEmptyStringErrorMessage != null && !"".equals(nullOrEmptyStringErrorMessage)) {
logger.warn(nullOrEmptyStringErrorMessage);
}
result = defaultValue;
} else {
try {
result = Short.parseShort(num.toString());
} catch (NumberFormatException nfe) {
if (numberFormatErrorMessage != null && !"".equals(numberFormatErrorMessage)) {
warn(numberFormatErrorMessage, nfe);
}
result = defaultValue;
}
}
return result;
}
/**
* This method parses String value into {@link TimeInterval}. This method supports time interval suffixes "s"
* for seconds, "m" for minutes, "h" for hours, and "d" for days. Suffix is case insensitive.
* If String parameter contains no suffix the default is milliseconds. So for example string "38s" will be parsed
* as 38 seconds, "24m" - 24 minutes "4h" - 4 hours, "3d" - 3 days and "45" as 45 milliseconds. If the string parses
* to a negative numerical value or 0 or the string is not a valid numerical value then {@link IllegalArgumentException}
* is thrown. Note that it is very convenient to extract time value from {@link TimeInterval}, See methods
* {@link TimeInterval#toMillis()}, {@link TimeInterval#toSeconds()}, {@link TimeInterval#toMinutes()},
* {@link TimeInterval#toHours()}, {@link TimeInterval#toDays()}.
*
* This method may be very useful for parsing time interval properties such as timeouts or waiting periods from
* configuration files. It eliminates unneeded calculations from different time scales to milliseconds back and forth.
* Consider that you have a {@code methodInvokingInterval} property that you need to set for 5 days. So in order to
* set the miliseconds value you will need to calculate that 5 days is 432000000 milliseconds (obviously not an
* impossible task but annoying and error prone) and then anyone else who sees the value 432000000 will have to
* calculate it back to 5 days which is frustrating. But using this method you will have a property value set to
* "5d" and invoking the code
*
* {@code long milliseconds = TextUtils.parsingStringToTimeInterval("5d").toMillis();}
*
* will solve your conversion problem
*
* @param valueStr String value to parse to {@link TimeInterval}
* @return {@link TimeInterval} parsed from the String
* @throws IllegalArgumentException if parsed value has invalid suffix, invalid numeric value or negative numeric value or 0
*/
public static TimeInterval parsingStringToTimeInterval(String valueStr) throws IllegalArgumentException {
if(StringUtils.isBlank(valueStr)) {
throw new IllegalArgumentException("Attempt to parse null or blank String");
}
TimeInterval result = new TimeInterval();
String potentialSuffix = valueStr.substring(valueStr.length() - 1);
boolean isLetter = Character.isLetter(potentialSuffix.codePointAt(0));
String valueToParse = (isLetter) ? valueStr.substring(0, valueStr.length() - 1) : valueStr;
result.setValue(INITIAL_PARSING_VALUE);
result = setTimeUnit(isLetter, potentialSuffix, result);
result = setTimeValue(valueToParse, result);
return result;
}
/**
* This method retrieves a stacktrace from {@link Throwable} as a String in full or shortened format. Shortened format skips the lines in the
* stacktrace that do not start with a configurable package prefix and replaces them with "..." line. The stacktrace is viewed as consisting
* possibly of several parts. If stacktrace contains {@code "caused by"} or {@code "Suppressed"} section, each such section for the purposes of
* this utility is called "Singular stacktrace". For example the stacktrace bellow contains 2 singular stacktraces: First is 4 top lines and the
* second starting from the line {@code "Caused by: ..."} and to the end.
*
*
* java.lang.Exception: Bad error
* at com.plain.analytics.v2.utils.test.UtilsTester.TestGetStackTrace(UtilsTester.java:80)
* at com.plain.analytics.v2.utils.test.UtilsTester.run(UtilsTester.java:30)
* at com.plain.analytics.v2.utils.test.UtilsTester.main(UtilsTester.java:25)
* Caused by: java.lang.NumberFormatException: For input string: "Hello"
* at java.lang.NumberFormatException.forInputString(Unknown Source)
* at java.lang.Integer.parseInt(Unknown Source)
* at java.lang.Integer.parseInt(Unknown Source)
* at com.plain.analytics.v2.utils.test2.UtilsTesterHelper.parseInt(UtilsTesterHelper.java:8)
* at com.plain.analytics.v2.utils.test.UtilsTester$Invoker.runParser(UtilsTester.java:97)
* at com.plain.analytics.v2.utils.test2.UtilsTesterHelper.innerInvokeParser(UtilsTesterHelper.java:17)
* at com.plain.analytics.v2.utils.test2.UtilsTesterHelper.invokeParser(UtilsTesterHelper.java:12)
* at com.plain.analytics.v2.utils.test.UtilsTester.TestGetStackTrace(UtilsTester.java:76)
* ... 2 more
*
*
* The way this method shortens the stacktrace is as follows. Each "singular" stacktraces are analyzed and shortened separately. For each singular
* stacktrace the error message is always printed. Then all the lines that follow are printed even if they do not start with prefix specified by
* relevantPackage. Once the first line with the prefix is found this line and all immediately following lines that start with the relevant
* package prefix are printed as well. The first line that does not start with the prefix after a section of the lines that did is also printed.
* But all the following lines that do not start with the prefix are skipped and replaced with a single line "...". If at some point within the
* stacktrace a line that starts with the prefix is encountered again this line and all the following line that start with the prefix + one
* following line that does not start with the prefix are printed in. And so on. Here is an example: Assume that exception above was passed as a
* parameter to this method and parameter relevantPackage is set to {@code "com.plain.analytics.v2.utils.test."} which means that the lines starting with
* that prefix are the important or "relevant" lines. (Also the parameter cutTBS set to true which means that stacktrace should be
* shortened at all. In this case the result of this method should be as follows:
*
*
* java.lang.Exception: Bad error
* at com.plain.analytics.v2.utils.test.UtilsTester.TestGetStackTrace(UtilsTester.java:80)
* at com.plain.analytics.v2.utils.test.UtilsTester.run(UtilsTester.java:30)
* at com.plain.analytics.v2.utils.test.UtilsTester.main(UtilsTester.java:25)
* Caused by: java.lang.NumberFormatException: For input string: "Hello"
* at java.lang.NumberFormatException.forInputString(Unknown Source)
* at java.lang.Integer.parseInt(Unknown Source)
* at java.lang.Integer.parseInt(Unknown Source)
* at com.plain.analytics.v2.utils.test2.UtilsTesterHelper.parseInt(UtilsTesterHelper.java:8)
* at com.plain.analytics.v2.utils.test.UtilsTester$Invoker.runParser(UtilsTester.java:97)
* at com.plain.analytics.v2.utils.test2.UtilsTesterHelper.innerInvokeParser(UtilsTesterHelper.java:17)
* ...
* at com.plain.analytics.v2.utils.test.UtilsTester.TestGetStackTrace(UtilsTester.java:76)
* ... 2 more
*
*
* Note that the first singular stacktrace is printed in full because all the lines start with the required prefix. The second singular stacktrace
* prints the first 7 lines because at first all the lines are printed until the first line with relevant prefix is found, and then all the lines
* with the prefix (one in our case) are printed + plus one following line without the prefix. And then the second line without the prefix (3d
* from the bottom) is skipped and replaced with line "...". But then again we encounter a line with the prefix which is printed and finally the
* last line is printed because it is the first line without prefix following the one with the prefix. In this particular example only one line
* was skipped over which is not very much, but for web-based environments for the long stacktraces that contain long traces of server related
* classes this method could be very effective in removing irrelevant lines and leaving only application related lines making log files more
* concise and clear.
*
*
* Important Note: Parameter relevantPackage may be left null. In this case the value of relevant package prefix will be taken from
* RelevantPackage property (See the methods {@link #setRelevantPackage(String)} and {@link #getRelevantPackage()}). Using method
* {@link #setRelevantPackage(String)} to set the value will preset the value of relevant package prefix for all calls for which parameter
* relevantPackage is null. In fact there is a convinience method {@link #getStacktrace(Throwable, boolean)} that invokes this method with
* parameter relevantPackage set to null and relies on the globally set property through method {@link #setRelevantPackage(String)}.
* However if the global property was not set and parameter relevantPackage was left null then the method will return stacktrace in full as
* if the parameter cutTBS was set to false
*
* @param e {@link Throwable} from which stacktrace should be retrieved
* @param cutTBS boolean that specifies if stacktrace should be shortened. The stacktrace should be shortened if this flag is set to {@code true}.
* Note that if this parameter set to {@code false} the stacktrace will be printed in full and parameter relevantPackage becomes
* irrelevant.
* @param relevantPackage {@link String} that contains the prefix specifying which lines are relevant. It is recommended to be in the following format
* "packag_name1.[package_name2.[...]]." In the example above it should be "com.plain.analytics.v2.utils.test.".
* @return String with stacktrace value
*/
public static String getStacktrace(Throwable e, boolean cutTBS, String relevantPackage) {
StringBuilder result = new StringBuilder("\n");
// retrieve full stacktrace as byte array
ByteArrayOutputStream stacktraceContent = new ByteArrayOutputStream();
e.printStackTrace(new PrintStream(stacktraceContent));
// Determine the value of relevant package prefix
String relPack = (relevantPackage != null && !relevantPackage.isEmpty()) ? relevantPackage : RELEVANT_PACKAGE;
/*
* If the relevant package prefix was not set neither locally nor globally revert to retrieving full stacktrace even if shortening was
* requested
*/
if (relPack == null || "".equals(relPack)) {
if (cutTBS) {
cutTBS = false;
logger.warn("Relevant package was not set for the method. Stacktrace can not be shortened. Returning full stacktrace");
}
}
if (cutTBS) {
if (stacktraceContent.size() > 0) {
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(new ByteArrayInputStream(stacktraceContent.toByteArray())))) {
/*
* First line from stacktrace is an actual error message and is always printed. If it happens to be null (which should never
* occur) it won't cause any problems as appending null to StringBuilder (within method traverseSingularStacktrace()) will be a
* no-op and subsequent readLine will still return null and will be detected in the method traverseSingularStacktrace()
*/
String line = reader.readLine();
do {
/*
* Process singular stacktraces until all are processed.
*/
line = traverseSingularStacktrace(result, relPack, reader, line);
} while (line != null);
} catch (IOException ioe) {
/*
* In the very unlikely event of any error just fall back on printing the full stacktrace
*/
error("Error occurred while reading and shortening stacktrace of an exception. Printing the original stacktrace", ioe);
result.delete(0, result.length()).append(new String(stacktraceContent.toByteArray()));
}
}
} else {
/*
* This is the branch that prints full stacktrace
*/
result.append(new String(stacktraceContent.toByteArray()));
}
return result.toString();
}
/**
* This method retrieves a stacktrace from {@link Throwable} as a String in full or shortened format. This is convenience method that invokes
* method {@link #getStacktrace(Throwable, boolean, String)} with last parameter as {@code null}. It relies on relevant package prefix to have
* been set by method {@link #setRelevantPackage(String)}. There are several ways to pre-invoke method {@link #setRelevantPackage(String)}:
*
* - Set system environment variable "MGNT_RELEVANT_PACKAGE" with relevant package value (for the purposes of our example
* it would be "com.plain.")
* - Run your code with System property "mgnt.relevant.package" set to relevant package value It could be done with
* -D: "-Dmgnt.relevant.package=com.plain." Note that System property value would take precedence over environment variable
* if both are set
* - In case when Spring framework is used and system property and environment variable described above are not used then it is
* recommended to add the following bean into your Spring configuration xml file. This will ensure an invocation of method
* {@link #setRelevantPackage(String)} which will appropriately initialize the package prefix and enable the use of this method
*
*
* <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
* <property name="targetClass" value="com.mgnt.utils.TextUtils"/>
* <property name="targetMethod" value="setRelevantPackage"/>
* <property name="arguments" value="com.plain."/>
* </bean>
*
*
*
* @param e {@link Throwable} from which stacktrace should be retrieved
* @param cutTBS boolean flag that specifies if stacktrace should be shortened or not. It is shortened if the flag value is {@code true}
* @return String that contains the stacktrace
* @see #getStacktrace(Throwable, boolean, String)
*/
public static String getStacktrace(Throwable e, boolean cutTBS) {
return getStacktrace(e, cutTBS, null);
}
/**
* This method retrieves a stacktrace from {@link Throwable} as a String in shortened format. This is convenience method that invokes method
* {@link #getStacktrace(Throwable, boolean, String)} with second parameter set to {@code 'true'} and last parameter as {@code null}. It relies on
* relevant package prefix to have been set by method {@link #setRelevantPackage(String)}. There are several ways to pre-invoke method {@link #setRelevantPackage(String)}:
*
* - Set system environment variable "MGNT_RELEVANT_PACKAGE" with relevant package value (for the purposes of our example
* it would be "com.plain.")
* - Run your code with System property "mgnt.relevant.package" set to relevant package value It could be done with
* -D: "-Dmgnt.relevant.package=com.plain." Note that System property value would take precedence over environment variable
* if both are set
* - In case when Spring framework is used and system property and environment variable described above are not used then it is
* recommended to add the following bean into your Spring configuration xml file. This will ensure an invocation of method
* {@link #setRelevantPackage(String)} which will appropriately initialize the package prefix and enable the use of this method
*
*
* <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
* <property name="targetClass" value="com.mgnt.utils.TextUtils"/>
* <property name="targetMethod" value="setRelevantPackage"/>
* <property name="arguments" value="com.plain."/>
* </bean>
*
*
* @param e {@link Throwable} from which stacktrace should be retrieved
* @return String that contains the stacktrace
* @see #getStacktrace(Throwable, boolean, String)
*/
public static String getStacktrace(Throwable e) {
return getStacktrace(e, true, null);
}
/**
* This method retrieves a stacktrace from {@link Throwable} as a String in shortened format. This is convenience method that invokes method
* {@link #getStacktrace(Throwable, boolean, String)} with second parameter set to {@code 'true'}.
*
* @param e {@link Throwable} from which stacktrace should be retrieved
* @param relevantPackage {@link String} that contains the prefix specifying which lines are relevant. It is recommended to be in the following format
* "packag_name1.[package_name2.[...]]."
* @return String that contains the stacktrace
* @see #getStacktrace(Throwable, boolean, String)
*/
public static String getStacktrace(Throwable e, String relevantPackage) {
return getStacktrace(e, true, relevantPackage);
}
/**
* This method traverses through Singular stacktrace and skips irrelevant lines from it replacing them by line "..." The resulting shortened
* stacktrace is appended into {@link StringBuilder} The stacktrace is viewed as consisting possibly of several parts. If stacktrace contains
* {@code "caused by"} or {@code "Suppressed"} section, each such section for the purposes of this utility is called "Singular stacktrace". For
* more detailed explanation see method {@link #getStacktrace(Throwable, boolean, String)}
*
* @param result {@link StringBuilder} to which the resultant stacktrace will be appended
* @param relPack {@link String} that contains relevant package prefix
* @param reader {@link BufferedReader} that contains the source from where the stacktrace may be read line by line. Current position in the reader
* is assumed to be at the beginning of the second line of the current singular stacktrace, following the line with the name of the
* exception and error message
* @param line {@link String} that contains the first line of the current singular stacktrace i.e. the line with the name of the exception and
* error message
* @return The first string of the next singular stacktrace or null if current singular stacktrace is the last one in the stacktrace
* @throws IOException if any error occurs.
* @see #getStacktrace(Throwable, boolean, String)
*/
private static String traverseSingularStacktrace(StringBuilder result, String relPack, BufferedReader reader, String line)
throws IOException {
result.append(line).append("\n");
// Flag that holds the status for the current line if it should be printed or not
boolean toBePrinted = true;
// Flag that holds information on previous line whether or not it starts with relevant prefix package
boolean relevantPackageReached = false;
// Flag that specifies if the current line starts with relevant prefix or not
boolean isCurLineRelevantPack = false;
// Flag that specifies that skipping line should be printed as next line
boolean skipLineToBePrinted = false;
// Main cycle reading lines until reaching the end of stacktrace
while ((line = reader.readLine()) != null) {
String trimmedLine = line.trim();
if (trimmedLine.startsWith(STANDARD_STAKTRACE_PREFIX)) {
/*
* This "if" branch deals with lines that are standard satacktrace lines atarting with "at "
*/
//Check if the current line starts with thge prefix (after the "at " part)
isCurLineRelevantPack = trimmedLine.substring(STANDARD_STAKTRACE_PREFIX.length()).startsWith(relPack);
if (!relevantPackageReached && isCurLineRelevantPack) {
/*
* If the current line starts with the prefix but previous line did not we change the printing status. This case deals with the
* first found line with the prefix and in this case it actually "changes" the flag "toBePrinted" from "true" to "true", but also
* it deals with the line with the prefix found after the first section of lines with the prefix was treated and was followed by
* some lines without prefix and then again the line with the prefix was found. That is why this "if" branch has to be before the
* actual printing. In this case flag "toBePrinted" is changed from "false" to "true". Also if previous line was the first line
* without prefix and we were supposed to print a skip line here we cancel that by setting flag "skipLineToBePrinted" back to
* false
*/
relevantPackageReached = true;
toBePrinted = true;
skipLineToBePrinted = false;
}
// Add (or "print" the line into result if it is considered to be relevant
if (toBePrinted) {
result.append(line).append("\n");
} else if (skipLineToBePrinted) {
result.append(SKIPPING_LINES_STRING).append("\n");
skipLineToBePrinted = false;
}
/*
* Check if the previous line was with the prefix but current one is not. If this is the case, change the value of the "toBePrinted"
* flag to false switch the value of the flag skipLineToBePrinted to true to indicate that next line that should be printed into the
* result is the skip line ("...")to indicate that some lines are skipped. Note that the current line already was added to the result
* which is by design as one "irrelevant" line is printed after a section of "relevant" lines. See documentation for method
* getStacktrace(Throwable e, boolean cutTBS, String relevantPackage) for details. Also note that here we don't actually print the
* skip line but just set the flag "skipLineToBePrinted" because we don't know if the next line in the stacktrace may be actually a
* relevant line and then the skip line won't be needed to be printed.
*/
if (relevantPackageReached && !isCurLineRelevantPack) {
relevantPackageReached = false;
toBePrinted = false;
skipLineToBePrinted = true;
}
} else {
/*
* This "else" branch deals with lines in the stacktrace that either start next singular stacktrace or are the last line in current
* singular stacktrace and it is of the form "... X more" where X is a number.
*/
if (trimmedLine.startsWith(CAUSE_STAKTRACE_PREFIX) || trimmedLine.startsWith(SUPPRESED_STAKTRACE_PREFIX)) {
/*
* If this is the first line of next singular stacktrace we break out of the current cycle and return this line for the next
* iteration which will invoke this method again
*/
break;
/*
* If it is last line in current singular stacktrace that starts with "..." then we print it if needed based on the value of the
* flag "toBePrinted" or in the case if we needed to add our line "..." instead we just print the original line as it has also the
* number of skipped lines
*/
} else if (toBePrinted || skipLineToBePrinted) {
result.append(line).append("\n");
//Just in case
skipLineToBePrinted = false;
}
}
}
return line;
}
/**
* This a getter method for global value of relevant package prefix property
*
* @return String that holds the prefix value.
*/
public static String getRelevantPackage() {
return RELEVANT_PACKAGE;
}
/**
* This is a setter method for relevant package prefix property for method {@link #getStacktrace(Throwable, boolean)} Once the value has been set
* the convenience method {@link #getStacktrace(Throwable, boolean)} could be used instead of method
* {@link #getStacktrace(Throwable, boolean, String)}
*
* @param relevantPackage {@link String} that contains the prefix specifying which lines are relevant. It is recommended to be in the following format
* "packag_name1.[package_name2.[...]]."
* @see #getStacktrace(Throwable, boolean, String)
*/
public static void setRelevantPackage(String relevantPackage) {
RELEVANT_PACKAGE = relevantPackage;
}
/**
* This method converts a String in such a way that its spaces are not modified by HTML renderer i.e. it replaces
* regular space characters with non-breaking spaces known as ' ' but they look in your source as regular
* space ' ' and not as ' ' It also replaces new line character with '<br>'.
* Here is an example. Lets say that you would like to write a text that has indentations So you can not simply write
* something like
*
* This is non-indented line
* This is 2 spaces indented line
* This is 4 spaces indented line
*
* Such a text after rendered by html would result into single non-indented line:
*
* This is non-indented line This is 2 spaces indented line This is 4 spaces indented line
*
*
* The solution would be to write your text as follows:
*
*
* This is non-indented line<br>
* This is 2 spaces indented line<br>
* This is 4 spaces indented line
*
* That works just fine once rendered in HTML but your source now is not very readable and difficult to maintain
* if you want to modify your indentations. So in order to remedy this you can pass your original string to this
* method, say the string looks like this:
*
* This is non-indented line
* This is 2 spaces indented line
* This is 4 spaces indented line
*
* And it will return you the string that looks like this:
*
* This is non-indented line<br>
* This is 2 spaces indented line<br>
* This is 4 spaces indented line<br>
*
* Except, that besides visible addition of <br> at the end of the lines your regular spaces (U+0020) have been
* replaced with non-breaking spaces (U+00A0) but they look the same in your source. So if you just place this
* modified string into your HTML source code your indentation will be preserved and you source code is readable.
*
IMPORTANT NOTE: if you want to modify indentations later you can NOT just type additional spaces.
* You will have to either use this method again after you modified your string or you can copy-pace those "normal
* looking" but actually non-breaking spaces. Also this was tested and found to be working with just regular HTML, but
* in combination with javascript it could sometimes produce unexpected results and instead of "normal-looking" space
* may show an 'Â' symbol. So, this method has its limitations. Test your results before you deliver. Use this method
* at your own risk. ☺
* @param rawText to be converted
* @return String that is converted as described above
*/
public static String formatStringToPreserveIndentationForHtml(String rawText) {
String result = rawText;
if(StringUtils.isNotEmpty(rawText)) {
result = rawText.replaceAll(" ", HTML_NON_BREAKING_SPACE_CHARACTER)
.replaceAll("\n", HTML_NEW_LINE + "\n");
}
return result;
}
private static void warn(String message, Throwable t) {
if (RELEVANT_PACKAGE != null && !RELEVANT_PACKAGE.isEmpty()) {
logger.warn(message + getStacktrace(t));
} else {
logger.warn(message, t);
}
}
private static void error(String message, Throwable t) {
if (RELEVANT_PACKAGE != null && !RELEVANT_PACKAGE.isEmpty()) {
logger.error(message + getStacktrace(t));
} else {
logger.error(message, t);
}
}
private static void debug(String message, Throwable t) {
if (RELEVANT_PACKAGE != null && !RELEVANT_PACKAGE.isEmpty()) {
logger.debug(message + getStacktrace(t));
} else {
logger.debug(message, t);
}
}
private static void fatal(String message, Throwable t) {
if (RELEVANT_PACKAGE != null && !RELEVANT_PACKAGE.isEmpty()) {
logger.error(message + getStacktrace(t));
} else {
logger.error(message, t);
}
}
private static void info(String message, Throwable t) {
if (RELEVANT_PACKAGE != null && !RELEVANT_PACKAGE.isEmpty()) {
logger.info(message + getStacktrace(t));
} else {
logger.info(message, t);
}
}
private static void trace(String message, Throwable t) {
if (RELEVANT_PACKAGE != null && !RELEVANT_PACKAGE.isEmpty()) {
logger.trace(message + getStacktrace(t));
} else {
logger.trace(message, t);
}
}
private static TimeInterval setTimeValue(String valueToParse, TimeInterval result) {
if (result.getValue() == INITIAL_PARSING_VALUE) {
result.setValue(Long.parseLong(valueToParse));
if (result.getValue() < 1) {
throw new IllegalArgumentException("Negative or zero value '" + result.getValue() + "' for time interval is illegal");
}
}
return result;
}
private static TimeInterval setTimeUnit(boolean isLetter, String potentialSuffix, TimeInterval result) {
if (isLetter) {
result.setTimeUnit(getTimeUnitBySuffix(potentialSuffix));
} else {
result.setTimeUnit(DEFAULT_TIMEOUT_TIME_UNIT);
}
return result;
}
private static TimeUnit getTimeUnitBySuffix(String suffix) {
TimeUnit result;
switch (suffix.toLowerCase()) {
case SECONDS_SUFFIX: {
result = TimeUnit.SECONDS;
break;
}
case MINUTES_SUFFIX: {
result = TimeUnit.MINUTES;
break;
}
case HOURS_SUFFIX: {
result = TimeUnit.HOURS;
break;
}
case DAYS_SUFFIX: {
result = TimeUnit.DAYS;
break;
}
default: {
throw new IllegalArgumentException("Time Unit Suffix " + suffix + " is invalid. Valid values are:\n'" +
SECONDS_SUFFIX + "' for seconds \n'" +
MINUTES_SUFFIX + "' for minutes \n'" +
HOURS_SUFFIX + "' for hours \n'" +
DAYS_SUFFIX + "' for days");
}
}
return result;
}
}