// ========================================================================
// $Id: NCSARequestLog.java,v 1.35 2005/08/13 00:01:24 gregwilkins Exp $
// Copyright 2000-2004 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at 
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========================================================================

package org.mortbay.http;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Locale;
import java.util.TimeZone;

import javax.servlet.http.Cookie;

import org.apache.commons.logging.Log;
import org.mortbay.log.LogFactory;
import org.mortbay.util.DateCache;
import org.mortbay.util.LogSupport;
import org.mortbay.util.RolloverFileOutputStream;
import org.mortbay.util.StringUtil;


/* ------------------------------------------------------------ */
/** NCSA HTTP Request Log.
 * NCSA common or NCSA extended (combined) request log.
 * @version $Id: NCSARequestLog.java,v 1.35 2005/08/13 00:01:24 gregwilkins Exp $
 * @author Tony Thompson
 * @author Greg Wilkins
 */
public class NCSARequestLog implements RequestLog
{
    private static Log log = LogFactory.getLog(NCSARequestLog.class);

    private String _filename;
    private boolean _extended;
    private boolean _append;
    private int _retainDays;
    private boolean _closeOut;
    private boolean _preferProxiedForAddress;
    private String _logDateFormat="dd/MMM/yyyy:HH:mm:ss ZZZ";
    private Locale _logLocale=Locale.getDefault();
    private String _logTimeZone=TimeZone.getDefault().getID();
    private String[] _ignorePaths;
    private boolean _logLatency=false;
    private boolean _logCookies=false;
    
    private transient OutputStream _out;
    private transient OutputStream _fileOut;
    private transient DateCache _logDateCache;
    private transient PathMap _ignorePathMap;
    private transient Writer _writer;
    
    /* ------------------------------------------------------------ */
    /** Constructor.
     */
    public NCSARequestLog()
    {
        _extended=true;
        _append=true;
        _retainDays=31;
    }
    
    /* ------------------------------------------------------------ */
    /** Constructor. 
     * @param filename Filename, which can be in
     * rolloverFileOutputStream format
     * @see org.mortbay.util.RolloverFileOutputStream
     * @exception IOException 
     */
    public NCSARequestLog(String filename)
        throws IOException
    {
        _extended=true;
        _append=true;
        _retainDays=31;
        setFilename(filename);
    }

    /* ------------------------------------------------------------ */
    /** Set the log filename.
     * @see NCSARequestLog#setRetainDays(int)
     * @param filename The filename to use. If the filename contains the
     * string "yyyy_mm_dd", then a RolloverFileOutputStream is used and the
     * log is rolled over nightly and aged according setRetainDays. If no
     * filename is set or a null filename
     * passed, then requests are logged to System.err.
     */
    public void setFilename(String filename)
    {
        if (filename!=null)
        {
            filename=filename.trim();
            if (filename.length()==0)
                filename=null;
        }
        _filename=filename;        
    }

    /* ------------------------------------------------------------ */
    /** Get the log filename.
     * @see NCSARequestLog#getDatedFilename()
     * @return The log filename without any date expansion.
     */
    public String getFilename()
    {
        return _filename;
    }

    /* ------------------------------------------------------------ */
    /** Get the dated log filename.
     * @see NCSARequestLog#getFilename()
     * @return The log filename with any date encoding expanded.
     */
    public String getDatedFilename()
    {
        if (_fileOut instanceof RolloverFileOutputStream)
            return ((RolloverFileOutputStream)_fileOut).getDatedFilename();
        return null;
    }
    
    
    /* ------------------------------------------------------------ */
    /** 
     * @param format The date format to use within the log file.
     */
    public void setLogDateFormat(String format)
    {
        _logDateFormat=format;
    }

    /* ------------------------------------------------------------ */
    /** 
     * @return The date format to use within the log file.
     */
    public String getLogDateFormat()
    {
        return _logDateFormat;
    }
    
    /* ------------------------------------------------------------ */
    /** 
     * @param tz The date format timezone to use within the log file.
     */
    public void setLogTimeZone(String tz)
    {
        _logTimeZone=tz;
    }

    /* ------------------------------------------------------------ */
    /** 
     * @return The date format timezone to use within the log file.
     */
    public String getLogTimeZone()
    {
        return _logTimeZone;
    }
    
