/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2, as
 * published by the Free Software Foundation, and/or the GNU Lesser
 * General Public License, version 2.1, also as published by the Free
 * Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License and the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License
 * and the GNU Lesser General Public License along with this program;
 * if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.rhq.core.pluginapi.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Date;
import java.util.List;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import org.rhq.core.domain.measurement.calltime.CallTimeData;

/**
 * This is a very simple log parser that uses a StringTokenizer instead of a regular expression to parse a HTTP
 * response-time log file. This should greatly improve the performance. It requires that lines in the log file have the
 * following format (with one line per HTTP request):
 *
 * <p/><code>URL date_in_milliseconds time_taken [status_code [IP_address]]</code>
 *
 * <p/>This is the output format used by the Apache RT module, as well as the servlet RT filter.
 *
 * @author Ian Springer
 */
public class ResponseTimeLogParser {
    public static final int DEFAULT_TIME_MULTIPLIER = 1;

    protected final Log log = LogFactory.getLog(this.getClass());

    /**
     * The parser will multiply by this factor to convert the duration from the log into milliseconds.
     */
    private double timeMultiplier;
    private long startingOffset;
    protected File logFile;
    protected List<Pattern> excludes;
    protected List<RegexSubstitution> transforms;

    public ResponseTimeLogParser(File logFile) {
        this(logFile, DEFAULT_TIME_MULTIPLIER);
    }

    public ResponseTimeLogParser(File logFile, double timeMultiplier) {
        this.logFile = logFile;
        this.timeMultiplier = timeMultiplier;
    }

    /**
     * Parse the log file, starting at the offset corresponding to the file's size after the last time this method was
     * called. Immediately after parsing, the file will be truncated, permissions permitting. If the log file does not
     * exist, a warning will be logged and the method will return. The parsed response-time data will be added to the
     * passed-in CallTimeData object.
     *
     * @param callTimeData the parsed response-time data will be added to this object
     * @throws IOException if an error occurs reading the log file
     */
    public synchronized void parseLog(CallTimeData callTimeData) throws IOException {
        log.debug("Parsing response-time log file " + this.logFile + "...");
        BufferedReader in = null;

        try {
            in = new BufferedReader(new FileReader(this.logFile));

            in.skip(this.startingOffset);

            String currentLine;
            while ((currentLine = in.readLine()) != null) {
                LogEntry logEntry;
                try {
                    logEntry = parseLine(currentLine);
                } catch (Exception e) {
                    log.debug("Problem parsing line [" + currentLine + "] - cause: " + e);
                    continue;
                }

                String url = logEntry.getUrl();

                // The URL should always begin with a slash. If it doesn't, log an error and skip the entry,
                // so we don't end up with bogus data in the DB.
                if (url.charAt(0) != '/') {
                    String truncatedUrl = url.substring(0, Math.min(url.length(), 120));
                    if (url.length() > 120)
                        truncatedUrl += "...";
                    log.error("URL ('" + truncatedUrl
                        + "') parsed from response-time log file does not begin with '/'. " + "Line being parsed is ["
                        + currentLine + "].");
                    continue;
                }

                if (isExcluded(url)) {
                    continue;
                }

                // Only collect stats for successful (2xx or 3xx) requests...
                if ((logEntry.getStatusCode() != null)
                    && ((logEntry.getStatusCode() < 200) || (logEntry.getStatusCode() >= 400))) {
                    continue;
                }

                String transformedUrl = applyTransforms(url);
                try {
                    callTimeData.addCallData(transformedUrl, new Date(logEntry.getStartTime()), logEntry.getDuration());
                } catch (IllegalArgumentException iae) {
                    // if any issue with the data, log them and continue processing the rest of the report
                    log.error(iae);
                }
            }
        } catch (FileNotFoundException e) {
            log.warn("Response-time log file '" + this.logFile + "' does not exist.");
            return;
        } finally {
            if (null != in) {
                try {
                    in.close();
                } catch (Exception e) {
                    log.error("Unable to close response-time log file.", e);
                }
            }
        }

        /*
         * After we're done parsing the file, truncate it. This is kosher, assuming we own any file being parsed by this
         * parser.
         */
        truncateLog(this.logFile);
        this.startingOffset = this.logFile.length();
    }

