/*
 * ====================================================================
 *
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 1999 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.util;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import java.util.List;
import java.util.ArrayList;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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;

// Refs:
// http://httpd.apache.org/docs/mod/mod_log_config.html

/**
 * A Utility class to seperate the process of building an access log entry
 * from the storage method used in a particular implementation. For now it
 * implements pretty much the same functionality as the existing
 * AccessLogValve does, withperhaps a few more items. Configuration can be
 * handled two ways:
 * <ol>
 * <li> Passing a format pattern similar to the apache mod_log_config</li>
 * <li> Passing a series of local field names and foreign field names 
 * (Not implemented yet)</li>
 * </ol>
 * <P>NOTES:
 * <ul>
 * <li> Plenty of stuff was stolen from the existing AccessLogValve... so
 * many thanks to the authors of that module.</li>
 * <li> While I work on the parsing of format patterns, this is hardcoded to
 * produce common log format.</li>
 * <li>The implementation for the second style of configuration using field
 * names hasn't been started yet.</li>
 * </ul>
 * 
 * @author Craig R. McClanahan
 * @author Jason Brittain
 * @author David Weinrich
 */
public class AccessLogHelper {

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

    /**
     * Create a new, blank AccessLogHelper
     */
    public AccessLogHelper() {
        loggableItems = new ArrayList();
    }

    /**
     * Create a new AccessLogHelper with the specified format pattern string
     *
     * @param format Format pattern or "common" for common log format.
     */
    public AccessLogHelper( String formatPattern ) {
    	loggableItems = new ArrayList();
        parseFormatPattern( formatPattern );
    }
    
    // ----------------------------------------------------- Instance Variables

    /**
     * Storage for the current formatPattern, if used.
     */
    private String formatPattern = null;

    /**
     * List of LoggableItems we use to build the output string/list
     */
    private List loggableItems = null;
    
    // ------------------------------------------------------------- Properties

    /**
     * Set the value of the current formatPattern string and parse/update
     * internal LoggableItem collection.
     *
     * @param formatPattern The new format pattern to be used ( or "common" )
     * for common log format.
     */
    public void setFormatPattern( String formatPattern ) {
    	this.formatPattern = formatPattern;
    }

    /**
     * Get the value of the current formatPattern string.
     */
    public String getFormatPattern() {
		return formatPattern;
    }

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


    /**
     * Create the log entry with the current information and return the entry
     * as a String.
     *
     * @param request the org.apache.catalina.Request item for this event.
     * @param response the org.apache.catalina.Response item for this event.
     */
    public String getEntryString( Request request, Response response ) {
    	StringBuffer output = new StringBuffer();

        for ( int i = 0; i < loggableItems.size(); i++ ) {
        	LogItem tempItem = (LogItem) loggableItems.get( i );
			output.append( tempItem.getItem( request, response ));
        }

        return output.toString();
    }

    /**
     * Create the log entry with the current information and return the entry
     * as a List of Strings. ( Not implemented yet )
     *
     * @param request the org.apache.catalina.Request item for this event.
     * @param response the org.apache.catalina.Response item for this event.
     */
    public List getEntryList( Request request, Response response ) {
        return ( new ArrayList() );
    }
    
    // -------------------------------------------------------- Private Methods

    /**
     * Parse a format pattern and setup the loggableItems list
     * NOTE: this is hardcoded to create a common log format right now.
     *
     * @param formatPattern format pattern string similar to a subset of
     * http://httpd.apache.org/docs/mod/mod_log_config.html
     */
    private void parseFormatPattern( String formatPattern ) {
    	LogItem space = new StaticString( " " );
    	loggableItems.add( new RemoteHost() );
        loggableItems.add( space );
        loggableItems.add( new RemoteLogname() );
        loggableItems.add( space );
        loggableItems.add( new RemoteAuthUser() );
        loggableItems.add( space );
        loggableItems.add( new CommonTimestamp() );
        loggableItems.add( new StaticString( " \"" ) );
        loggableItems.add( new FirstLineOfRequest() );
        loggableItems.add( new StaticString( "\" " ) );
        loggableItems.add( new ResponseStatus() );
        loggableItems.add( space );
        loggableItems.add( new CLFBytesSent() );
    }
}


abstract class LogItem  {

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