    /* ------------------------------------------------------------ */
    /** 
     * @return The number of days to retain rollovered log files.
     */
    public int getRetainDays()
    {
        return _retainDays;
    }

    /* ------------------------------------------------------------ */
    /** 
     * @param retainDays The number of days to retain rollovered log files.
     */
    public void setRetainDays(int retainDays)
    {
        _retainDays = retainDays;
    }

    /* ------------------------------------------------------------ */
    /** 
     * @return True if NCSA extended format is to be used.
     */
    public boolean isExtended()
    {
        return _extended;
    }

    /* ------------------------------------------------------------ */
    /** 
     * @param e True if NCSA extended format is to be used.
     */
    public void setExtended(boolean e)
    {
        _extended=e;
    }
    
    /* ------------------------------------------------------------ */
    /** 
     * @return True if logs are appended to existing log files.
     */
    public boolean isAppend()
    {
        return _append;
    }

    /* ------------------------------------------------------------ */
    /** 
     * @param a True if logs are appended to existing log files.
     */
    public void setAppend(boolean a)
    {
        _append=a;
    }
    
    /* ------------------------------------------------------------ */
    /**
     * @deprecated ignored
     */
    public void setBuffered(boolean b)
    {}
    
    /* ------------------------------------------------------------ */
    /** Set which paths to ignore.
     *
     * @param ignorePaths Array of path specifications to ignore
     */
    public void setIgnorePaths(String[] ignorePaths)
    {
        // Contributed by  Martin Vilcans (martin@jadestone.se)
        _ignorePaths = ignorePaths;
    }

    /* ------------------------------------------------------------ */
    public String[] getIgnorePaths()
    {
        return _ignorePaths;
    }

    /* ------------------------------------------------------------ */
    /**
     * @return Returns the logCookies.
     */
    public boolean getLogCookies()
    {
        return _logCookies;
    }
    /* ------------------------------------------------------------ */
    /**
     * @param logCookies The logCookies to set.
     */
    public void setLogCookies(boolean logCookies)
    {
        _logCookies = logCookies;
    }
    
    /* ------------------------------------------------------------ */
    /**
     * @return Returns true if logging latency
     */
    public boolean getLogLatency()
    {
        return _logLatency;
    }
    
    /* ------------------------------------------------------------ */
    /**
     * @param logLatency If true, latency is logged at the end of the log line
     */
    public void setLogLatency(boolean logLatency)
    {
        _logLatency = logLatency;
    }
    
    /* ------------------------------------------------------------ */
    /**
    * Prefer to log the proxied-for IP address (if present in
    * the request header) over the native requester IP address.
    * Useful in reverse-proxy situations when you'd rather see
    * the IP address of the host before the most recent proxy
    * server, as opposed to your own proxy server(s) every time.
    *
    * jlrobins@socialserve.com, March 2004.
    **/
    public void setPreferProxiedForAddress(boolean value)
    {
       _preferProxiedForAddress = value;
    }
    
    /* ------------------------------------------------------------ */
    public void start()
        throws Exception
    {
        _logDateCache=new DateCache(_logDateFormat,_logLocale);
        _logDateCache.setTimeZoneID(_logTimeZone);
        
        if (_filename != null)
        {
            _fileOut=new RolloverFileOutputStream(_filename,_append,_retainDays);
            _closeOut=true;
        }
        else
            _fileOut=System.err;

        _out=_fileOut;

        if (_ignorePaths!=null && _ignorePaths.length>0)
        {
            _ignorePathMap=new PathMap();
            for (int i=0;i<_ignorePaths.length;i++)
                _ignorePathMap.put(_ignorePaths[i],_ignorePaths[i]);
        }
        else
            _ignorePathMap=null;

        _writer=new OutputStreamWriter(_out);
    }

    /* ------------------------------------------------------------ */
    public boolean isStarted()
    {
        return _fileOut!=null;
    }
    
    /* ------------------------------------------------------------ */
    public void stop()
    {
        try{if (_writer!=null)_writer.flush();} catch (IOException e){LogSupport.ignore(log,e);}
        if (_out!=null && _closeOut)
            try{_out.close();}catch(IOException e){LogSupport.ignore(log,e);}
        _out=null;
        _fileOut=null;
        _closeOut=false;
        _logDateCache=null;
        _writer=null;
    }
    