    protected boolean isExcluded(String url) {
        boolean excluded = false;
        if (this.excludes != null) {
            for (Pattern exclude : this.excludes) {
                Matcher matcher = exclude.matcher(url);
                if (matcher.find()) {
                    log.debug("URL '" + url + "' excluded by exclude '" + exclude + "'");
                    excluded = true;
                }
            }
        }

        return excluded;
    }

    protected String applyTransforms(String url) {
        String transformedUrl = null;
        if (this.transforms != null) {
            for (RegexSubstitution transform : this.transforms) {
                Matcher matcher = transform.getPattern().matcher(url);
                if (matcher.find()) {
                    transformedUrl = matcher.replaceFirst(transform.getReplacement());
                    log.debug("URL '" + url + "' transformed to '" + transformedUrl + "' by transform '" + transform
                        + "'.");
                    break;
                }
            }
        }

        return (transformedUrl != null) ? transformedUrl : url;
    }

    /**
     * Parses a line from a response time log and returns a LogEntry.
     *
     * @param line the line to be parsed
     *
     * @return a LogEntry representing the line
     *
     * @throws Exception if parsing of the line fails
     */
    @NotNull
    protected LogEntry parseLine(String line) throws Exception {
        LogEntry logEntry;
        try {
            StringTokenizer tokenizer = new StringTokenizer(line);
            String url = tokenizer.nextToken();
            long startTime = Long.parseLong(tokenizer.nextToken());
            long duration = (long) (Double.parseDouble(tokenizer.nextToken()) * this.timeMultiplier);
            Integer statusCode = null;
            String ipAddress = null;
            if (tokenizer.hasMoreTokens()) {
                statusCode = Integer.valueOf(tokenizer.nextToken());
                if (tokenizer.hasMoreTokens()) {
                    ipAddress = tokenizer.nextToken();
                }
            }

            logEntry = new LogEntry(url, startTime, duration, statusCode, ipAddress);
        } catch (RuntimeException e) {
            throw new Exception("Failed to parse response time log file line [" + line + "].", e);
        }

        return logEntry;
    }

    private void truncateLog(File logFile) throws IOException {
        log.debug("Truncating response-time log file: '" + logFile + "'...");
        RandomAccessFile randomAccessFile = null;

        try {
            String mode = "rws";
            randomAccessFile = new RandomAccessFile(logFile, mode);
            log.debug("Truncating response-time log file: setting length to 0.");
            randomAccessFile.setLength(0);
        } catch (SecurityException e) {
            /* User doesn't have permission to change the length, so
             * ignore this exception.
             */
            log.debug("Unable to truncate response-time log file.", e);
        } catch (FileNotFoundException e) {
            /* Can't happen.  We have just parsed this file.
             * Could be a permission error.  Log it.
             */
            log.error("Unable to truncate response-time log file.", e);
        } finally {
            if (null != randomAccessFile) {
                try {
                    log.debug("Truncating response-time log file: closing file.");
                    randomAccessFile.close();
                } catch (Exception e) {
                    log.error("Unable to close response-time log file.", e);
                }
            }
        }
    }

    public File getLogFile() {
        return logFile;
    }

    public void setLogFile(File logFile) {
        this.logFile = logFile;
    }

    public double getTimeMultiplier() {
        return timeMultiplier;
    }

    public List<Pattern> getExcludes() {
        return excludes;
    }

    public void setExcludes(List<Pattern> excludes) {
        this.excludes = excludes;
    }

    public List<RegexSubstitution> getTransforms() {
        return transforms;
    }

    public void setTransforms(List<RegexSubstitution> transforms) {
        this.transforms = transforms;
    }

    public class LogEntry {
        public LogEntry(@NotNull String url, long startTime, long duration, @Nullable Integer statusCode,
            @Nullable String ipAddress) {
            this.url = url;
            this.startTime = startTime;
            this.duration = duration;
            this.statusCode = statusCode;
            this.ipAddress = ipAddress;
        }

        private String url;
        private long startTime;
        private long duration;
        private Integer statusCode;
        private String ipAddress;

        @NotNull
        public String getUrl() {
            return url;
        }

        public long getStartTime() {
            return startTime;
        }

        public long getDuration() {
            return duration;
        }

        @Nullable
        public Integer getStatusCode() {
            return statusCode;
        }

        @Nullable
        public String getIpAddress() {
            return ipAddress;
        }
    }
}