pl.otros.logview.parser.log4j.Log4jPatternMultilineLogParser.java Source code

Java tutorial

Introduction

Here is the source code for pl.otros.logview.parser.log4j.Log4jPatternMultilineLogParser.java

Source

/*******************************************************************************
 * Copyright 2011 Krzysztof Otrebski
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package pl.otros.logview.parser.log4j;

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LocationInfo;
import org.apache.log4j.spi.LoggingEvent;
import org.apache.log4j.spi.ThrowableInformation;
import pl.otros.logview.LogData;
import pl.otros.logview.gui.table.TableColumns;
import pl.otros.logview.importer.InitializationException;
import pl.otros.logview.parser.MultiLineLogParser;
import pl.otros.logview.parser.ParserDescription;
import pl.otros.logview.parser.ParsingContext;
import pl.otros.logview.parser.TableColumnNameSelfDescribable;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
 * LogFilePatternReceiver can parse and tail log files, converting entries into LoggingEvents. If the file doesn't exist when the receiver is initialized, the
 * receiver will look for the file once every 10 seconds.
 * <p>
 * This receiver relies on java.util.regex features to perform the parsing of text in the log file, however the only regular expression field explicitly
 * supported is a glob-style wildcard used to ignore fields in the log file if needed. All other fields are parsed by using the supplied keywords.
 * <p>
 * <b>Features:</b><br>
 * - specify the URL of the log file to be processed<br>
 * - specify the timestamp format in the file (if one exists, using patterns from {@link java.text.SimpleDateFormat})<br>
 * - specify the pattern (logFormat) used in the log file using keywords, a wildcard character (*) and fixed text<br>
 * - 'tail' the file (allows the contents of the file to be continually read and new events processed)<br>
 * - specify custom charset (default UTF-8) - supports the parsing of multi-line messages and exceptions - 'hostname' property set to URL host (or 'file' if not
 * available) - 'application' property set to URL path (or value of fileURL if not available)
 * <p>
 * <b>Keywords:</b><br>
 * TIMESTAMP<br>
 * LOGGER<br>
 * LEVEL<br>
 * THREAD<br>
 * CLASS<br>
 * FILE<br>
 * LINE<br>
 * METHOD<br>
 * RELATIVETIME<br>
 * MESSAGE<br>
 * NDC<br>
 * PROP(key)<br>
 * <p>
 * Use a * to ignore portions of the log format that should be ignored
 * <p>
 * Example:<br>
 * If your file's patternlayout is this:<br>
 * <b>%d %-5p [%t] %C{2} (%F:%L) - %m%n</b>
 * <p>
 * specify this as the log format:<br>
 * <b>TIMESTAMP LEVEL [THREAD] CLASS (FILE:LINE) - MESSAGE</b>
 * <p>
 * To define a PROPERTY field, use PROP(key)
 * <p>
 * Example:<br>
 * If you used the RELATIVETIME pattern layout character in the file, you can use PROP(RELATIVETIME) in the logFormat definition to assign the RELATIVETIME
 * field as a property on the event.
 * <p>
 * If your file's patternlayout is this:<br>
 * <b>%r [%t] %-5p %c %x - %m%n</b>
 * <p>
 * specify this as the log format:<br>
 * <b>PROP(RELATIVETIME) [THREAD] LEVEL LOGGER * - MESSAGE</b>
 * <p>
 * Note the * - it can be used to ignore a single word or sequence of words in the log file (in order for the wildcard to ignore a sequence of words, the text
 * being ignored must be followed by some delimiter, like '-' or '[') - ndc is being ignored in the following example.
 * <p>
 * Assign a filterExpression in order to only process events which match a filter. If a filterExpression is not assigned, all events are processed.
 * <p>
 * <b>Limitations:</b><br>
 * - no support for the single-line version of throwable supported by patternlayout<br>
 * (this version of throwable will be included as the last line of the message)<br>
 * - the relativetime patternLayout character must be set as a property: PROP(RELATIVETIME)<br>
 * - messages should appear as the last field of the logFormat because the variability in message content<br>
 * - exceptions are converted if the exception stack trace (other than the first line of the exception)<br>
 * is stored in the log file with a tab followed by the word 'at' as the first characters in the line<br>
 * - tailing may fail if the file rolls over.
 * <p>
 * <b>Example receiver configuration settings</b> (add these as params, specifying a LogFilePatternReceiver 'plugin'):<br>
 * param: "timestampFormat" value="yyyy-MM-d HH:mm:ss,SSS"<br>
 * param: "logFormat" value="PROP(RELATIVETIME) [THREAD] LEVEL LOGGER * - MESSAGE"<br>
 * param: "fileURL" value="file:///c:/events.log"<br>
 * param: "tailing" value="true"
 * <p>
 * This configuration will be able to process these sample events:<br>
 * 710 [ Thread-0] DEBUG first.logger first - <test> <test2>something here</test2> <test3 blah=something/> <test4> <test5>something else</test5> </test4></test>
 * <br>
 * 880 [ Thread-2] DEBUG first.logger third - <test> <test2>something here</test2> <test3 blah=something/> <test4> <test5>something else</test5> </test4></test>
 * <br>
 * 880 [ Thread-0] INFO first.logger first - infomsg-0<br>
 * java.lang.Exception: someexception-first<br>
 * at Generator2.run(Generator2.java:102)<br>
 * 
 * @author Code highly based on
 *         http://svn.apache.org/repos/asf/logging/log4j/companions/receivers/trunk/src/main/java/org/apache/log4j/varia/LogFilePatternReceiver.java
 */
