/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the License).  You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * Header Notice in each file and include the License file 
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.  
 * If applicable, add the following below the CDDL Header, 
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information: 
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 */

package com.sun.enterprise.web;


import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.FileOutputStream;

import java.net.InetAddress;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.BufferOverflowException;
import java.nio.channels.FileChannel;
    
import java.text.SimpleDateFormat;
import java.text.DecimalFormat;

import java.util.Date;
import java.util.LinkedList;
import java.util.TimeZone;
import java.util.logging.Logger;
import java.util.logging.Level;

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

import org.apache.catalina.Container;
import org.apache.catalina.HttpResponse;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Request;
import org.apache.catalina.Response;
import org.apache.catalina.util.LifecycleSupport;
import org.apache.catalina.util.StringManager;
import org.apache.catalina.valves.ValveBase;

import com.sun.logging.LogDomains;

/**
 * <p>Implementation of the <b>Valve</b> interface that generates a web server
 * access log with the detailed line contents matching a configurable pattern.
 * The syntax of the available patterns is similar to that supported by the
 * Apache <code>mod_log_config</code> module.  As an additional feature,
 * automatic rollover of log files at a specified interval is also supported.
 *
 * </p>This class uses a direct <code>ByteBuffer</code> to store and write 
 * logs. 
 *
 * @author Jean-Francois Arcand
 * @author Charlie J. Hunt
 */

