/*
 * $Header: /home/cvs/jakarta-tomcat-catalina/catalina/src/share/org/apache/catalina/valves/ExtendedAccessLogValve.java,v 1.7 2004/01/26 19:48:18 remm Exp $
 * $Revision: 1.7 $
 * $Date: 2004/01/26 19:48:18 $
 * ====================================================================
 *
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 1999-2001 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 * [Additional notices, if required by prior licensing conditions]
 *
 */


package org.apache.catalina.valves;


import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.URLEncoder;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.TimeZone;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.catalina.HttpResponse;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Request;
import org.apache.catalina.Response;
import org.apache.catalina.ValveContext;
import org.apache.catalina.util.LifecycleSupport;
import org.apache.catalina.util.ServerInfo;
import org.apache.catalina.util.StringManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;



/**
 * An implementation of the W3c Extended Log File Format. See
 * http://www.w3.org/TR/WD-logfile.html for more information about the format.
 *
 * The following fields are supported:
 * <ul>
 * <li><code>c-dns</code>:  Client hostname</li>
 * <li><code>c-ip</code>:  Client ip address</li>
 * <li><code>bytes</code>:  bytes served</li>
 * <li><code>cs-method</code>:  request method</li>
 * <li><code>cs-uri</code>:  The full uri requested</li>
 * <li><code>cs-uri-query</code>:  The query string</li>
 * <li><code>cs-uri-stem</code>:  The uri without query string</li>
 * <li><code>date</code>:  The date in yyyy-mm-dd  format for GMT</li>
 * <li><code>s-dns</code>: The server dns entry </li>
 * <li><code>s-ip</code>:  The server ip address</li>
 * <li><code>cs(XXX)</code>:  The value of header XXX from client to server</li>
 * <li><code>sc(XXX)</code>: The value of header XXX from server to client </li>
 * <li><code>sc-status</code>:  The status code</li>
 * <li><code>time</code>:  Time the request was served</li>
 * <li><code>time-taken</code>:  Time (in seconds) taken to serve the request</li>
 * <li><code>x-A(XXX)</code>: Pull XXX attribute from the servlet context </li>
 * <li><code>x-C(XXX)</code>: Pull the first cookie of the name XXX </li>
 * <li><code>x-R(XXX)</code>: Pull XXX attribute from the servlet request </li>
 * <li><code>x-S(XXX)</code>: Pull XXX attribute from the session </li>
 * <li><code>x-P(...)</code>:  Call request.getParameter(...)
 *                             and URLencode it. Helpful to capture
 *                             certain POST parameters.
 * </li>
 * <li>For any of the x-H(...) the following method will be called from the
 *                HttpServletRequestObject </li>
 * <li><code>x-H(authType)</code>: getAuthType </li>
 * <li><code>x-H(characterEncoding)</code>: getCharacterEncoding </li>
 * <li><code>x-H(contentLength)</code>: getContentLength </li>
 * <li><code>x-H(locale)</code>:  getLocale</li>
 * <li><code>x-H(protocol)</code>: getProtocol </li>
 * <li><code>x-H(remoteUser)</code>:  getRemoteUser</li>
 * <li><code>x-H(requestedSessionId)</code>: getGequestedSessionId</li>
 * <li><code>x-H(requestedSessionIdFromCookie)</code>:
 *                  isRequestedSessionIdFromCookie </li>
 * <li><code>x-H(requestedSessionIdValid)</code>:
 *                  isRequestedSessionIdValid</li>
 * <li><code>x-H(scheme)</code>:  getScheme</li>
 * <li><code>x-H(secure)</code>:  isSecure</li>
 * </ul>
 *
 *
 *
 * <p>
 * Log rotation can be on or off. This is dictated by the rotatable
 * property.
 * </p>
 *
 * <p>
 * For UNIX users, another field called <code>checkExists</code>is also
 * available. If set to true, the log file's existence will be checked before
 * each logging. This way an external log rotator can move the file
 * somewhere and tomcat will start with a new file.
 * </p>
 *
 * <p>
 * For JMX junkies, a public method called </code>rotate</code> has
 * been made available to allow you to tell this instance to move
 * the existing log file to somewhere else start writing a new log file.
 * </p>
 *
 * <p>
 * Conditional logging is also supported. This can be done with the
 * <code>condition</code> property.
 * If the value returned from ServletRequest.getAttribute(condition)
 * yields a non-null value. The logging will be skipped.
 * </p>
 *
 * <p>
 * For extended attributes coming from a getAttribute() call,
 * it is you responsibility to ensure there are no newline or
 * control characters.
 * </p>
 *
 *
 * @author Tim Funk
 * @version $Revision: 1.7 $ $Date: 2004/01/26 19:48:18 $
 */