    /**
     * The local name for this LogItem, and for clients that use the field 
     * name for configuration, the field name.
     */
    private String localItemName = null;


    /**
     * The foreign name fo this LogItem, for now intended primarily for the
     * case ( such as a JDBCAccessLog ) where the name of this item in the
     * database is different from the field name set up by the helper.
     */
    private String foreignItemName = null;
    
    // ------------------------------------------------------------- Properties

    /**
     * Set the local name of this item type.
     *
     * @param name Local name for this item type.
     */
    public void setLocalItemName( String name ) {
		localItemName = name;
    }

    /**
     * Return the local name of this item type.
     */
    public String getLocalItemName() { return localItemName; }

    /**
     * Set the current foreign name of this item type.
     *
     * @param name Foreign name for this item type.
     */
    public void setForeignItemName( String name ) {
	foreignItemName = name;
    }

    /**
     * Return the current foreign name of this item type.
     */
    public String getForeignItemName() { return foreignItemName; }

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

    public abstract String getItem( Request request, Response response );
}




/**
 * Convenience class to implement static strings to be placed into a log.
 */
class StaticString extends LogItem {
    // ----------------------------------------------------------- Constructors

    public StaticString( String string ) {
		logEntry = string;
    }

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

    private String logEntry = null;

    // --------------------------------------------------------- Public Methods
    
    /**
     * Return the item to be logged, fully formatted for the proper destination
     * based on the given request/response.
     *
     * @param request ServletRequest for this logged event.
     * @param response ServletResponse for this logged event.
     */
    public String getItem( Request request, Response response ) {
    	return logEntry;
    }
}

/**
 * Class to provide timestamps in String form from a specified 
 * SimpleDateFormat String, or ISO-ish date format by default.
 */
class Timestamp extends LogItem {

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

    /**
     * Create new TimestampLogItem with ISO date format
     */
    public Timestamp() {
    	currentDate = new Date();
		setDateFormat( "yyyy-MM-dd HH:mm:ss " + getTimeZoneString() );
    }

    /**
     * Create new TimestampLogItem with ISO date format
     */
    public Timestamp( String dateFormat ) {
    	currentDate = new Date();
        this.dateFormat = new SimpleDateFormat( dateFormat );
    }
    
    // ----------------------------------------------------- Instance Variables

    /**
     * The formatter used for this timestamp.
     */
    private SimpleDateFormat dateFormat = null;

    /**
     * The current time as a Date item, managed internally by the getDate()
     * method.
     */
    private Date currentDate = null;


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

    public void setDateFormat( String dateFormat ) {
		this.dateFormat = new SimpleDateFormat( dateFormat );
    }

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

    /**
     * Return the item to be logged, fully formatted for the proper destination
     * based on the given request/response.
     *
     * @param request ServletRequest for this logged event.
     * @param response ServletResponse for this logged event.
     */
	public String getItem( Request request, Response response ) {
		return ( dateFormat.format( getDate() ) );
    }
    
	// -------------------------------------------------------- Private Methods

    /**
     * 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() {
        
        // Only create a new Date once per second, max.
        if ((System.currentTimeMillis() - currentDate.getTime()) > 1000) {
            currentDate = new Date();
        }
        
        return currentDate;
    }

    /**
     * Returns the TimeZone offset in the format "-0800" for ISO
     * date format.
     */
    private String getTimeZoneString() {
    	String timeZone = null;
    	TimeZone tz = TimeZone.getDefault();
        timeZone = "" + (tz.getRawOffset() / (60 * 60 * 10));
        if (timeZone.length() < 5) {
            timeZone = timeZone.substring(0, 1) + "0" +
                timeZone.substring(1, timeZone.length());
        }

        return timeZone;
    }
}

/**
 * Class that manages creation of Common Log Format timestamps.
 */
class CommonTimestamp extends LogItem {

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

    /**
     * Create a timestamp item that creates Common Log Format timestamps.
     */
    public CommonTimestamp() {

        dayFormatter = new SimpleDateFormat("dd");
        monthFormatter = new SimpleDateFormat("MM");
        yearFormatter = new SimpleDateFormat("yyyy");
        timeFormatter = new SimpleDateFormat("HH:mm:ss");
        timeZone = getTimeZoneString();
        currentDate = new Date();

    }

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