public final class PEAccessLogValve
    extends ValveBase
    implements Lifecycle, Runnable {

    private static final Logger _logger =
        LogDomains.getLogger(LogDomains.WEB_LOGGER);

    /**
     * Name of the system property whose value specifies the max number of
     * access log history files to keep.
     * If this property has been specified without any value, a default value
     * of 10 is used.
     * Else, if it has been specified with a value of 0, no access log
     * history files will be maintained, and the current access log file will
     * be reset after each rotation.
     * If undefined, all access log history files will be preserved.
     */
    private static final String LOGGING_MAX_HISTORY_FILES = 
        "com.sun.enterprise.server.logging.max_history_files";

    /*
     * HTTP header names
     */
    private static final String HTTP_HEADER_ACCEPT = "Accept";
    private static final String HTTP_HEADER_AUTHORIZATION = "Authorization";
    private static final String HTTP_HEADER_DATE = "Date";
    private static final String HTTP_HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";

    /*
     * Supported access log entry tokens
     */
    private static final String AUTH_USER_NAME = "auth-user-name";
    private static final String CLIENT_DNS = "client.dns";
    private static final String CLIENT_NAME = "client.name";
    private static final String COOKIE_VALUE = "cookie.value";
    private static final String DATE_TIME = "datetime";
    private static final String HEADER_ACCEPT = "header.accept";
    private static final String HEADER_ANY = "header.";
    private static final int HEADER_ANY_LEN = HEADER_ANY.length();
    private static final String HEADER_AUTH = "header.auth";
    private static final String HEADER_DATE = "header.date";
    private static final String HEADER_IF_MOD_SINCE = "header.if-mod-since";
    private static final String HEADER_USER_AGENT = "header.user-agent";
    private static final String HEADER_REFERER = "header.referer";
    private static final String HTTP_METHOD = "http-method";
    private static final String HTTP_URI = "http-uri";
    private static final String HTTP_VERSION = "http-version";
    private static final String QUERY_STR = "query-str";
    private static final String REFERER = "referer";
    private static final String REQUEST = "request";
    private static final String RESPONSE_LENGTH = "response.length";
    private static final String STATUS = "status";
    private static final String USER_AGENT = "user.agent";
    private static final String VS_ID = "vs.id";


    // ----------------------------------------------------- Instance Variables


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


    /**
     * The descriptive information about this implementation.
     */
    protected static final String info =
        "com.sun.enterprise.web.PEAccessLogValve/1.0";


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


    /**
     * The set of month abbreviations for log messages.
     */
    protected static final String months[] =
    { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
      "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };


    /**
     * 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 = "";


    /**
     * Should we rotate our log file?
     */
    private boolean rotatable;


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


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


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


    /**
     * If prefix ends in '.', and suffix starts with '.', we must remove the
     * leading '.' from suffix when prefix and suffix are concatenated.
     */
    private boolean removeLeadingDotFromSuffix = false;


    /**
     * Suffix from which leading '.' has been removed if
     * removeLeadingDotFromSuffix is true
     */
    private String dotLessSuffix = 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 Dates into a day string in the format
     * "dd".
     */
    private SimpleDateFormat dayFormatter = null;


    /**
     * A date formatter to format a Date into a month string in the format
     * "MM".
     */
    private SimpleDateFormat monthFormatter = null;


    /**
     * A date formatter to format a Date into a year string in the format
     * "yyyy".
     */
    private SimpleDateFormat yearFormatter = 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;


    /**
     * The time zone relative to GMT.
     */
    private String timeZone = null;


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


    /**
     * When formatting log lines, we often use strings like this one (" ").
     */
    private String space = " ";


    /**
     * Resolve hosts.
     */
    private boolean resolveHosts = false;


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


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


    /**
     * Date format to place in log file name. Use at your own risk!
     */
    private String fileDateFormat = null;
    
    
    /**
     * The <code>FileChannel</code> used to write the access log.
     */
    protected FileChannel fileChannel;
    
    
    /**
     * The stream used to store the logs.
     */
    FileOutputStream fos;
    
    /**
     * The interval (in seconds) between writing the logs
     */
    private int writeInterval;

   
    /**
     * The background writerThread.
     */
    private Thread writerThread = null;   
    
    
    /**
     * The background writerThread completion semaphore.
     */
    private boolean threadDone = false;

    
    /**
     * The <code>CharBuffer</code> used to store the logs.
     */
    private CharBuffer charBuffer;


    /**
     * The size of the last bytes written to the <code>CharBuffer</code>. This
     * is used to predict and avoid BufferOverflow when writting to the i
     * <code>CharBuffer</code>
     */
    public int chunkSize = 0;
   
    
    /**
     * The <code>byteBuffer</code> used to store the log.
     */
    protected int bufferSize = 512 * 1024;
    private int expandedBufferSize;
    
    
    /**
     * If the writer interval is equals to zero, then always flush the 
     * direct byte buffer after every request.
     */
    protected boolean flushRealTime = false;
    

    /**
     * Are we supposed to add datestamp to first access log file we create,
     * or only after first rotation?
     *
     * If set to false, the current access log file will never have any
     * date stamp: It will be moved to a date-stamped file upon rotation
     */
    private boolean addDateStampToFirstAccessLogFile;


    /**
     * List of access log pattern components
     */
    private LinkedList<String> patternComponents;


    /**
     * The current access log file.
     */
    private File logFile;


    /**
     * The maximum number of access log history files to keep
     */
    private int maxHistoryFiles;


    /**
     * True if no access log history files are to be kept, false otherwise
     */
    private boolean deleteAllHistoryFiles;


    /**
     * List of most recent access log history files (the size of this list
     * is not to exceed <code>maxHistoryFiles</code>)
     */
    private LinkedList<File> historyFiles;


    /**
     * Return writerThread interval (seconds)
     */
    public int getWriterInterval() {        
        return writeInterval;       
    }

        
    /**
     * Set writerthread interval (seconds)
     */
    public void setWriterInterval(int t) {
        writeInterval = t;
    }
    
    
    /**
     * Set the direct <code>ByteBuffer</code> size
     */
    public void setBufferSize(int size){
        bufferSize = size;
    }
    
    /**
     * Return the direct <code>ByteBuffer</code> size
     */    
    public int getBufferSize(){
        return bufferSize;
    }

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


    /**
     * Are we supposed to add datestamp to first access log file we create,
     * or only starting with first rotation?
     */
    public void setAddDateStampToFirstAccessLogFile(boolean add) {
        this.addDateStampToFirstAccessLogFile = add;
    }


    /**
     * 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 (this.info);

    }


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

        return (this.pattern);

    }


    /**
     * Set the format pattern, first translating any recognized alias.
     *
     * @param p The new pattern
     */
    public void setPattern(String p) {
        this.pattern = p;
        this.patternComponents = parsePattern();
    }


    /**
     * 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 p) {

        prefix = p;

        if (prefix != null && suffix != null && prefix.endsWith(".")
                && suffix.startsWith(".")) {
            removeLeadingDotFromSuffix = true;
            dotLessSuffix = suffix.substring(1);
        } else {
            removeLeadingDotFromSuffix = false;
        }
    }


    /**
     * Should we rotate the logs
     */
    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 s) {

        suffix = s;

        if (prefix != null && suffix != null && prefix.endsWith(".")
                && suffix.startsWith(".")) {
            removeLeadingDotFromSuffix = true;
            dotLessSuffix = suffix.substring(1);
        } else {
            removeLeadingDotFromSuffix = false;
        }
    }


    /**
     * Set the resolve hosts flag.
     *
     * @param resolveHosts The new resolve hosts value
     */
    public void setResolveHosts(boolean resolveHosts) {

        this.resolveHosts = resolveHosts;

    }


    /**
     * Get the value of the resolve hosts flag.
     */
    public boolean isResolveHosts() {

        return resolveHosts;

    }


    /**
     * 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;

    }

    /**
     *  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 int invoke(Request request, Response response)
            throws IOException, ServletException {
        return INVOKE_NEXT;
    }

   
    public synchronized void postInvoke(Request request, Response response){
 
        if (condition!=null &&
                null!=request.getRequest().getAttribute(condition)) {
             return;

        }

        int lastPosition = charBuffer.position();

        // Reset properly the buffer in case of an unexpected
        // exception.
        if (charBuffer.position() == charBuffer.limit()){
            charBuffer.limit(charBuffer.capacity());
        }    

        boolean flushBuffer = !charBuffer.hasRemaining();  

        // Try to predict if the bufer has enough place.
        if ( (chunkSize + charBuffer.position()) > charBuffer.limit()){
            flushBuffer = true;
        }           

        if ( !flushBuffer ){  

            try{
                createLogEntry(request, response, patternComponents);
                // Last chunck size used to predict if the buffer
                // will soon be full.
                chunkSize = charBuffer.position() - lastPosition;
            } catch (BufferOverflowException ex ){
                // Will happen if the current chunk is larger than
                // the previous one, or if the buffer size is too small to
                // hold a single access log entry
                flushBuffer = true;
                charBuffer.position(lastPosition);
            }
        }

        /*
         * Flush the buffer if it is full or we're logging in real time.
         * We also flush if we had to expand the buffer (see below), and
         * the buffer content is larger than the buffer size that was 
         * originally configured, so in essence we pretend that the original
         * buffer size is still effective.
         */
        if ( flushBuffer || flushRealTime
                || (charBuffer.position() - lastPosition > bufferSize)) {
            log();
        }

        if (flushBuffer) {
            // We've flushed our buffer to make room for the current request.
            // Now process the current request
            boolean expand = true;
            while (expand) {
                try {
                    createLogEntry(request, response, patternComponents);
                    expand = false;
                    log();
                 } catch (BufferOverflowException ex) {                 
                    // Even after flushing, the current buffer size is too
                    // small to handle the single log entry corresponding to
                    // the current request. Double the buffer size
                    if (!flushRealTime) {
                        _logger.log(
                            Level.WARNING,
                            "peaccesslogvalve.bufferSizeTooSmall",
                            new Object[] {
                                Integer.toString(expandedBufferSize) });
                    }
                    expandedBufferSize *= 2;
                    charBuffer = CharBuffer.allocate(expandedBufferSize);   
                }
            }
        }

    }


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


    private void createLogEntry(Request request,
                                Response response,
                                LinkedList<String> patternComponents) {

        ServletRequest req = request.getRequest();
        HttpServletRequest hreq = (HttpServletRequest) req;

        for (int i=0; i<patternComponents.size(); i++) {
            String pc = patternComponents.get(i);
            if (AUTH_USER_NAME.equals(pc)) {
                appendAuthUserName(charBuffer, hreq);
            } else if (CLIENT_DNS.equals(pc)) {
                appendClientDNS(charBuffer, req);
            } else if (CLIENT_NAME.equals(pc)) {
                appendClientName(charBuffer, req);
            } else if (COOKIE_VALUE.equals(pc)) {
                appendCookieValue(charBuffer, hreq);
            } else if (DATE_TIME.equals(pc)) {
                appendCurrentDate(charBuffer);       
            } else if (HEADER_ACCEPT.equals(pc)) {
                appendHeaderAccept(charBuffer, hreq);
            } else if (HEADER_AUTH.equals(pc)) {
                appendHeaderAuth(charBuffer, hreq);
            } else if (HEADER_DATE.equals(pc)) {
                appendHeaderDate(charBuffer, hreq);
            } else if (HEADER_IF_MOD_SINCE.equals(pc)) {
                appendHeaderIfModSince(charBuffer, hreq);
            } else if (HEADER_USER_AGENT.equals(pc)) {
                appendUserAgent(charBuffer, hreq);
            } else if (HEADER_REFERER.equals(pc)) {
                appendReferer(charBuffer, hreq);
            } else if (HTTP_METHOD.equals(pc)) {
                appendHTTPMethod(charBuffer, hreq);
            } else if (HTTP_URI.equals(pc)) {
                appendHTTPUri(charBuffer, hreq);
            } else if (HTTP_VERSION.equals(pc)) {
                appendHTTPVersion(charBuffer, hreq);
            } else if (QUERY_STR.equals(pc)) {
                appendQueryString(charBuffer, hreq);
            } else if (REFERER.equals(pc)) {
                appendReferer(charBuffer, hreq);
            } else if (REQUEST.equals(pc)) {
                appendRequestInfo(charBuffer, hreq);
            } else if (RESPONSE_LENGTH.equals(pc)) {
                appendResponseLength(charBuffer, response);
            } else if (STATUS.equals(pc)) {
                appendResponseStatus(charBuffer, response);
            } else if (USER_AGENT.equals(pc)) {
                appendUserAgent(charBuffer, hreq);
            } else if (VS_ID.equals(pc)) {
                appendVirtualServerId(charBuffer);
            } else if (pc.startsWith(HEADER_ANY)) {
                appendHeaderAny(charBuffer, pc.substring(HEADER_ANY_LEN),
                                hreq);
            }

            charBuffer.put(space);
        }

        charBuffer.put("\n");
    }


    /*
     * Parses the access log pattern (that was specified via setPattern) into
     * its individual components, and returns them as a list.
     *
     * @return List containing the access log pattern components
     */
    private LinkedList<String> parsePattern() {

        LinkedList list = new LinkedList();

        int from = 0;
        int end = -1;
        int index = -1;
        while ((index = pattern.indexOf('%', from)) >= 0) {
            end = pattern.indexOf('%', index+1);
            if (end < 0) {
                _logger.log(
                    Level.SEVERE,
                    "peaccesslogvalve.missingAccessLogPatternEndDelimiter",
                    pattern);
            }
            String component = pattern.substring(index+1, end);

            if (!AUTH_USER_NAME.equals(component) 
                    && !CLIENT_DNS.equals(component) 
                    && !CLIENT_NAME.equals(component) 
                    && !COOKIE_VALUE.equals(component) 
                    && !DATE_TIME.equals(component) 
                    && !HEADER_ACCEPT.equals(component) 
                    && !HEADER_AUTH.equals(component) 
                    && !HEADER_DATE.equals(component) 
                    && !HEADER_IF_MOD_SINCE.equals(component) 
                    && !HEADER_USER_AGENT.equals(component) 
                    && !HEADER_REFERER.equals(component) 
                    && !HTTP_METHOD.equals(component) 
                    && !HTTP_URI.equals(component) 
                    && !HTTP_VERSION.equals(component) 
                    && !QUERY_STR.equals(component) 
                    && !REFERER.equals(component) 
                    && !REQUEST.equals(component) 
                    && !RESPONSE_LENGTH.equals(component) 
                    && !STATUS.equals(component) 
                    && !USER_AGENT.equals(component) 
                    && !VS_ID.equals(component)
                    && !component.startsWith(HEADER_ANY)) {
                _logger.log(
                    Level.SEVERE,
                    "peaccesslogvalve.invalidAccessLogPatternComponent",
                    new Object[] { component, pattern });
            }

            list.add(component);
            from = end + 1;    
        }

        return list;
    }


    /*
     * Appends the client host name of the given request to the given char
     * buffer.
     */
    private void appendClientName(CharBuffer cb, ServletRequest req) {
        cb.put("\"");
        String value = req.getRemoteHost();
        if (value == null) {
            value = "NULL-CLIENT-NAME";
        }
        cb.put(value);
        cb.put("\"");
    }


    /*
     * Appends the client DNS of the given request to the given char
     * buffer.
     */
    private void appendClientDNS(CharBuffer cb, ServletRequest req) {
        cb.put("\"");
        String value = req.getRemoteAddr();
        if (value == null) {
            value = "NULL-CLIENT-DNS";
        }
        cb.put(value);
        cb.put("\"");
    }


    /*
     * Appends the authenticated user (if any) to the given char buffer.
     */
    private void appendAuthUserName(CharBuffer cb, HttpServletRequest hreq) {
        cb.put("\"");
        String user = hreq.getRemoteUser();
        if (user == null) {
            user = "NULL-AUTH-USER";
        }
        cb.put(user);
        cb.put("\"");
    }


    /*
     * Appends the current date to the given char buffer.
     */
    private void appendCurrentDate(CharBuffer cb) {
        cb.put("\"");
        Date date = getDate();
        cb.put(dayFormatter.format(date));           // Day
        cb.put('/');
        cb.put(lookup(monthFormatter.format(date))); // Month
        cb.put('/');
        cb.put(yearFormatter.format(date));          // Year
        cb.put(':');
        cb.put(timeFormatter.format(date));          // Time
        cb.put(space);
        cb.put(timeZone);                            // Time Zone
        cb.put("\"");
    }


    /*
     * Appends info about the given request to the given char buffer.
     */
    private void appendRequestInfo(CharBuffer cb, HttpServletRequest hreq) {
        cb.put("\"");
        cb.put(hreq.getMethod());
        cb.put(space);
        cb.put(hreq.getRequestURI());
        if (hreq.getQueryString() != null) {
            cb.put('?');
            cb.put(hreq.getQueryString());
        }
        cb.put(space);
        cb.put(hreq.getProtocol());
        cb.put("\"");
    }


    /*
     * Appends the response status to the given char buffer.
     */
    private void appendResponseStatus(CharBuffer cb, Response response) {
        cb.put(String.valueOf(((HttpResponse) response).getStatus()));
    }


    /*
     * Appends the content length of the given response to the given char
     * buffer.
     */
    private void appendResponseLength(CharBuffer cb, Response response) {
        cb.put("" + response.getContentCount());
    }


    /*
     * Appends the value of the 'user-agent' header of the given request to
     * the given char buffer.
     */
    private void appendUserAgent(CharBuffer cb, HttpServletRequest hreq) {
        cb.put("\"");
        String ua = hreq.getHeader("user-agent");
        if (ua == null) {
            ua = "NULL-USER-AGENT";
        }
        cb.put(ua);
        cb.put("\"");
    }


    /*
     * Appends the value of the 'referer' header of the given request to
     * the given char buffer.
     */
    private void appendReferer(CharBuffer cb, HttpServletRequest hreq) {
        cb.put("\"");
        String referer = hreq.getHeader("referer");
        if (referer == null) {
            referer = "NULL-REFERER";
        }
        cb.put(referer);
        cb.put("\"");
    }


    /*
     * Appends the Accept header of the given request to the given char
     * buffer.
     */
    private void appendHeaderAccept(CharBuffer cb, HttpServletRequest hreq) {
        cb.put("\"");
        String accept = hreq.getHeader(HTTP_HEADER_ACCEPT);
        if (accept == null) {
            accept = "NULL-HEADER-ACCEPT";
        }
        cb.put(accept);
        cb.put("\"");
    }


    /*
     * Appends the Authorization header of the given request to the given char
     * buffer.
     */
    private void appendHeaderAuth(CharBuffer cb, HttpServletRequest hreq) {
        cb.put("\"");
        String auth = hreq.getHeader(HTTP_HEADER_AUTHORIZATION);
        if (auth == null) {
            auth = "NULL-HEADER-AUTHORIZATION";
        }
        cb.put(auth);
        cb.put("\"");
    }


    /*
     * Appends the Date header of the given request to the given char buffer.
     */
    private void appendHeaderDate(CharBuffer cb, HttpServletRequest hreq) {
        cb.put("\"");
        String date = hreq.getHeader(HTTP_HEADER_DATE);
        if (date == null) {
            date = "NULL-HEADER-DATE";
        }
        cb.put(date);
        cb.put("\"");
    }


    /*
     * Appends the If-Modified-Since header of the given request to the
     * given char buffer.
     */
    private void appendHeaderIfModSince(CharBuffer cb,
                                        HttpServletRequest hreq) {
        cb.put("\"");
        String ifModSince = hreq.getHeader(HTTP_HEADER_IF_MODIFIED_SINCE);
        if (ifModSince == null) {
            ifModSince = "NULL-HEADER-IF-MODIFIED-SINCE";
        }
        cb.put(ifModSince);
        cb.put("\"");
    }


    /*
     * Appends the value of the specified header name of the given request
     * to the given char buffer.
     */
    private void appendHeaderAny(CharBuffer cb,
                                 String headerName,
                                 HttpServletRequest hreq) {
        cb.put("\"");
        String value = hreq.getHeader(headerName);
        if (value == null) {
            value = "NULL-HEADER-" + headerName.toUpperCase();
        }
        cb.put(value);
        cb.put("\"");
    }


    /*
     * Appends the value of the first cookie of the given request to the
     * given char buffer.
     */
    private void appendCookieValue(CharBuffer cb, HttpServletRequest hreq) {
        cb.put("\"");
        String cookieValue = "NULL-COOKIE-VALUE";
        Cookie[] cookies = hreq.getCookies();
        if (cookies != null && cookies.length > 0) {
            cookieValue = cookies[0].getValue();
        }
        cb.put(cookieValue);
        cb.put("\"");
    }


    /*
     * Appends the HTTP method of the given request to the given char buffer.
     */
    private void appendHTTPMethod(CharBuffer cb, HttpServletRequest hreq) {
        cb.put("\"");
        String method = hreq.getMethod();
        if (method == null) {
            method = "NULL-HTTP-METHOD";
        }
        cb.put(method);
        cb.put("\"");
    }


    /*
     * Appends the URI of the given request to the given char buffer.
     */
    private void appendHTTPUri(CharBuffer cb, HttpServletRequest hreq) {
        cb.put("\"");
        String uri = hreq.getRequestURI();
        if (uri == null) {
            uri = "NULL-HTTP-URI";
        }
        cb.put(uri);
        cb.put("\"");
    }


    /*
     * Appends the HTTP protocol version of the given request to the given
     * char buffer.
     */
    private void appendHTTPVersion(CharBuffer cb, HttpServletRequest hreq) {
        cb.put("\"");
        String protocol = hreq.getProtocol();
        if (protocol == null) {
            protocol = "NULL-HTTP-PROTOCOL";
        }
        cb.put(protocol);
        cb.put("\"");
    }


    /*
     * Appends the query string of the given request to the given char buffer.
     */
    private void appendQueryString(CharBuffer cb, HttpServletRequest hreq) {
        cb.put("\"");
        String query = hreq.getQueryString();
        if (query == null) {
            query = "NULL-QUERY";
        }
        cb.put(query);
        cb.put("\"");
    }


    /*
     * Appends the id of the virtual server with which this access log valve
     * has been associated to the given char buffer.
     */
    private void appendVirtualServerId(CharBuffer cb) {
        String vsId = "NULL-VIRTUAL-SERVER";
        Container cont = getContainer();
        if (cont != null) {
            vsId = cont.getName();
        }
        cb.put(vsId);
    }


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

        try{            
            // Make sure the byteBuffer is clean
            log();
            fileChannel.close();
            fos.close();
        } catch (IOException ex){
            ;
        }
    }


    /**
     * 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)
     */
    public void log() {
        
        if (rotatable){

            long systime = System.currentTimeMillis();
            if ((systime-lastAccessLogCreationTime) > (writeInterval*1000)) {
                synchronized (this) {
                    systime = System.currentTimeMillis();
                    if ((systime-lastAccessLogCreationTime) >
                                                    (writeInterval*1000)) {

                        // Rotate only if the formatted datestamps are
                        // different
                        String lastDateStamp = dateFormatter.format(
                            new Date(lastAccessLogCreationTime));
                        String newDateStamp = dateFormatter.format(
                            new Date(systime));

                        lastAccessLogCreationTime = systime;

                        if (!lastDateStamp.equals(newDateStamp)) {
                            close();
                            open(newDateStamp, false);
                        }
                    }
                }
            }
        }
        
        try{
            charBuffer.flip();
            ByteBuffer byteBuffer =
                ByteBuffer.wrap(charBuffer.toString().getBytes());
            while (byteBuffer.hasRemaining()){
                fileChannel.write(byteBuffer);
            }
            charBuffer.clear();
        } catch (IOException ex){
            ;
        }

    }


    /**
     * Return the month abbreviation for the specified month, which must
     * be a two-digit String.
     *
     * @param month Month number ("01" .. "12").
     */
    private String lookup(String month) {

        int index;
        try {
            index = Integer.parseInt(month) - 1;
        } catch (Throwable t) {
            index = 0;  // Can not happen, in theory
        }
        return (months[index]);

    }

    
    /**
     * Open new access log file.
     *
     * @param dateStamp The date stamp of the new access log file (if log
     * rotation has been enabled)
     * @param firstAccessLogFile true if we are creating our first access log
     * file, and false if we have rotated
     */
    private synchronized void open(String dateStamp,
                                   boolean firstAccessLogFile) {
        
        // 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 && addDateStampToFirstAccessLogFile) {
                pathname = dir.getAbsolutePath() + File.separator +
                            prefix + dateStamp + suffix;
            } else {
                if (removeLeadingDotFromSuffix) {
                    pathname = dir.getAbsolutePath() + File.separator +
                               prefix + dotLessSuffix;
                } else {
                    pathname = dir.getAbsolutePath() + File.separator +
                               prefix + suffix;
                }
            }
            
            if (rotatable
                    && !addDateStampToFirstAccessLogFile
                    && !firstAccessLogFile) {
                // Move current access log file, which has no date stamp,
                // to date-stamped file
                String dateStampedPathname = dir.getAbsolutePath()
                                        + File.separator
                                        + prefix + dateStamp + suffix;
                File renameToFile = new File(dateStampedPathname);
                if (!logFile.renameTo(renameToFile)) {
                    _logger.log(
                        Level.WARNING,
                        "peaccesslogvalve.unableToRenameLogFile",
                        new Object[] {
                            logFile.toString(), dateStampedPathname });
                }
                File removeFile = null;
                if (deleteAllHistoryFiles) {
                    removeFile = renameToFile;
                } else {
                    if (historyFiles != null) {
                        historyFiles.addLast(renameToFile);
                        if (historyFiles.size() > maxHistoryFiles) {
                            removeFile = historyFiles.removeFirst();
                        }
                    }
                }
                if (removeFile != null && !removeFile.delete()) {
                    _logger.log(Level.WARNING,
                                "peaccesslogvalve.unableToRemoveLogFile",
                                removeFile.toString());
                }
            }

            // Open the file and then get a channel from the stream
            logFile = new File(pathname);
            fos = new FileOutputStream(logFile, true);
            fileChannel = fos.getChannel();

        } catch (IOException e) {
            try{
                if ( fileChannel != null )
                    fileChannel.close();
            } catch (IOException ex){
                ;
            }
        } 

    }

    
    /**
     * This method returns a Date object that is accurate to within one
     * second.  If a writerThread calls this method to get a Date and it's been
     * less than 5 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 synchronized Date getDate() {

        // Only create a new Date once per second, max.
        long systime = System.currentTimeMillis();
        if ((systime - currentDate.getTime()) > 5000) {
            currentDate = new Date(systime);
        }

        return currentDate;

    }


    private String calculateTimeZoneOffset(long offset) {
        StringBuffer tz = new StringBuffer();
        if ((offset<0))  {
            tz.append("-");
            offset = -offset;
        } else {
            tz.append("+");
        }

        long hourOffset = offset/(1000*60*60);
        long minuteOffset = (offset/(1000*60)) % 60;

        if (hourOffset<10)
            tz.append("0");
        tz.append(hourOffset);

        if (minuteOffset<10)
            tz.append("0");
        tz.append(minuteOffset);

        return tz.toString();
    }


    // ------------------------------------------------------ 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("accessLogValve.alreadyStarted"));
        }

        lifecycle.fireLifecycleEvent(START_EVENT, null);
        started = true;

        deleteAllHistoryFiles = false;
        historyFiles = null;
        String prop = System.getProperty(LOGGING_MAX_HISTORY_FILES);
        if (prop != null) {
            maxHistoryFiles = 10;
            if (!"".equals(prop)) {
                try {
                    maxHistoryFiles = Integer.parseInt(prop);
                } catch (NumberFormatException e) {};
            }
	    if (maxHistoryFiles == 0) {
                deleteAllHistoryFiles = true;
            } else if (maxHistoryFiles > 0) {
                historyFiles = new LinkedList<File>();
            }
        }
    
        flushRealTime =
            (writeInterval <= 0 || bufferSize <= 0)? true : false;

        expandedBufferSize = bufferSize;
        if (bufferSize <= 0) {
            // We need to expand (double) the buffer size as needed.
            // However, null times two is still null.
            expandedBufferSize = 1;
        }

        charBuffer = CharBuffer.allocate(expandedBufferSize);

        // Initialize the timeZone, Date formatters, and currentDate
        TimeZone tz = TimeZone.getDefault();
        timeZone = calculateTimeZoneOffset(tz.getRawOffset());

        if (fileDateFormat==null || fileDateFormat.length()==0)
            fileDateFormat = "yyyy-MM-dd";
        dateFormatter = new SimpleDateFormat(fileDateFormat);
        dateFormatter.setTimeZone(tz);
        dayFormatter = new SimpleDateFormat("dd");
        dayFormatter.setTimeZone(tz);
        monthFormatter = new SimpleDateFormat("MM");
        monthFormatter.setTimeZone(tz);
        yearFormatter = new SimpleDateFormat("yyyy");
        yearFormatter.setTimeZone(tz);
        timeFormatter = new SimpleDateFormat("HH:mm:ss");
        timeFormatter.setTimeZone(tz);

        long systime = System.currentTimeMillis();
        currentDate = new Date(systime);
        open(dateFormatter.format(currentDate), true);
        lastAccessLogCreationTime = systime;

        if (!flushRealTime){
            // Start the background writer writerThread
            threadStart();
        }
   }


    /**
     * 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("accessLogValve.notStarted"));
        lifecycle.fireLifecycleEvent(STOP_EVENT, null);
        started = false;
        
        if (!flushRealTime){
            // Stop the background writer thread
            threadStop();
        }
        
        close();

    }

    
   /**
     * The background writerThread that checks for write the log.
     */
    public void run() {

        // Loop until the termination semaphore is set
        while (!threadDone) {
            threadSleep();
            log();
        }

    }
    
    
    /**
     * Sleep for the duration specified by the <code>writeInterval</code>
     * property.
     */
    private void threadSleep() {

        try {
            writerThread.sleep(writeInterval * 1000L);
        } catch (InterruptedException e) {
            ;
        }

    }

        
   /**
     * Start the background writerThread that will periodically write access log
     */
    private void threadStart() {

        if (writerThread != null)
            return;

        threadDone = false;
        String threadName = "AccessLogWriter";
        writerThread = new Thread(this, threadName);
        writerThread.setDaemon(true);
        writerThread.start();

    }


    /**
     * Stop the background writerThread that is periodically write logs
     */
    private void threadStop() {

        if (writerThread == null)
            return;

        threadDone = true;
        writerThread.interrupt();
        try {
            writerThread.join();
        } catch (InterruptedException e) {
            ;
        }

        writerThread = null;

    }
}