public final class ExtendedAccessLogValve
    extends ValveBase
    implements Lifecycle {


    // ----------------------------------------------------------- Constructors


    /**
     * Construct a new instance of this class with default property values.
     */
    public ExtendedAccessLogValve() {

        super();


    }


    // ----------------------------------------------------- Instance Variables
    private static Log log = LogFactory.getLog(ExtendedAccessLogValve.class);


    /**
     * The descriptive information about this implementation.
     */
    protected static final String info =
        "org.apache.catalina.valves.ExtendedAccessLogValve/1.0";


    /**
     * The lifecycle event support for this component.
     */
    protected LifecycleSupport lifecycle = new LifecycleSupport(this);



    /**
     * The string manager for this package.
     */
    private StringManager sm =
        StringManager.getManager(Constants.Package);


    /**
     * Has this component been started yet?
     */
    private boolean started = false;


    /**
     * The as-of date for the currently open log file, or a zero-length
     * string if there is no open log file.
     */
    private String dateStamp = "";


    /**
     * The PrintWriter to which we are currently logging, if any.
     */
    private PrintWriter writer = null;


    /**
     * The formatter for the date contained in the file name.
     */
    private SimpleDateFormat fileDateFormatter = null;


    /**
     * A date formatter to format a Date into a date in the format
     * "yyyy-MM-dd".
     */
    private SimpleDateFormat dateFormatter = null;


    /**
     * A date formatter to format a Date into a time in the format
     * "kk:mm:ss" (kk is a 24-hour representation of the hour).
     */
    private SimpleDateFormat timeFormatter = null;


    /**
     * Time taken formatter for 3 decimal places.
     */
     private DecimalFormat timeTakenFormatter = null;


    /**
     * My ip address. Look it up once and remember it. Dump this if we can
     * determine another reliable way to get server ip address since this
     * server may have many ip's.
     */
    private String myIpAddress = null;


    /**
     * My dns name. Look it up once and remember it. Dump this if we can
     * determine another reliable way to get server name address since this
     * server may have many ip's.
     */
    private String myDNSName = null;


    /**
     * Holder for all of the fields to log after the pattern is decoded.
     */
    private FieldInfo[] fieldInfos;


    /**
     * The current log file we are writing to. Helpful when checkExists
     * is true.
     */
    private File currentLogFile = null;



    /**
     * The system time when we last updated the Date that this valve
     * uses for log lines.
     */
    private Date currentDate = null;


    /**
     * Instant when the log daily rotation was last checked.
     */
    private long rotationLastChecked = 0L;


    /**
     * The directory in which log files are created.
     */
    private String directory = "logs";


    /**
     * The pattern used to format our access log lines.
     */
    private String pattern = null;


    /**
     * The prefix that is added to log file filenames.
     */
    private String prefix = "access_log.";


    /**
     * Should we rotate our log file? Default is true (like old behavior)
     */
    private boolean rotatable = true;


    /**
     * The suffix that is added to log file filenames.
     */
    private String suffix = "";


    /**
     * Are we doing conditional logging. default false.
     */
    private String condition = null;


    /**
     * Do we check for log file existence? Helpful if an external
     * agent renames the log file so we can automagically recreate it.
     */
    private boolean checkExists = false;


    /**
     * Date format to place in log file name. Use at your own risk!
     */
    private String fileDateFormat = null;


    // ------------------------------------------------------------- Properties


    /**
     * Return the directory in which we create log files.
     */
    public String getDirectory() {

        return (directory);

    }


    /**
     * Set the directory in which we create log files.
     *
     * @param directory The new log file directory
     */
    public void setDirectory(String directory) {

        this.directory = directory;

    }


    /**
     * Return descriptive information about this implementation.
     */
    public String getInfo() {

        return (info);

    }


    /**
     * Return the format pattern.
     */
    public String getPattern() {

        return (this.pattern);

    }


    /**
     * Set the format pattern, first translating any recognized alias.
     *
     * @param pattern The new pattern pattern
     */
    public void setPattern(String pattern) {

        FieldInfo[] f= decodePattern(pattern);
        if (f!=null) {
            this.pattern = pattern;
            this.fieldInfos = f;
        }
    }


    /**
     * Return the log file prefix.
     */
    public String getPrefix() {

        return (prefix);

    }


    /**
     * Set the log file prefix.
     *
     * @param prefix The new log file prefix
     */
    public void setPrefix(String prefix) {

        this.prefix = prefix;

    }


    /**
     * Return true if logs are automatically rotated.
     */
    public boolean isRotatable() {

        return rotatable;

    }


    /**
     * Set the value is we should we rotate the logs
     *
     * @param rotatable true is we should rotate.
     */
    public void setRotatable(boolean rotatable) {

        this.rotatable = rotatable;

    }


    /**
     * Return the log file suffix.
     */
    public String getSuffix() {

        return (suffix);

    }


    /**
     * Set the log file suffix.
     *
     * @param suffix The new log file suffix
     */
    public void setSuffix(String suffix) {

        this.suffix = suffix;

    }



    /**
     * Return whether the attribute name to look for when
     * performing conditional loggging. If null, every
     * request is logged.
     */
    public String getCondition() {

        return condition;

    }


    /**
     * Set the ServletRequest.attribute to look for to perform
     * conditional logging. Set to null to log everything.
     *
     * @param condition Set to null to log everything
     */
    public void setCondition(String condition) {

        this.condition = condition;

    }



    /**
     * Check for file existence before logging.
     */
    public boolean isCheckExists() {

        return checkExists;

    }


    /**
     * Set whether to check for log file existence before logging.
     *
     * @param checkExists true meaning to check for file existence.
     */
    public void setCheckExists(boolean checkExists) {

        this.checkExists = checkExists;

    }


    /**
     *  Return the date format date based log rotation.
     */
    public String getFileDateFormat() {
        return fileDateFormat;
    }


    /**
     *  Set the date format date based log rotation.
     */
    public void setFileDateFormat(String fileDateFormat) {
        this.fileDateFormat =  fileDateFormat;
    }


    // --------------------------------------------------------- Public Methods


    /**
     * Log a message summarizing the specified request and response, according
     * to the format specified by the <code>pattern</code> property.
     *
     * @param request Request being processed
     * @param response Response being processed
     * @param context The valve context used to invoke the next valve
     *  in the current processing pipeline
     *
     * @exception IOException if an input/output error has occurred
     * @exception ServletException if a servlet error has occurred
     */
    public void invoke(Request request, Response response,
                       ValveContext context)
        throws IOException, ServletException {

        // Pass this request on to the next valve in our pipeline
        long endTime;
        long runTime;
        long startTime=System.currentTimeMillis();

        context.invokeNext(request, response);

        endTime = System.currentTimeMillis();
        runTime = endTime-startTime;

        if (fieldInfos==null || condition!=null &&
              null!=request.getRequest().getAttribute(condition)) {
            return;
        }


        Date date = getDate(endTime);
        StringBuffer result = new StringBuffer();

        for (int i=0; fieldInfos!=null && i<fieldInfos.length; i++) {
            switch(fieldInfos[i].type) {
                case FieldInfo.DATA_CLIENT:
                    if (FieldInfo.FIELD_IP==fieldInfos[i].location)
                        result.append(request.getRequest().getRemoteAddr());
                    else if (FieldInfo.FIELD_DNS==fieldInfos[i].location)
                        result.append(request.getRequest().getRemoteHost());
                    else
                        result.append("?WTF?"); /* This should never happen! */
                    break;
                case FieldInfo.DATA_SERVER:
                    if (FieldInfo.FIELD_IP==fieldInfos[i].location)
                        result.append(myIpAddress);
                    else if (FieldInfo.FIELD_DNS==fieldInfos[i].location)
                        result.append(myDNSName);
                    else
                        result.append("?WTF?"); /* This should never happen! */
                    break;
                case FieldInfo.DATA_REMOTE:
                    result.append('?'); /* I don't know how to handle these! */
                    break;
                case FieldInfo.DATA_CLIENT_TO_SERVER:
                    result.append(getClientToServer(fieldInfos[i], request));
                    break;
                case FieldInfo.DATA_SERVER_TO_CLIENT:
                    result.append(getServerToClient(fieldInfos[i], response));
                    break;
                case FieldInfo.DATA_SERVER_TO_RSERVER:
                    result.append('-');
                    break;
                case FieldInfo.DATA_RSERVER_TO_SERVER:
                    result.append('-');
                    break;
                case FieldInfo.DATA_APP_SPECIFIC:
                    result.append(getAppSpecific(fieldInfos[i], request));
                    break;
                case FieldInfo.DATA_SPECIAL:
                    if (FieldInfo.SPECIAL_DATE==fieldInfos[i].location)
                        result.append(dateFormatter.format(date));
                    else if (FieldInfo.SPECIAL_TIME_TAKEN==fieldInfos[i].location)
                        result.append(timeTakenFormatter.format(runTime/1000d));
                    else if (FieldInfo.SPECIAL_TIME==fieldInfos[i].location)
                        result.append(timeFormatter.format(date));
                    else if (FieldInfo.SPECIAL_BYTES==fieldInfos[i].location) {
                        int length = response.getContentCount();
                        if (length > 0)
                            result.append(length);
                        else
                            result.append("-");
                    } else if (FieldInfo.SPECIAL_CACHED==fieldInfos[i].location)
                        result.append('-'); /* I don't know how to evaluate this! */
                    else
                        result.append("?WTF?"); /* This should never happen! */
                    break;
                default:
                    result.append("?WTF?"); /* This should never happen! */
            }

            if (fieldInfos[i].postWhiteSpace!=null) {
                result.append(fieldInfos[i].postWhiteSpace);
            }
        }
        log(result.toString(), date);

    }


    /**
     * Rename the existing log file to something else. Then open the
     * old log file name up once again. Intended to be called by a JMX
     * agent.
     *
     *
     * @param newFileName The file name to move the log file entry to
     * @return true if a file was rotated with no error
     */
    public synchronized boolean rotate(String newFileName) {

        if (currentLogFile!=null) {
            File holder = currentLogFile;
            close();
            try {
                holder.renameTo(new File(newFileName));
            } catch(Throwable e){
                log.error("rotate failed", e);
            }

            /* Make sure date is correct */
            currentDate = new Date(System.currentTimeMillis());
            dateStamp = fileDateFormatter.format(currentDate);

            open();
            return true;
        } else {
            return false;
        }

    }

    // -------------------------------------------------------- Private Methods


    /**
     *  Return the client to server data.
     *  @param fieldInfo The field to decode.
     *  @param request The object we pull data from.
     *  @return The appropriate value.
     */
     private String getClientToServer(FieldInfo fieldInfo, Request request) {

        ServletRequest sr = request.getRequest();
        HttpServletRequest hsr = null;
        if (sr instanceof HttpServletRequest)
            hsr = (HttpServletRequest)sr;

        switch(fieldInfo.location) {
            case FieldInfo.FIELD_METHOD:
                return hsr.getMethod();
            case FieldInfo.FIELD_URI:
                if (null==hsr.getQueryString())
                    return hsr.getRequestURI();
                else
                    return hsr.getRequestURI() + "?" + hsr.getQueryString();
            case FieldInfo.FIELD_URI_STEM:
                return hsr.getRequestURI();
            case FieldInfo.FIELD_URI_QUERY:
                if (null==hsr.getQueryString())
                    return "-";
                return hsr.getQueryString();
            case FieldInfo.FIELD_HEADER:
                return wrap(hsr.getHeader(fieldInfo.value));
            default:
                ;
        }

        return "-";

    }


    /**
     *  Return the server to client data.
     *  @param fieldInfo The field to decode.
     *  @param response The object we pull data from.
     *  @return The appropriate value.
     */
    private String getServerToClient(FieldInfo fieldInfo, Response response) {
        HttpResponse r = (HttpResponse)response;
        switch(fieldInfo.location) {
            case FieldInfo.FIELD_STATUS:
                return "" + r.getStatus();
            case FieldInfo.FIELD_COMMENT:
                return "?"; /* Not coded yet*/
            case FieldInfo.FIELD_HEADER:
                return wrap(r.getHeader(fieldInfo.value));
            default:
                ;
        }

        return "-";

    }


    /**
     * Get app specific data.
     * @param fieldInfo The field to decode
     * @param request Where we will pull the data from.
     * @return The appropriate value
     */
    private String getAppSpecific(FieldInfo fieldInfo, Request request) {

        ServletRequest sr = request.getRequest();
        HttpServletRequest hsr = null;
        if (sr instanceof HttpServletRequest)
            hsr = (HttpServletRequest)sr;

        switch(fieldInfo.xType) {
            case FieldInfo.X_PARAMETER:
                return wrap(urlEncode(sr.getParameter(fieldInfo.value)));
            case FieldInfo.X_REQUEST:
                return wrap(sr.getAttribute(fieldInfo.value));
            case FieldInfo.X_SESSION:
                HttpSession session = null;
                if (hsr!=null){
                    session = hsr.getSession(false);
                    if (session!=null)
                        return wrap(session.getAttribute(fieldInfo.value));
                }
                break;
            case FieldInfo.X_COOKIE:
                Cookie[] c = hsr.getCookies();
                for (int i=0; c != null && i < c.length; i++){
                    if (fieldInfo.value.equals(c[i].getName())){
                        return wrap(c[i].getValue());
                    }
                 }
            case FieldInfo.X_APP:
                return wrap(request.getContext().getServletContext()
                                .getAttribute(fieldInfo.value));
            case FieldInfo.X_SERVLET_REQUEST:
                if (fieldInfo.location==FieldInfo.X_LOC_AUTHTYPE) {
                    return wrap(hsr.getAuthType());
                } else if (fieldInfo.location==FieldInfo.X_LOC_REMOTEUSER) {
                    return wrap(hsr.getRemoteUser());
                } else if (fieldInfo.location==
                            FieldInfo.X_LOC_REQUESTEDSESSIONID) {
                    return wrap(hsr.getRequestedSessionId());
                } else if (fieldInfo.location==
                            FieldInfo.X_LOC_REQUESTEDSESSIONIDFROMCOOKIE) {
                    return wrap(""+hsr.isRequestedSessionIdFromCookie());
                } else if (fieldInfo.location==
                            FieldInfo.X_LOC_REQUESTEDSESSIONIDVALID) {
                    return wrap(""+hsr.isRequestedSessionIdValid());
                } else if (fieldInfo.location==FieldInfo.X_LOC_CONTENTLENGTH) {
                    return wrap(""+hsr.getContentLength());
                } else if (fieldInfo.location==
                            FieldInfo.X_LOC_CHARACTERENCODING) {
                    return wrap(hsr.getCharacterEncoding());
                } else if (fieldInfo.location==FieldInfo.X_LOC_LOCALE) {
                    return wrap(hsr.getLocale());
                } else if (fieldInfo.location==FieldInfo.X_LOC_PROTOCOL) {
                    return wrap(hsr.getProtocol());
                } else if (fieldInfo.location==FieldInfo.X_LOC_SCHEME) {
                    return wrap(hsr.getScheme());
                } else if (fieldInfo.location==FieldInfo.X_LOC_SECURE) {
                    return wrap(""+hsr.isSecure());
                }
                break;
            default:
                ;
        }

        return "-";

    }


    /**
     *  urlEncode the given string. If null or empty, return null.
     */
    private String urlEncode(String value) {
        if (null==value || value.length()==0) {
            return null;
        }
        return URLEncoder.encode(value);
    }


    /**
     *  Wrap the incoming value into quotes and escape any inner
     *  quotes with double quotes.
     *
     *  @param value - The value to wrap quotes around
     *  @return '-' if empty of null. Otherwise, toString() will
     *     be called on the object and the value will be wrapped
     *     in quotes and any quotes will be escaped with 2
     *     sets of quotes.
     */
    private String wrap(Object value) {

        String svalue;
        // Does the value contain a " ? If so must encode it
        if (value==null || "-".equals(value))
            return "-";


        try {
            svalue = value.toString();
            if ("".equals(svalue))
                return "-";
        } catch(Throwable e){
            /* Log error */
            return "-";
        }

        /* Wrap all quotes in double quotes. */
        StringBuffer buffer = new StringBuffer(svalue.length()+2);
        buffer.append('"');
        int i=0;
        while (i<svalue.length()) {
            int j = svalue.indexOf('"', i);
            if (j==-1) {
                buffer.append(svalue.substring(i));
                i=svalue.length();
            } else {
                buffer.append(svalue.substring(i, j+1));
                buffer.append('"');
                i=j+2;
            }
        }

        buffer.append('"');
        return buffer.toString();

    }


    /**
     * Close the currently open log file (if any)
     */
    private synchronized void close() {

        if (writer == null)
            return;
        writer.flush();
        writer.close();
        writer = null;
        currentLogFile = null;

    }


    /**
     * Log the specified message to the log file, switching files if the date
     * has changed since the previous log call.
     *
     * @param message Message to be logged
     * @param date the current Date object (so this method doesn't need to
     *        create a new one)
     */
    private void log(String message, Date date) {

        if (rotatable){
            // Only do a logfile switch check once a second, max.
            long systime = System.currentTimeMillis();
            if ((systime - rotationLastChecked) > 1000) {

                // We need a new currentDate
                currentDate = new Date(systime);
                rotationLastChecked = systime;

                // Check for a change of date
                String tsDate = fileDateFormatter.format(currentDate);

                // If the date has changed, switch log files
                if (!dateStamp.equals(tsDate)) {
                    synchronized (this) {
                        if (!dateStamp.equals(tsDate)) {
                            close();
                            dateStamp = tsDate;
                            open();
                        }
                    }
                }
            }
        }

        /* In case something external rotated the file instead */
        if (checkExists){
            synchronized (this) {
                if (currentLogFile!=null && !currentLogFile.exists()) {
                    try {
                        close();
                    } catch (Throwable e){
                        log.info("at least this wasn't swallowed", e);
                    }

                    /* Make sure date is correct */
                    currentDate = new Date(System.currentTimeMillis());
                    dateStamp = fileDateFormatter.format(currentDate);

                    open();
                }
            }
        }

        // Log this message
        if (writer != null) {
            writer.println(message);
        }

    }


    /**
     * Open the new log file for the date specified by <code>dateStamp</code>.
     */
    private synchronized void open() {

        // Create the directory if necessary
        File dir = new File(directory);
        if (!dir.isAbsolute())
            dir = new File(System.getProperty("catalina.base"), directory);
        dir.mkdirs();

        // Open the current log file
        try {
            String pathname;

            // If no rotate - no need for dateStamp in fileName
            if (rotatable){
                pathname = dir.getAbsolutePath() + File.separator +
                            prefix + dateStamp + suffix;
            } else {
                pathname = dir.getAbsolutePath() + File.separator +
                            prefix + suffix;
            }

            currentLogFile = new File(pathname);
            writer = new PrintWriter(new FileWriter(pathname, true), true);
            if (currentLogFile.length()==0) {
                writer.println("#Fields: " + pattern);
                writer.println("#Version: 1.0");
                writer.println("#Software: " + ServerInfo.getServerInfo());
            }


        } catch (IOException e) {
            writer = null;
            currentLogFile = null;
        }

    }


    /**
     * This method returns a Date object that is accurate to within one
     * second.  If a thread calls this method to get a Date and it's been
     * less than 1 second since a new Date was created, this method
     * simply gives out the same Date again so that the system doesn't
     * spend time creating Date objects unnecessarily.
     */
    private Date getDate(long systime) {
        /* Avoid extra call to System.currentTimeMillis(); */
        if (0==systime) {
            systime = System.currentTimeMillis();
        }

        // Only create a new Date once per second, max.
        if ((systime - currentDate.getTime()) > 1000) {
            currentDate.setTime(systime);
        }

        return currentDate;

    }


    // ------------------------------------------------------ Lifecycle Methods


    /**
     * Add a lifecycle event listener to this component.
     *
     * @param listener The listener to add
     */
    public void addLifecycleListener(LifecycleListener listener) {

        lifecycle.addLifecycleListener(listener);

    }


    /**
     * Get the lifecycle listeners associated with this lifecycle. If this
     * Lifecycle has no listeners registered, a zero-length array is returned.
     */
    public LifecycleListener[] findLifecycleListeners() {

        return lifecycle.findLifecycleListeners();

    }


    /**
     * Remove a lifecycle event listener from this component.
     *
     * @param listener The listener to add
     */
    public void removeLifecycleListener(LifecycleListener listener) {

        lifecycle.removeLifecycleListener(listener);

    }


    /**
     * Prepare for the beginning of active use of the public methods of this
     * component.  This method should be called after <code>configure()</code>,
     * and before any of the public methods of the component are utilized.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents this component from being used
     */
    public void start() throws LifecycleException {

        // Validate and update our current component state
        if (started)
            throw new LifecycleException
                (sm.getString("extendedAccessLogValve.alreadyStarted"));
        lifecycle.fireLifecycleEvent(START_EVENT, null);
        started = true;

        // Initialize the timeZone, Date formatters, and currentDate
        TimeZone tz = TimeZone.getTimeZone("GMT");
        dateFormatter = new SimpleDateFormat("yyyy-MM-dd");
        dateFormatter.setTimeZone(tz);
        timeFormatter = new SimpleDateFormat("HH:mm:ss");
        timeFormatter.setTimeZone(tz);
        currentDate = new Date(System.currentTimeMillis());
        if (fileDateFormat==null || fileDateFormat.length()==0)
            fileDateFormat = "yyyy-MM-dd";
        fileDateFormatter = new SimpleDateFormat(fileDateFormat);
        dateStamp = fileDateFormatter.format(currentDate);
        timeTakenFormatter = new DecimalFormat("0.000");

        /* Everybody say ick ... ick */
        try {
            InetAddress inetAddress = InetAddress.getLocalHost();
            myIpAddress = inetAddress.getHostAddress();
            myDNSName = inetAddress.getHostName();
        } catch(Throwable e){
            myIpAddress="127.0.0.1";
            myDNSName="localhost";
        }

        open();

    }


    /**
     * Gracefully terminate the active use of the public methods of this
     * component.  This method should be the last one called on a given
     * instance of this component.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that needs to be reported
     */
    public void stop() throws LifecycleException {

        // Validate and update our current component state
        if (!started)
            throw new LifecycleException
                (sm.getString("extendedAccessLogValve.notStarted"));
        lifecycle.fireLifecycleEvent(STOP_EVENT, null);
        started = false;

        close();

    }


    /**
     * Decode the given pattern. Is public so a pattern may
     * allows to be validated.
     * @param fields The pattern to decode
     * @return null on error.  Otherwise array of decoded fields
     */
    public FieldInfo[] decodePattern(String fields) {

        if (log.isDebugEnabled())
            log.debug("decodePattern, fields=" + fields);

        LinkedList list = new LinkedList();

        //Ignore leading whitespace.
        int i=0;
        for (;i<fields.length() && Character.isWhitespace(fields.charAt(i));i++);

        if (i>=fields.length()) {
            log.info("fields was just empty or whitespace");
            return null;
        }

        int j;
        while(i<fields.length()) {
            if (log.isDebugEnabled())
                log.debug("fields.substring(i)=" + fields.substring(i));

            FieldInfo currentFieldInfo = new FieldInfo();


            if (fields.startsWith("date",i)) {
                currentFieldInfo.type = FieldInfo.DATA_SPECIAL;
                currentFieldInfo.location = FieldInfo.SPECIAL_DATE;
                i+="date".length();
            } else if (fields.startsWith("time-taken",i)) {
                currentFieldInfo.type = FieldInfo.DATA_SPECIAL;
                currentFieldInfo.location = FieldInfo.SPECIAL_TIME_TAKEN;
                i+="time-taken".length();
            } else if (fields.startsWith("time",i)) {
                currentFieldInfo.type = FieldInfo.DATA_SPECIAL;
                currentFieldInfo.location = FieldInfo.SPECIAL_TIME;
                i+="time".length();
            } else if (fields.startsWith("bytes",i)) {
                currentFieldInfo.type = FieldInfo.DATA_SPECIAL;
                currentFieldInfo.location = FieldInfo.SPECIAL_BYTES;
                i+="bytes".length();
            } else if (fields.startsWith("cached",i)) {
                currentFieldInfo.type = FieldInfo.DATA_SPECIAL;
                currentFieldInfo.location = FieldInfo.SPECIAL_CACHED;
                i+="cached".length();
            } else if (fields.startsWith("c-ip",i)) {
                currentFieldInfo.type = FieldInfo.DATA_CLIENT;
                currentFieldInfo.location = FieldInfo.FIELD_IP;
                i+="c-ip".length();
            } else if (fields.startsWith("c-dns",i)) {
                currentFieldInfo.type = FieldInfo.DATA_CLIENT;
                currentFieldInfo.location = FieldInfo.FIELD_DNS;
                i+="c-dns".length();
            } else if (fields.startsWith("s-ip",i)) {
                currentFieldInfo.type = FieldInfo.DATA_SERVER;
                currentFieldInfo.location = FieldInfo.FIELD_IP;
                i+="s-ip".length();
            } else if (fields.startsWith("s-dns",i)) {
                currentFieldInfo.type = FieldInfo.DATA_SERVER;
                currentFieldInfo.location = FieldInfo.FIELD_DNS;
                i+="s-dns".length();
            } else if (fields.startsWith("cs",i)) {
                i = decode(fields, i+2, currentFieldInfo,
                            FieldInfo.DATA_CLIENT_TO_SERVER);
                if (i<0)
                    return null;
            } else if (fields.startsWith("sc",i)) {
                i = decode(fields, i+2, currentFieldInfo,
                            FieldInfo.DATA_SERVER_TO_CLIENT);
                if (i<0)
                    return null;
            } else if (fields.startsWith("sr",i)) {
                i = decode(fields, i+2, currentFieldInfo,
                            FieldInfo.DATA_SERVER_TO_RSERVER);
                if (i<0)
                    return null;
            } else if (fields.startsWith("rs",i)) {
                i = decode(fields, i+2, currentFieldInfo,
                            FieldInfo.DATA_RSERVER_TO_SERVER);
                if (i<0)
                    return null;
            } else if (fields.startsWith("x",i)) {
                i = decodeAppSpecific(fields, i, currentFieldInfo);
            } else {
                // Unable to decode ...
                log.error("unable to decode with rest of chars being: " +
                            fields.substring(i));
                return null;
            }

            // By this point we should have the field, get the whitespace
            j=i;
            for (;j<fields.length() && Character.isWhitespace(fields.charAt(j));j++);

            if (j>=fields.length()) {
                if (j==i) {
                    // Special case - end of string
                    currentFieldInfo.postWhiteSpace = "";
                } else {
                    currentFieldInfo.postWhiteSpace = fields.substring(i);
                    i=j;
                }
            } else {
                currentFieldInfo.postWhiteSpace = fields.substring(i,j);
                i=j;
            }

            list.add(currentFieldInfo);
        }

        i=0;
        FieldInfo[] f = new FieldInfo[list.size()];
        for (Iterator k = list.iterator(); k.hasNext();)
             f[i++] = (FieldInfo)k.next();

        if (log.isDebugEnabled())
            log.debug("finished decoding with length of: " + i);

        return f;
    }

    /**
     * Decode the cs or sc fields.
     * Returns negative on error.
     *
     * @param fields The pattern to decode
     * @param i The string index where we are decoding.
     * @param fieldInfo Where to store the results
     * @param type The type we are decoding.
     * @return -1 on error. Otherwise the new String index.
     */
    private int decode(String fields, int i, FieldInfo fieldInfo, short type) {

        if (fields.startsWith("-status",i)) {
            fieldInfo.location = FieldInfo.FIELD_STATUS;
            i+="-status".length();
        } else if (fields.startsWith("-comment",i)) {
            fieldInfo.location = FieldInfo.FIELD_COMMENT;
            i+="-comment".length();
        } else if (fields.startsWith("-uri-query",i)) {
            fieldInfo.location = FieldInfo.FIELD_URI_QUERY;
            i+="-uri-query".length();
        } else if (fields.startsWith("-uri-stem",i)) {
            fieldInfo.location = FieldInfo.FIELD_URI_STEM;
            i+="-uri-stem".length();
        } else if (fields.startsWith("-uri",i)) {
            fieldInfo.location = FieldInfo.FIELD_URI;
            i+="-uri".length();
        } else if (fields.startsWith("-method",i)) {
            fieldInfo.location = FieldInfo.FIELD_METHOD;
            i+="-method".length();
        } else if (fields.startsWith("(",i)) {
            fieldInfo.location = FieldInfo.FIELD_HEADER;
            i++;                                  /* Move past the ( */
            int j = fields.indexOf(')', i);
            if (j==-1) {                          /* Not found */
                log.error("No closing ) found for in decode");
                return -1;
            }
            fieldInfo.value = fields.substring(i,j);
            i=j+1;                                // Move pointer past ) */
        } else {
            log.error("The next characters couldn't be decoded: " + fields.substring(i));
            return -1;
        }

        fieldInfo.type = type;
        return i;

    }


    /**
      * Decode app specific log entry.
      *
      * Special fields are of the form:
      * x-C(...) - For cookie
      * x-A(...) - Value in servletContext
      * x-S(...) - Value in session
      * x-R(...) - Value in servletRequest
      * @param fields The pattern to decode
      * @param i The string index where we are decoding.
      * @param fieldInfo Where to store the results
      * @return -1 on error. Otherwise the new String index.
      */
    private int decodeAppSpecific(String fields, int i, FieldInfo fieldInfo) {

        fieldInfo.type = FieldInfo.DATA_APP_SPECIFIC;
        /* Move past 'x-' */
        i+=2;

        if (i>=fields.length()) {
            log.error("End of line reached before decoding x- param");
            return -1;
        }

        switch(fields.charAt(i)) {
            case 'A':
                fieldInfo.xType = FieldInfo.X_APP;
                break;
            case 'C':
                fieldInfo.xType = FieldInfo.X_COOKIE;
                break;
            case 'R':
                fieldInfo.xType = FieldInfo.X_REQUEST;
                break;
            case 'S':
                fieldInfo.xType = FieldInfo.X_SESSION;
                break;
            case 'H':
                fieldInfo.xType = FieldInfo.X_SERVLET_REQUEST;
                break;
            case 'P':
                fieldInfo.xType = FieldInfo.X_PARAMETER;
                break;
            default:
                return -1;
        }

        /* test that next char is a ( */
        if (i+1!=fields.indexOf('(',i)) {
            log.error("x param in wrong format. Needs to be 'x-#(...)' read the docs!");
            return -1;
        }
        i+=2; /* Move inside of the () */

        /* Look for ending ) and return error if not found. */
        int j = fields.indexOf(')',i);
        if (j==-1) {
            log.error("x param in wrong format. No closing ')'!");
            return -1;
        }

        fieldInfo.value = fields.substring(i,j);

        if (fieldInfo.xType == FieldInfo.X_SERVLET_REQUEST) {
            if ("authType".equals(fieldInfo.value)){
                fieldInfo.location = FieldInfo.X_LOC_AUTHTYPE;
            } else if ("remoteUser".equals(fieldInfo.value)){
                fieldInfo.location = FieldInfo.X_LOC_REMOTEUSER;
            } else if ("requestedSessionId".equals(fieldInfo.value)){
                fieldInfo.location = FieldInfo.X_LOC_REQUESTEDSESSIONID;
            } else if ("requestedSessionIdFromCookie".equals(fieldInfo.value)){
                fieldInfo.location = FieldInfo.X_LOC_REQUESTEDSESSIONIDFROMCOOKIE;
            } else if ("requestedSessionIdValid".equals(fieldInfo.value)){
                fieldInfo.location = FieldInfo.X_LOC_REQUESTEDSESSIONID;
            } else if ("contentLength".equals(fieldInfo.value)){
                fieldInfo.location = FieldInfo.X_LOC_CONTENTLENGTH;
            } else if ("characterEncoding".equals(fieldInfo.value)){
                fieldInfo.location = FieldInfo.X_LOC_CHARACTERENCODING;
            } else if ("locale".equals(fieldInfo.value)){
                fieldInfo.location = FieldInfo.X_LOC_LOCALE;
            } else if ("protocol".equals(fieldInfo.value)){
                fieldInfo.location = FieldInfo.X_LOC_PROTOCOL;
            } else if ("scheme".equals(fieldInfo.value)){
                fieldInfo.location = FieldInfo.X_LOC_SCHEME;
            } else if ("secure".equals(fieldInfo.value)){
                fieldInfo.location = FieldInfo.X_LOC_SECURE;
            } else {
                log.error("x param for servlet request, couldn't decode value: " +
                            fieldInfo.location);
                return -1;
            }
        }

        return j+1;

    }


}