public class Log4jPatternMultilineLogParser implements MultiLineLogParser, TableColumnNameSelfDescribable {

    private static final java.util.logging.Logger LOG = java.util.logging.Logger
            .getLogger(Log4jPatternMultilineLogParser.class.getName());

    public static final String PROPERTY_NAME = "name";
    public static final String PROPERTY_PATTERN = "pattern";
    public static final String PROPERTY_REPATTERN = "rePattern";
    public static final String PROPERTY_DATE_FORMAT = "dateFormat";
    public static final String PROPERTY_CUSTOM_LEVELS = "customLevels";
    public static final String PROPERTY_DESCRIPTION = "description";
    public static final String PROPERTY_TYPE = "type";
    public static final String PROPERTY_CHARSET = "charset";

    private final List<String> keywords = new ArrayList<String>();
    // private SimpleDateFormat dateFormat;
    // private Rule expressionRule;
    private String[] emptyException = new String[] { "" };
    private Map<String, Level> customLevelDefinitionMap = new HashMap<String, Level>();
    private boolean appendNonMatches;
    private List<String> matchingKeywords = new ArrayList<String>();

    private static final String PROP_START = "PROP(";
    private static final String PROP_END = ")";

    protected static final String PROPERTY_LOG_EVENT_PROPERTIES = "Log4jPatternMultilineLogParser.logEventProperties";
    protected static final String LOGGER = "LOGGER";
    protected static final String MESSAGE = "MESSAGE";
    protected static final String TIMESTAMP = "TIMESTAMP";
    protected static final String NDC = "NDC";
    protected static final String LEVEL = "LEVEL";
    protected static final String THREAD = "THREAD";
    protected static final String CLASS = "CLASS";
    protected static final String FILE = "FILE";
    protected static final String LINE = "LINE";
    protected static final String METHOD = "METHOD";

    // all lines other than first line of exception begin with tab followed by
    // 'at' followed by text
    private static final String EXCEPTION_PATTERN = "^\\s+at.*";
    private static final String REGEXP_DEFAULT_WILDCARD = ".*?";
    private static final String REGEXP_GREEDY_WILDCARD = ".*";
    private static final String PATTERN_WILDCARD = "*";
    private static final String NOSPACE_GROUP = "(\\S*\\s*?)";
    private static final String DEFAULT_GROUP = "(" + REGEXP_DEFAULT_WILDCARD + ")";
    private static final String GREEDY_GROUP = "(" + REGEXP_GREEDY_WILDCARD + ")";
    private static final String MULTIPLE_SPACES_REGEXP = "[ ]+";
    private final String newLine = System.getProperty("line.separator");

    private static final String VALID_DATEFORMAT_CHARS = "GyMwWDdFEaHkKhmsSzZ";
    private static final String VALID_DATEFORMAT_CHAR_PATTERN = "[" + VALID_DATEFORMAT_CHARS + "]";

    private String timestampFormat = "yyyy-MM-d HH:mm:ss,SSS";
    private String logFormat;
    private String customLevelDefinitions;
    // private String filterExpression;
    private String regexp;
    private Pattern regexpPattern;
    private Pattern exceptionPattern;
    private String timestampPatternText;

    private ParserDescription parserDescription;

    public Log4jPatternMultilineLogParser() {
        keywords.add(TIMESTAMP);
        keywords.add(LOGGER);
        keywords.add(LEVEL);
        keywords.add(THREAD);
        keywords.add(CLASS);
        keywords.add(FILE);
        keywords.add(LINE);
        keywords.add(METHOD);
        keywords.add(MESSAGE);
        keywords.add(NDC);
        try {
            exceptionPattern = Pattern.compile(EXCEPTION_PATTERN);
        } catch (PatternSyntaxException pse) {
            // shouldn't happen
        }
        parserDescription = new ParserDescription();
        parserDescription.setDescription("desc");
        parserDescription.setDisplayName("displayName");

    }