    /* ------------------------------------------------------------ */
    /** Log a request.
     * @param request The request
     * @param response The response to this request.
     * @param responseLength The bytes written to the response.
     */
    public void log(HttpRequest request,
                    HttpResponse response,
                    int responseLength)
    {
        try{
            // ignore ignorables
            if (_ignorePathMap != null &&
                _ignorePathMap.getMatch(request.getPath()) != null)
                return;

            // log the rest
            if (_fileOut==null)
                return;

            StringBuffer buf = new StringBuffer(160);
            
            String addr = null;
            if(_preferProxiedForAddress)
            {
                // If header is not present, addr will remain null ...
                addr = request.getField(HttpFields.__XForwardedFor);
            }
            if(addr == null)
                addr = request.getRemoteAddr();
            buf.append(addr);
            
            buf.append(" - ");
            String user = request.getAuthUser();
            buf.append((user==null)?"-":user);
            buf.append(" [");
            buf.append(_logDateCache.format(request.getTimeStamp()));
            buf.append("] \"");
            buf.append(request.getMethod());
            buf.append(' ');
            buf.append(request.getURI());
            buf.append(' ');
            buf.append(request.getVersion());
            buf.append("\" ");
            int status=response.getStatus();    
            buf.append((char)('0'+((status/100)%10)));
            buf.append((char)('0'+((status/10)%10)));
            buf.append((char)('0'+(status%10)));
            if (responseLength>=0)
            {
                buf.append(' ');
                if (responseLength>99999)
                    buf.append(Integer.toString(responseLength));
                else
                {
                    if (responseLength>9999) 
                        buf.append((char)('0'+((responseLength/10000)%10)));
                    if (responseLength>999) 
                        buf.append((char)('0'+((responseLength/1000)%10)));
                    if (responseLength>99) 
                        buf.append((char)('0'+((responseLength/100)%10)));
                    if (responseLength>9) 
                        buf.append((char)('0'+((responseLength/10)%10)));
                    buf.append((char)('0'+(responseLength%10)));
                }
                buf.append(' ');
            }
            else
                buf.append(" - ");

            String log =buf.toString();
            synchronized(_writer)
            {
                _writer.write(log);
                if (_extended)
                {
                    logExtended(request,response,_writer);
                    if (!_logCookies)
                        _writer.write(" -");
                }
                
                if (_logCookies)
                {
                    Cookie[] cookies = request.getCookies();
                    if (cookies==null || cookies.length==0)
                        _writer.write(" -");
                    else
                    {
                        _writer.write(" \"");
                        for (int i=0;i<cookies.length;i++)
                        {
                            if (i!=0)
                                _writer.write(';');
                            _writer.write(cookies[i].getName());
                            _writer.write('=');
                            _writer.write(cookies[i].getValue());
                        }
                        _writer.write("\"");
                    }
                }
                
                if (_logLatency)
                    _writer.write(" "+(System.currentTimeMillis()-request.getTimeStamp()));
                
                _writer.write(StringUtil.__LINE_SEPARATOR);
                _writer.flush();
            }
        }
        catch(IOException e)
        {
            log.warn(LogSupport.EXCEPTION,e);
        }
    }

    /* ------------------------------------------------------------ */
    /** Log Extended fields.
     * This method can be extended by a derived class to add extened fields to
     * each log entry.  It is called by the log method after all standard
     * fields have been added, but before the line terminator.
     * Derived implementations should write extra fields to the Writer
     * provided.
     * The default implementation writes the referer and user agent.
     * @param request The request to log.
     * @param response The response to log.
     * @param log The writer to write the extra fields to.
     * @exception IOException Problem writing log
     */
    protected void logExtended(HttpRequest request,
                               HttpResponse response,
                               Writer log)
        throws IOException
    {
        String referer = request.getField(HttpFields.__Referer);
        if(referer==null)
            log.write("\"-\" ");
        else
        {
            log.write('"');
            log.write(referer);
            log.write("\" ");
        }
        
        String agent = request.getField(HttpFields.__UserAgent);
        if(agent==null)
            log.write("\"-\"");
        else
        {
            log.write('"');
            log.write(agent);
            log.write('"');
        }
        

    }
}