/**
 * A simple helper for decoding the pattern.
 */
class FieldInfo {
    /*
       The goal of the constants listed below is to make the construction of the log
       entry as quick as possible via numerci decodings of the methods to call instead
       of performing many String comparisons on each logging request.
    */

    /* Where the data is located. */
    static final short DATA_CLIENT = 0;
    static final short DATA_SERVER = 1;
    static final short DATA_REMOTE = 2;
    static final short DATA_CLIENT_TO_SERVER = 3;
    static final short DATA_SERVER_TO_CLIENT = 4;
    static final short DATA_SERVER_TO_RSERVER = 5; /* Here to honor the spec. */
    static final short DATA_RSERVER_TO_SERVER = 6; /* Here to honor the spec. */
    static final short DATA_APP_SPECIFIC = 7;
    static final short DATA_SPECIAL = 8;

    /* The type of special fields. */
    static final short SPECIAL_DATE         = 1;
    static final short SPECIAL_TIME_TAKEN   = 2;
    static final short SPECIAL_TIME         = 3;
    static final short SPECIAL_BYTES        = 4;
    static final short SPECIAL_CACHED       = 5;

    /* Where to pull the data for prefixed values */
    static final short FIELD_IP            = 1;
    static final short FIELD_DNS           = 2;
    static final short FIELD_STATUS        = 3;
    static final short FIELD_COMMENT       = 4;
    static final short FIELD_METHOD        = 5;
    static final short FIELD_URI           = 6;
    static final short FIELD_URI_STEM      = 7;
    static final short FIELD_URI_QUERY     = 8;
    static final short FIELD_HEADER        = 9;