    /**
     * If the log file contains non-log4j level strings, they can be mapped to log4j levels using the format (android example):
     * V=TRACE,D=DEBUG,I=INFO,W=WARN,E=ERROR,F=FATAL,S=OFF
     * 
     * @param customLevelDefinitions
     *          the level definition string
     */
    public void setCustomLevelDefinitions(String customLevelDefinitions) {
        this.customLevelDefinitions = customLevelDefinitions;
    }

    public String getCustomLevelDefinitions() {
        return customLevelDefinitions;
    }

    /**
     * Mutator. Specify a pattern from {@link java.text.SimpleDateFormat}
     * 
     * @param timestampFormat
     */
    public void setTimestampFormat(String timestampFormat) {
        this.timestampFormat = timestampFormat;
    }

    /**
     * Accessor
     * 
     * @return timestamp format
     */
    public String getTimestampFormat() {
        return timestampFormat;
    }

    /**
     * Walk the additionalLines list, looking for the EXCEPTION_PATTERN.
     * <p>
     * Return the index of the first matched line (the match may be the 1st line of an exception)
     * <p>
     * Assumptions: <br>
     * - the additionalLines list may contain both message and exception lines<br>
     * - message lines are added to the additionalLines list and then exception lines (all message lines occur in the list prior to all exception lines)
     * 
     * @return -1 if no exception line exists, line number otherwise
     */
    private int getExceptionLine(ParsingContext ctx) {
        String[] additionalLines = ctx.getUnmatchedLog().toString().split("\n");
        for (int i = 0; i < additionalLines.length; i++) {
            Matcher exceptionMatcher = exceptionPattern.matcher(additionalLines[i]);
            if (exceptionMatcher.matches()) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Combine all message lines occuring in the additionalLines list, adding a newline character between each line
     * <p>
     * the event will already have a message - combine this message with the message lines in the additionalLines list (all entries prior to the exceptionLine
     * index)
     * 
     * @param firstMessageLine
     *          primary message line
     * @param exceptionLine
     *          index of first exception line
     * @return message
     */
    private String buildMessage(String firstMessageLine, int exceptionLine, ParsingContext ctx) {
        if (ctx.getUnmatchedLog().length() == 0) {
            return firstMessageLine;
        }
        StringBuffer message = new StringBuffer();
        if (firstMessageLine != null) {
            message.append(firstMessageLine);
        }
        String[] additionalLines = ctx.getUnmatchedLog().toString().split("\n");
        int linesToProcess = (exceptionLine == -1 ? additionalLines.length : exceptionLine);

        for (int i = 0; i < linesToProcess; i++) {
            message.append(newLine);
            message.append(additionalLines[i]);
        }
        return message.toString();
    }

    /**
     * Combine all exception lines occuring in the additionalLines list into a String array
     * <p>
     * (all entries equal to or greater than the exceptionLine index)
     * 
     * @param exceptionLine
     *          index of first exception line
     * @return exception
     */
    private String[] buildException(int exceptionLine, ParsingContext ctx) {
        if (exceptionLine == -1) {
            return emptyException;
        }
        String[] additionalLines = ctx.getUnmatchedLog().toString().split("\n");
        String[] exception = new String[additionalLines.length - exceptionLine];
        for (int i = 0; i < exception.length; i++) {
            exception[i] = additionalLines[i + exceptionLine];
        }
        return exception;
    }

    /**
     * Construct a logging event from currentMap and additionalLines (additionalLines contains multiple message lines and any exception lines)
     * <p>
     * CurrentMap and additionalLines are cleared in the process
     * 
     * @return event
     */
    private LoggingEvent buildEvent(ParsingContext ctx) {
        HashMap<String, Object> logEventParsingProperitesMap = (HashMap<String, Object>) ctx
                .getCustomConextProperties().get(PROPERTY_LOG_EVENT_PROPERTIES);
        if (logEventParsingProperitesMap.size() == 0) {
            String[] additionalLines = ctx.getUnmatchedLog().toString().split("\n");
            for (String line : additionalLines) {
                LOG.finest(String.format("found non-matching (file %s) line: \"%s\"", ctx.getLogSource(), line));
            }
            ctx.getUnmatchedLog().setLength(0);
            return null;
        }
        // the current map contains fields - build an event
        int exceptionLine = getExceptionLine(ctx);
        String[] exception = buildException(exceptionLine, ctx);
        String[] additionalLines = ctx.getUnmatchedLog().toString().split("\n");
        // messages are listed before exceptions in additional lines
        if (additionalLines.length > 0 && exception.length > 0) {
            logEventParsingProperitesMap.put(MESSAGE,
                    buildMessage((String) logEventParsingProperitesMap.get(MESSAGE), exceptionLine, ctx));
        }
        DateFormat dateFormat = ctx.getDateFormat();
        LoggingEvent event = convertToEvent(logEventParsingProperitesMap, exception, dateFormat);

        logEventParsingProperitesMap.clear();
        ctx.getUnmatchedLog().setLength(0);
        return event;
    }

    protected void createPattern() {
        regexpPattern = Pattern.compile(regexp);
    }

    /**
     * Helper method that supports the evaluation of the expression
     * 
     * @param event
     * @return true if expression isn't set, or the result of the evaluation otherwise
     */
    private boolean passesExpression(LoggingEvent event) {
        if (event != null) {
            // if (expressionRule != null) {
            // return (expressionRule.evaluate(event));
            // }
        }
        return true;
    }

    /**
     * Convert the match into a map.
     * <p>
     * Relies on the fact that the matchingKeywords list is in the same order as the groups in the regular expression
     * 
     * @param result
     * @return map
     */
    private Map processEvent(MatchResult result) {
        Map map = new HashMap();
        // group zero is the entire match - process all other groups
        for (int i = 1; i < result.groupCount() + 1; i++) {
            Object key = matchingKeywords.get(i - 1);
            Object value = result.group(i);
            map.put(key, value);

        }
        return map;
    }

    /**
     * Helper method that will convert timestamp format to a pattern
     * 
     * 
     * @return string
     */
    private String convertTimestamp() {
        // some locales (for example, French) generate timestamp text with
        // characters not included in \w -
        // now using \S (all non-whitespace characters) instead of /w
        String result = timestampFormat.replaceAll(VALID_DATEFORMAT_CHAR_PATTERN + "+", "\\\\S+");
        // make sure dots in timestamp are escaped
        result = result.replaceAll(Pattern.quote("."), "\\\\.");
        return result;
    }

    /**
     * Build the regular expression needed to parse log entries
     * 
     */
    protected void initializePatterns() {

        // if custom level definitions exist, parse them
        updateCustomLevelDefinitionMap();

        List<String> buildingKeywords = new ArrayList<String>();

        String newPattern = logFormat;

        int index = 0;
        String current = newPattern;
        // build a list of property names and temporarily replace the property
        // with an empty string,
        // we'll rebuild the pattern later
        // ?  The propertyNames list is never used.
        List<String> propertyNames = new ArrayList<String>();
        while (index > -1) {
            if (current.indexOf(PROP_START) > -1 && current.indexOf(PROP_END) > -1) {
                index = current.indexOf(PROP_START);
                String longPropertyName = current.substring(current.indexOf(PROP_START),
                        current.indexOf(PROP_END) + 1);
                String shortProp = getShortPropertyName(longPropertyName);
                buildingKeywords.add(shortProp);
                propertyNames.add(longPropertyName);
                current = current.substring(longPropertyName.length() + 1 + index);
                newPattern = singleReplace(newPattern, longPropertyName,
                        new Integer(buildingKeywords.size() - 1).toString());
            } else {
                // no properties
                index = -1;
            }
        }

        /*
         * we're using a treemap, so the index will be used as the key to ensure keywords are ordered correctly
         * 
         * examine pattern, adding keywords to an index-based map patterns can contain only one of these per entry...properties are the only 'keyword' that can
         * occur multiple times in an entry
         */
        Iterator iter = keywords.iterator();
        while (iter.hasNext()) {
            String keyword = (String) iter.next();
            int index2 = newPattern.indexOf(keyword);
            if (index2 > -1) {
                buildingKeywords.add(keyword);
                newPattern = singleReplace(newPattern, keyword,
                        new Integer(buildingKeywords.size() - 1).toString());
            }
        }

        String buildingInt = "";

        for (int i = 0; i < newPattern.length(); i++) {
            String thisValue = String.valueOf(newPattern.substring(i, i + 1));
            if (isInteger(thisValue)) {
                buildingInt = buildingInt + thisValue;
            } else {
                if (isInteger(buildingInt)) {
                    matchingKeywords.add(buildingKeywords.get(Integer.parseInt(buildingInt)));
                }
                // reset
                buildingInt = "";
            }
        }

        // if the very last value is an int, make sure to add it
        if (isInteger(buildingInt)) {
            matchingKeywords.add(buildingKeywords.get(Integer.parseInt(buildingInt)));
        }

        newPattern = replaceMetaChars(newPattern);

        // compress one or more spaces in the pattern into the [ ]+ regexp
        // (supports padding of level in log files)
        newPattern = newPattern.replaceAll(MULTIPLE_SPACES_REGEXP, MULTIPLE_SPACES_REGEXP);
        newPattern = newPattern.replaceAll(Pattern.quote(PATTERN_WILDCARD), REGEXP_DEFAULT_WILDCARD);
        // use buildingKeywords here to ensure correct order
        for (int i = 0; i < buildingKeywords.size(); i++) {
            String keyword = (String) buildingKeywords.get(i);
            // make the final keyword greedy (we're assuming it's the message)
            if (i == (buildingKeywords.size() - 1)) {
                newPattern = singleReplace(newPattern, String.valueOf(i), GREEDY_GROUP);
            } else if (TIMESTAMP.equals(keyword)) {
                newPattern = singleReplace(newPattern, String.valueOf(i),
                        "(" + timestampPatternText.replaceAll("'", "") + ")");
            } else if (LOGGER.equals(keyword) || LEVEL.equals(keyword)) {
                newPattern = singleReplace(newPattern, String.valueOf(i), NOSPACE_GROUP);
            } else {
                newPattern = singleReplace(newPattern, String.valueOf(i), DEFAULT_GROUP);
            }
        }

        regexp = newPattern;
        LOG.fine("regexp is " + regexp);
    }

    private void updateCustomLevelDefinitionMap() {
        if (customLevelDefinitions != null) {
            StringTokenizer entryTokenizer = new StringTokenizer(customLevelDefinitions, ",");

            customLevelDefinitionMap.clear();
            while (entryTokenizer.hasMoreTokens()) {
                StringTokenizer innerTokenizer = new StringTokenizer(entryTokenizer.nextToken(), "=");
                String key = innerTokenizer.nextToken();
                String value = innerTokenizer.nextToken();
                customLevelDefinitionMap.put(key, Level.toLevel(value));
            }
        }
    }

    private boolean isInteger(String value) {
        try {
            Integer.parseInt(value);
            return true;
        } catch (NumberFormatException nfe) {
            return false;
        }
    }

    private String quoteTimeStampChars(String input) {
        // put single quotes around text that isn't a supported dateformat char
        StringBuffer result = new StringBuffer();
        // ok to default to false because we also check for index zero below
        boolean lastCharIsDateFormat = false;
        for (int i = 0; i < input.length(); i++) {
            String thisVal = input.substring(i, i + 1);
            boolean thisCharIsDateFormat = VALID_DATEFORMAT_CHARS.contains(thisVal);
            // we have encountered a non-dateformat char
            if (!thisCharIsDateFormat && (i == 0 || lastCharIsDateFormat)) {
                result.append("'");
            }
            // we have encountered a dateformat char after previously
            // encountering a non-dateformat char
            if (thisCharIsDateFormat && i > 0 && !lastCharIsDateFormat) {
                result.append("'");
            }
            lastCharIsDateFormat = thisCharIsDateFormat;
            result.append(thisVal);
        }
        // append an end single-quote if we ended with non-dateformat char
        if (!lastCharIsDateFormat) {
            result.append("'");
        }
        return result.toString();
    }

    private String singleReplace(String inputString, String oldString, String newString) {
        int propLength = oldString.length();
        int startPos = inputString.indexOf(oldString);
        if (startPos == -1) {
            LOG.info("string: " + oldString + " not found in input: " + inputString + " - returning input");
            return inputString;
        }
        if (startPos == 0) {
            inputString = inputString.substring(propLength);
            inputString = newString + inputString;
        } else {
            inputString = inputString.substring(0, startPos) + newString
                    + inputString.substring(startPos + propLength);
        }
        return inputString;
    }

    private String getShortPropertyName(String longPropertyName) {
        String currentProp = longPropertyName.substring(longPropertyName.indexOf(PROP_START));
        String prop = currentProp.substring(0, currentProp.indexOf(PROP_END) + 1);
        String shortProp = prop.substring(PROP_START.length(), prop.length() - 1);
        return shortProp;
    }

    /**
     * Some perl5 characters may occur in the log file format. Escape these characters to prevent parsing errors.
     * 
     * @param input
     * @return string
     */
    private String replaceMetaChars(String input) {
        // escape backslash first since that character is used to escape the
        // remaining meta chars
        input = input.replaceAll("\\\\", "\\\\\\");

        // don't escape star - it's used as the wildcard
        input = input.replaceAll(Pattern.quote("]"), "\\\\]");
        input = input.replaceAll(Pattern.quote("["), "\\\\[");
        input = input.replaceAll(Pattern.quote("^"), "\\\\^");
        input = input.replaceAll(Pattern.quote("$"), "\\\\$");
        input = input.replaceAll(Pattern.quote("."), "\\\\.");
        input = input.replaceAll(Pattern.quote("|"), "\\\\|");
        input = input.replaceAll(Pattern.quote("?"), "\\\\?");
        input = input.replaceAll(Pattern.quote("+"), "\\\\+");
        input = input.replaceAll(Pattern.quote("("), "\\\\(");
        input = input.replaceAll(Pattern.quote(")"), "\\\\)");
        input = input.replaceAll(Pattern.quote("-"), "\\\\-");
        input = input.replaceAll(Pattern.quote("{"), "\\\\{");
        input = input.replaceAll(Pattern.quote("}"), "\\\\}");
        input = input.replaceAll(Pattern.quote("#"), "\\\\#");
        return input;
    }

    /**
     * Convert a keyword-to-values map to a LoggingEvent
     * 
     * @param fieldMap
     * @param exception
     * 
     * @return logging event
     */
    private LoggingEvent convertToEvent(Map fieldMap, String[] exception, DateFormat dateFormat) {
        if (fieldMap == null) {
            return null;
        }

        // a logger must exist at a minimum for the event to be processed
        if (!fieldMap.containsKey(LOGGER)) {
            fieldMap.put(LOGGER, "Unknown");
        }
        if (exception == null) {
            exception = emptyException;
        }

        Logger logger = null;
        long timeStamp = 0L;
        String level = null;
        String threadName = null;
        Object message = null;
        String ndc = null;
        String className = null;
        String methodName = null;
        String eventFileName = null;
        String lineNumber = null;
        Hashtable properties = new Hashtable();

        logger = Logger.getLogger((String) fieldMap.remove(LOGGER));

        if ((dateFormat != null) && fieldMap.containsKey(TIMESTAMP)) {
            String dateString = (String) fieldMap.remove(TIMESTAMP);
            try {
                timeStamp = dateFormat.parse(dateString).getTime();
            } catch (Exception e) {
                LOG.log(java.util.logging.Level.WARNING,
                        "Error parsing date with format \"" + dateFormat + "\" with String \"" + dateString + "\"",
                        e);
            }
        }
        // use current time if timestamp not parseable
        if (timeStamp == 0L) {
            timeStamp = System.currentTimeMillis();
        }

        message = fieldMap.remove(MESSAGE);
        if (message == null) {
            message = "";
        }

        level = (String) fieldMap.remove(LEVEL);
        Level levelImpl;
        if (level == null) {
            levelImpl = Level.DEBUG;
        } else {
            // first try to resolve against custom level definition map, then
            // fall back to regular levels
            level = level.trim();
            levelImpl = customLevelDefinitionMap.get(level);
            if (levelImpl == null) {
                levelImpl = Level.toLevel(level.trim());
                if (!level.equals(levelImpl.toString())) {
                    // check custom level map
                    levelImpl = Level.DEBUG;
                    LOG.fine("found unexpected level: " + level + ", logger: " + logger.getName() + ", msg: "
                            + message);
                    // make sure the text that couldn't match a level is
                    // added to the message
                    message = level + " " + message;
                }
            }
        }

        threadName = (String) fieldMap.remove(THREAD);
        if (threadName == null) {
            threadName = "";
        }

        ndc = (String) fieldMap.remove(NDC);

        className = (String) fieldMap.remove(CLASS);

        methodName = (String) fieldMap.remove(METHOD);

        eventFileName = (String) fieldMap.remove(FILE);

        lineNumber = (String) fieldMap.remove(LINE);

        // properties.put(Constants.HOSTNAME_KEY, host);
        // properties.put(Constants.APPLICATION_KEY, path);
        // properties.put(Constants.RECEIVER_NAME_KEY, getName());

        // all remaining entries in fieldmap are properties
        properties.putAll(fieldMap);

        LocationInfo info = null;

        if ((eventFileName != null) || (className != null) || (methodName != null) || (lineNumber != null)) {
            info = new LocationInfo(eventFileName, className, methodName, lineNumber);
        } else {
            info = LocationInfo.NA_LOCATION_INFO;
        }
        // LoggingEvent event = new LoggingEvent(null,
        // logger, timeStamp, levelImpl, message,
        // threadName,
        // new ThrowableInformation(exception),
        // ndc,
        // info,
        // properties);
        // LoggingEvent event = new LoggingEvent();
        LoggingEvent event = new LoggingEvent(null, logger, timeStamp, levelImpl, message, threadName,
                new ThrowableInformation(exception), ndc, info, properties);
        // event.setLogger(logger);
        // event.setTimeStamp(timeStamp);
        // event.setLevel(levelImpl);
        // event.setMessage(message);
        // event.setThreadName(threadName);
        // event.setThrowableInformation(new ThrowableInformation(exception));
        // event.setNDC(ndc);
        // event.setLocationInformation(info);
        // event.setProperties(properties);
        return event;
    }

    @Override
    public LogData parse(String line, ParsingContext parsingContext) throws ParseException {

        LogData logData = null;
        if (line.trim().equals("")) {
            parsingContext.getUnmatchedLog().append('\n');
            parsingContext.getUnmatchedLog().append(line);
            return null;
        }

        Matcher eventMatcher = regexpPattern.matcher(line);
        Matcher exceptionMatcher = exceptionPattern.matcher(line);
        HashMap<String, Object> logEventParsingProperties = (HashMap<String, Object>) parsingContext
                .getCustomConextProperties().get(PROPERTY_LOG_EVENT_PROPERTIES);
        if (eventMatcher.matches()) {
            // build an event from the previous match (held in current map)
            LoggingEvent event = buildEvent(parsingContext);
            if (event != null && passesExpression(event)) {
                // doPost(event);
                logData = Log4jUtil.translateLog4j(event);
            }
            // Allow for optional capture fields.
            // This is used by rePattern now, but traditional patterns could be
            // enhanced to support optional fields too.
            for (Map.Entry<String, Object> entry : (Set<Map.Entry<String, Object>>) processEvent(
                    eventMatcher.toMatchResult()).entrySet())
                if (entry.getValue() != null) // We never write null key
                    logEventParsingProperties.put(entry.getKey(), entry.getValue());
        } else if (exceptionMatcher.matches()) {
            // an exception line
            if (parsingContext.getUnmatchedLog().length() > 0)
                parsingContext.getUnmatchedLog().append('\n');
            parsingContext.getUnmatchedLog().append(line);
        } else {
            // neither...either post an event with the line or append as
            // additional lines
            // if this was a logging event with multiple lines, each line
            // will show up as its own event instead of being
            // appended as multiple lines on the same event..
            // choice is to have each non-matching line show up as its own
            // line, or append them all to a previous event
            if (appendNonMatches) {
                // hold on to the previous time, so we can do our best to
                // preserve time-based ordering if the event is a non-match
                String lastTime = (String) logEventParsingProperties.get(TIMESTAMP);
                // build an event from the previous match (held in current
                // map)
                if (logEventParsingProperties.size() > 0) {
                    LoggingEvent event = buildEvent(parsingContext);
                    if (event != null) {
                        if (passesExpression(event)) {
                            logData = Log4jUtil.translateLog4j(event);
                        }
                    }
                }
                if (lastTime != null) {
                    logEventParsingProperties.put(TIMESTAMP, lastTime);
                }
                logEventParsingProperties.put(MESSAGE, line);
            } else {
                if (parsingContext.getUnmatchedLog().length() > 0) {
                    parsingContext.getUnmatchedLog().append('\n');
                }
                parsingContext.getUnmatchedLog().append(line.trim());
            }
        }

        return logData;
    }

    @Override
    public ParserDescription getParserDescription() {
        return parserDescription;
    }

    @Override
    public LogData parseBuffer(ParsingContext parsingContext) throws ParseException {
        LogData logData = null;
        // build an event from the previous match (held in current map)
        LoggingEvent event = buildEvent(parsingContext);
        if (event != null) {
            if (passesExpression(event)) {
                // doPost(event);
                logData = Log4jUtil.translateLog4j(event);
            }
        }
        return logData;
        // data.currentMap.putAll(processEvent(eventMatcher.toMatchResult(), data));
    }

    @Override
    public void init(Properties properties) throws InitializationException {
        String rePattern = properties.getProperty(PROPERTY_REPATTERN);
        logFormat = properties.getProperty(PROPERTY_PATTERN);
        if (!StringUtils.isBlank(logFormat) && rePattern != null) {
            throw new InitializationException(String.format("Conflicting log patterns set (properties %s and %s)",
                    PROPERTY_PATTERN, PROPERTY_REPATTERN));
        }
        if (StringUtils.isBlank(logFormat) && rePattern == null) {
            throw new InitializationException(
                    String.format("Log pattern not set (property %s or %s)", PROPERTY_PATTERN, PROPERTY_REPATTERN));
        }
        timestampFormat = properties.getProperty(PROPERTY_DATE_FORMAT);
        if (StringUtils.isBlank(timestampFormat)) {
            throw new InitializationException(
                    String.format("Date format not set (property %s)", PROPERTY_DATE_FORMAT));
        }
        customLevelDefinitions = properties.getProperty(PROPERTY_CUSTOM_LEVELS);
        parserDescription.setDisplayName(properties.getProperty(PROPERTY_NAME, "?"));
        parserDescription.setDescription(properties.getProperty(PROPERTY_DESCRIPTION, "?"));
        parserDescription.setCharset(properties.getProperty(PROPERTY_CHARSET, "UTF-8"));
        if (timestampFormat != null) {
            // dateFormat = new SimpleDateFormat(quoteTimeStampChars(timestampFormat));
            timestampPatternText = convertTimestamp();
        }

        if (rePattern == null) {
            initializePatterns();
            createPattern();
        } else {
            try {
                regexpPattern = Pattern.compile(rePattern);
            } catch (PatternSyntaxException pse) {
                throw new InitializationException(String.format("Malformatted regex pattern for '%s' (%s): %s",
                        PROPERTY_REPATTERN, rePattern, pse.getDescription()));
            }
            // if custom level definitions exist, parse them
            updateCustomLevelDefinitionMap();

            Map<Integer, String> groupMap = new HashMap<Integer, String>();
            Enumeration<String> e = (Enumeration<String>) properties.propertyNames();
            String key = null, val = null;
            int keyLen, dotGrouplen;
            int dotGroupLen = ".group".length();
            while (e.hasMoreElements())
                try {
                    key = e.nextElement();
                    keyLen = key.length();
                    if (keyLen <= dotGroupLen || !key.endsWith(".group"))
                        continue;
                    val = properties.getProperty(key);
                    groupMap.put(Integer.valueOf(val), key.substring(0, keyLen - dotGroupLen));
                } catch (NumberFormatException ne) {
                    throw new InitializationException(
                            String.format("Group property '%s.group' set to non-integer: %s", key, val));
                }
            if (groupMap.size() < 1)
                throw new InitializationException(PROPERTY_REPATTERN + " set but no group properties set.  "
                        + "Set group indexes like 'TIMESTAMP.group=1', " + "starting with index 1");
            for (int i = 1; i <= groupMap.size(); i++) {
                if (!groupMap.containsKey(Integer.valueOf(i)))
                    throw new InitializationException("Group property numbers not consecutive starting at 1");
                matchingKeywords.add(groupMap.get(Integer.valueOf(i)));
            }
            if (matchingKeywords.contains(Log4jPatternMultilineLogParser.MESSAGE) && !matchingKeywords
                    .get(matchingKeywords.size() - 1).equals(Log4jPatternMultilineLogParser.MESSAGE))
                throw new InitializationException("If MESSAGE group is present, it must be last");
        }

    }

    @Override
    public void initParsingContext(ParsingContext parsingContext) {
        if (timestampFormat != null) {
            parsingContext.setDateFormat(new SimpleDateFormat(timestampFormat));
        }
        parsingContext.getCustomConextProperties().put(PROPERTY_LOG_EVENT_PROPERTIES,
                new HashMap<String, Object>());

    }

    @Override
    public int getVersion() {
        return LOG_PARSER_VERSION_1;
    }

    @Override
    public TableColumns[] getTableColumnsToUse() {
        /* This replaces method Log4jUtil.getUsedColumns(logFormat), which is
         * less robust and efficient.
         * Seems to be no intentionto share the functionality so it doesn't
         * belong in any *Util* class.
         */
        ArrayList<TableColumns> list = new ArrayList<TableColumns>();
        if (matchingKeywords.contains(Log4jPatternMultilineLogParser.CLASS))
            list.add(TableColumns.CLASS);
        if (matchingKeywords.contains(Log4jPatternMultilineLogParser.TIMESTAMP))
            list.add(TableColumns.TIME);
        if (matchingKeywords.contains(Log4jPatternMultilineLogParser.LEVEL))
            list.add(TableColumns.LEVEL);
        if (matchingKeywords.contains(Log4jPatternMultilineLogParser.THREAD))
            list.add(TableColumns.THREAD);
        if (matchingKeywords.contains(Log4jPatternMultilineLogParser.MESSAGE))
            list.add(TableColumns.MESSAGE);
        if (matchingKeywords.contains(Log4jPatternMultilineLogParser.METHOD))
            list.add(TableColumns.METHOD);
        list.add(TableColumns.ID);
        list.add(TableColumns.MARK);
        list.add(TableColumns.NOTE);

        if (matchingKeywords.contains(Log4jPatternMultilineLogParser.FILE))
            list.add(TableColumns.FILE);

        if (matchingKeywords.contains(Log4jPatternMultilineLogParser.LINE))
            list.add(TableColumns.LINE);

        if (matchingKeywords.contains(Log4jPatternMultilineLogParser.NDC))
            list.add(TableColumns.NDC);

        if (!keywords.containsAll(matchingKeywords))
            list.add(TableColumns.PROPERTIES);

        return list.toArray(new TableColumns[0]);
    }
}