package com.log4jviewer.logfile;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import com.log4jviewer.logfile.fields.AbstractField;
import com.log4jviewer.logfile.fields.DateField;
import com.log4jviewer.logfile.fields.LevelField;
import com.log4jviewer.logfile.fields.LogFieldName;
import com.log4jviewer.logfile.fields.NamedField;
import com.log4jviewer.logfile.fields.NumberedField;
import com.log4jviewer.logfile.fields.WildcardField;
/**
* Class provides pattern parsing for using it to read text logs.
*
* @author Apache
*/
public class PatternParser {
private static final int LITERAL_STATE = 0;
private static final int CONVERTER_STATE = 1;
private static final int DOT_STATE = 3;
private static final int MIN_STATE = 4;
private static final int MAX_STATE = 5;
private String pattern;
private int state;
private StringBuilder regexChars;
private int patternCharIndex;
private boolean leftAlign;
private boolean rightAlign;
private int minWidth = -1;
private int maxWidth = -1;
private StringBuilder recordRegex;
private List<AbstractField> logFields;
private Pattern recordPattern;
public PatternParser(final String pattern) {
this.pattern = preConvertPattern(pattern);
state = LITERAL_STATE;
recordRegex = new StringBuilder();
logFields = new ArrayList<AbstractField>();
parse();
}
public List<AbstractField> getLogFields() {
return logFields;
}
public Pattern getRecordPattern() {
if (recordPattern == null) {
if (recordRegex.toString().endsWith("\\n")) {
int end = recordRegex.toString().length();
int start = end - 2;
recordRegex.replace(start, end, "(?:\\n)");
}
recordPattern = Pattern.compile(recordRegex.toString(), Pattern.DOTALL);
}
return recordPattern;
}
private void parse() {
final String lineSep = System.getProperty("line.separator");
final char escapeChar = '%';
regexChars = new StringBuilder();
patternCharIndex = 0;
while (patternCharIndex < pattern.length()) {
char currentPatternChar = pattern.charAt(patternCharIndex++);
switch (state) {
case LITERAL_STATE:
// In literal state, the last char is always a literal.
if (patternCharIndex == pattern.length()) {
regexChars.append(currentPatternChar);
continue;
}
if (currentPatternChar == escapeChar) {
// peek at the next char.
switch (pattern.charAt(patternCharIndex)) {
case escapeChar:
regexChars.append(currentPatternChar);
patternCharIndex++; // move pointer
break;
case 'n':
regexChars.append(lineSep);
patternCharIndex++; // move pointer
break;
default:
if (regexChars.length() != 0) {
addCharsToRecordRegex(regexChars.toString());
}
regexChars.setLength(0);
regexChars.append(currentPatternChar); // append %
state = CONVERTER_STATE;
resetFormat();
}
} else {
regexChars.append(currentPatternChar);
}
break;
case CONVERTER_STATE:
regexChars.append(currentPatternChar);
switch (currentPatternChar) {
case '-':
leftAlign = true;
break;
case '.':
state = DOT_STATE;
break;
default:
if ((currentPatternChar >= '0') && (currentPatternChar <= '9')) {
minWidth = currentPatternChar - '0';
if (!leftAlign) {
rightAlign = true;
}
state = MIN_STATE;
} else {
finalizeDescriptor(currentPatternChar);
}
} // switch
break;
case MIN_STATE:
regexChars.append(currentPatternChar);
if ((currentPatternChar >= '0') && (currentPatternChar <= '9')) {
minWidth = (minWidth * 10) + (currentPatternChar - '0');
} else if (currentPatternChar == '.') {
state = DOT_STATE;
} else {
finalizeDescriptor(currentPatternChar);
}
break;
case DOT_STATE:
regexChars.append(currentPatternChar);
if ((currentPatternChar >= '0') && (currentPatternChar <= '9')) {
maxWidth = currentPatternChar - '0';
if (!leftAlign) {
rightAlign = true;
}
state = MAX_STATE;
} else {
state = LITERAL_STATE;
}
break;
case MAX_STATE:
regexChars.append(currentPatternChar);
if ((currentPatternChar >= '0') && (currentPatternChar <= '9')) {
maxWidth = (maxWidth * 10) + (currentPatternChar - '0');
} else {
finalizeDescriptor(currentPatternChar);
state = LITERAL_STATE;
}
break;
default:
// no code
}
}
if (regexChars.length() != 0) {
addCharsToRecordRegex(regexChars.toString());
}
}
private void addCharsToRecordRegex(final String chars) {
recordRegex.append(SpecialCharsConverter.convertSpecialChars(chars));
}
private void addFieldRegex(final AbstractField logField) {
logFields.add(logField);
recordRegex.append(logField.getRegex());
}
private void finalizeDescriptor(final char patternChar) {
final String isoDateFormat = "ISO8601";
final String absTimeDateFormat = "ABSOLUTE";
final String dateAndTimeDateFormat = "DATE";
final String relativeTimeDateFormat = "RELATIVE";
AbstractField logField = null;
switch (patternChar) {
case 'c':
logField = new NamedField(LogFieldName.CATEGORY, leftAlign, rightAlign, extractPrecisionOption());
break;
case 'C':
logField = new NamedField(LogFieldName.CLASS, leftAlign, rightAlign, extractPrecisionOption());
break;
case 'd':
String dateFormatString = "yyyy-MM-dd HH:mm:ss,SSS"; // 'ISO8601' date format
String dOpt = extractOption();
if ((dOpt != null) && !dOpt.equalsIgnoreCase(isoDateFormat)) {
if (dOpt.equalsIgnoreCase(absTimeDateFormat)) {
dateFormatString = "HH:mm:ss,SSS";
} else if (dOpt.equalsIgnoreCase(dateAndTimeDateFormat)) {
dateFormatString = "dd MMM yyyy HH:mm:ss,SSS";
} else if (dOpt.equalsIgnoreCase(relativeTimeDateFormat)) {
dateFormatString = "SSSS";
} else {
dateFormatString = dOpt;
}
}
logField = new DateField(LogFieldName.DATE, leftAlign, rightAlign, dateFormatString);
break;
case 'F':
logField = new NamedField(LogFieldName.FILE, leftAlign, rightAlign, 2);
break;
case 'L':
logField = new NumberedField(LogFieldName.LINE, leftAlign, rightAlign);
break;
case 'm':
logField = new WildcardField(LogFieldName.MESSAGE, leftAlign, rightAlign);
break;
case 'M':
logField = new NamedField(LogFieldName.METHOD, leftAlign, rightAlign, 1);
break;
case 'p':
logField = new LevelField(LogFieldName.LEVEL, leftAlign, rightAlign);
break;
case 'r':
logField = new NumberedField(LogFieldName.MILLISECONDS, leftAlign, rightAlign);
break;
case 't':
logField = new WildcardField(LogFieldName.THREAD, leftAlign, rightAlign);
break;
case 'x':
logField = new WildcardField(LogFieldName.NDC, leftAlign, rightAlign);
break;
case 'X':
logField = new WildcardField(LogFieldName.MDC, leftAlign, rightAlign);
break;
default:
addCharsToRecordRegex(regexChars.toString());
}
regexChars.setLength(0);
// Add the pattern converter to the list.
addFieldRegex(logField);
// Next pattern is assumed to be a literal.
state = LITERAL_STATE;
// Reset format
resetFormat();
}
private String preConvertPattern(final String pattern) {
return pattern.replaceAll("%l", "%C.%M(%F:%L)");
}
private void resetFormat() {
leftAlign = false;
rightAlign = false;
minWidth = -1;
maxWidth = -1;
}
// The option is expected to be in decimal and positive. In case of error, 0 is returned.
private int extractPrecisionOption() throws NumberFormatException {
String opt = extractOption();
int extractedPrecisionOption = 0;
if (opt != null) {
extractedPrecisionOption = Integer.parseInt(opt);
if (extractedPrecisionOption <= 0) {
extractedPrecisionOption = 0;
}
}
return extractedPrecisionOption;
}
private String extractOption() {
if ((patternCharIndex < pattern.length()) && (pattern.charAt(patternCharIndex) == '{')) {
int end = pattern.indexOf('}', patternCharIndex);
if (end > patternCharIndex) {
String extractedOption = pattern.substring(patternCharIndex + 1, end);
patternCharIndex = end + 1;
return extractedOption;
}
}
return null;
}
}