    /* Application Specific parameters */
    static final short X_REQUEST = 1; /* For x app specific */
    static final short X_SESSION = 2; /* For x app specific */
    static final short X_COOKIE  = 3; /* For x app specific */
    static final short X_APP     = 4; /* For x app specific */
    static final short X_SERVLET_REQUEST = 5; /* For x app specific */
    static final short X_PARAMETER = 6; /* For x app specific */

    static final short X_LOC_AUTHTYPE                       = 1;
    static final short X_LOC_REMOTEUSER                     = 2;
    static final short X_LOC_REQUESTEDSESSIONID             = 3;
    static final short X_LOC_REQUESTEDSESSIONIDFROMCOOKIE   = 4;
    static final short X_LOC_REQUESTEDSESSIONIDVALID        = 5;
    static final short X_LOC_CONTENTLENGTH                  = 6;
    static final short X_LOC_CHARACTERENCODING              = 7;
    static final short X_LOC_LOCALE                         = 8;
    static final short X_LOC_PROTOCOL                       = 9;
    static final short X_LOC_SCHEME                         = 10;
    static final short X_LOC_SECURE                         = 11;



    /** The field type */
    short type;

    /** Where to pull the data from? Icky variable name. */
    short location;

    /** The x- specific place to pull the data from. */
    short xType;

    /** The field value if needed. Needed for headers and app specific. */
    String value;

    /** Any white space after this field? Put it here. */
    String postWhiteSpace = null;

}