    /**
     * A small army of SimpleDateFormat objects, simply to get
     * around the fact that a three character month isn't covered
     * by SimlpeDateFormat :)
     */
    private SimpleDateFormat dayFormatter = null;
    private SimpleDateFormat monthFormatter = null;
    private SimpleDateFormat yearFormatter = null;
    private SimpleDateFormat timeFormatter = null;

    /**
     * String representation of the local time zone ( "-0800" for example )
     */
    private String timeZone = null;

    /**
     * local copy of the current date/time, updated no more than
     * once a second as needed by getDate() method.
     */
    private Date currentDate = null;

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

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

    /**
     * Create the timestamp. Copied directly from the existing AccessLogValve
     * implementation. TODO: redo to use less SimpleDateFormat objects, and
     * possibly get around the need for the lookup() method.
     *
     * @param request ServletRequest for this logged event.
     * @param response ServletResponse for this logged event.
     */
    public String getItem( Request request, Response response ) {
    	Date date = getDate();
    	StringBuffer result = new StringBuffer( 29 );
    	result.append("[");
        result.append(dayFormatter.format(date));            // Day
        result.append('/');
        result.append(lookup(monthFormatter.format(date))); // Month
        result.append('/');
        result.append(yearFormatter.format(date));            // Year
        result.append(':');
        result.append(timeFormatter.format(date));        // Time
        result.append(' ');
        result.append(timeZone);                            // Time Zone
        result.append("]");

        return result.toString();
    }

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

    /**
     * Return 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() {
        
        // Only create a new Date once per second, max.
        if ((System.currentTimeMillis() - currentDate.getTime()) > 1000) {
            currentDate = new Date();
        }
        
        return currentDate;
    }

    /**
     * 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]);
    }

    /**
     * Returns the TimeZone offset in the format "-0800"
     */
    private String getTimeZoneString() {
    	String timezone = null;
    	TimeZone tz = TimeZone.getDefault();
        timeZone = "" + (tz.getRawOffset() / (60 * 60 * 10));
        if (timeZone.length() < 5) {
            timeZone = timeZone.substring(0, 1) + "0" +
                timeZone.substring(1, timeZone.length());
        }

        return timeZone;
    }
}

/**
 * Class to return the RemoteHost of the given request.
 */
class RemoteHost extends LogItem {
    public String getItem( Request request, Response response ) {
        return request.getRequest().getRemoteHost();
    }
}

/**
 * Class to return the remote user name as retrieved by
 * ident. We don't use ident so this always returns '-'
 */
class RemoteLogname extends StaticString {
    public RemoteLogname() {
		super( "-" );
    }
}

/**
 * Class to return the username that this request is authorized
 * with.
 */
class RemoteAuthUser extends LogItem {
    public String getItem( Request request, Response response ) {
    	HttpServletRequest hreq = (HttpServletRequest) request.getRequest();
        String user = null;

        if ( hreq != null ) {
            user = hreq.getRemoteUser();

            if ( user == null )
                user = "-";

        } else {
            user = "-";
        }

        return (user);
    }
}
/**
 * Class to return the first line of the request string
 * as implemented by current AccessLogValve.
 */
class FirstLineOfRequest extends LogItem {
    public String getItem( Request request, Response response ) {
        HttpServletRequest hreq = (HttpServletRequest) request.getRequest();
        String value;

        if (hreq != null) {
           value = hreq.getMethod()
             + " " + hreq.getRequestURI()
             + " " + hreq.getProtocol();
        } else {
            value = "- - " + request.getRequest().getProtocol();
        }

        return (value);
    }
}

/**
 * Class to return the HTTP status code.
 */
class ResponseStatus extends LogItem {
    public String getItem( Request request, Response response ) {
        ServletResponse res = response.getResponse();
        HttpServletResponse hres = null;
        String value = null;

        if (res instanceof HttpServletResponse) {
            hres = (HttpServletResponse) res;
            value = "" + ((HttpResponse) response).getStatus();
        } else {
            value = "-";
        }

        return (value);
    }
}

/**
 * Class to return the bytes sent in Common Log Format
 */
class CLFBytesSent extends LogItem {
    public String getItem( Request request, Response response ) {
        int length = response.getContentCount();
        String value = null;

        if (length <= 0)
            value = "-";
        else
            value = "" + length;

        return (value);
    }
}

