// CommonLogger.java
// $Id: CommonLogger.java,v 1.34 2000/08/07 11:27:11 ylafon Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.jigsaw.http ;

import java.io.* ;
import java.util.Date ;

import org.w3c.jigsaw.daemon.*;
import org.w3c.jigsaw.auth.AuthFilter;
import org.w3c.util.*;

/**
 * The CommonLogger class implements the abstract Logger class.
 * The resulting log will conform to the 
 * <a href="http://www.w3.org/Daemon/User/Config/Logging.html#common-logfile-format">common log format</a>).
 * @see org.w3c.jigsaw.http.Logger
 */

public class CommonLogger extends Logger implements PropertyMonitoring {
    protected static final String monthnames[] = {
	"Jan", "Feb", "Mar", "Apr", "May", "Jun",
	"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
    };
	
    /**
     * Name of the property indicating the log file.
     * This property indicates the name of the log file to use.
     * <p>This property defaults to the <code>log</code> file in the server
     * log directory.
     */
    public static final 
    String LOGNAME_P = "org.w3c.jigsaw.logger.logname" ;
    /**
     * Name of the property indicating the error log file.
     * This property indicates the name of the error log file to use.
     * <p>This property defaults to the <code>errlog</code> file in the
     * server log directory.
     */
    public static final 
    String ERRLOGNAME_P = "org.w3c.jigsaw.logger.errlogname" ;
    /**
     * Name of the property indicating the server trace file.
     * This property indicates the name of the trace file to use.
     * <p>This property defaults to the <code>trace</code> file in the 
     * server log directory.
     */
    public static final 
    String LOGDIRNAME_P = "org.w3c.jigsaw.logger.logdirname";
    /**
     * Name of the property indicating the server log directory.
     * <p>This property defaults to the <code>logs</code> directory in the 
     * server main directory.
     */
    public static final 
    String TRACELOGNAME_P = "org.w3c.jigsaw.logger.tracelogname";
    /**
     * Name of the property indicating the buffer size for the logger.
     * This buffer size applies only the the log file, not to the error
     * log file, or the trace log file. It can be set to zero if you want
     * no buffering.
     * <p>This property default to <strong>4096</strong>.
     */
    public static final 
    String BUFSIZE_P = "org.w3c.jigsaw.logger.bufferSize";
    /**
     * Name of the property indicating the buffer size for the logger.
     * This buffer size applies only the the log file, not to the error
     * log file, or the trace log file. It can be set to zero if you want
     * no buffering.
     * <p>This property default to <strong>4096</strong>.
     */
    public static final 
    String ROTATE_LEVEL_P = "org.w3c.jigsaw.logger.rotateLevel";

    private   byte                 msgbuf[]    = null ;
    protected RandomAccessFile     log         = null ;
    protected RandomAccessFile     errlog      = null ;
    protected RandomAccessFile     trace       = null ;
    protected httpd                server      = null ;
    protected ObservableProperties props       = null ;
    protected int                  bufsize     = 8192 ;
    protected int                  bufptr      = 0    ;
    protected int                  rotateLevel = 0    ;
    protected byte                 buffer[]    = null ;
    protected int                  year        = -1   ;
    protected int                  month       = -1   ;
    protected int                  day         = -1   ;
    protected int                  tz          = -1   ;

    /**
     * Property monitoring for the logger.
     * The logger allows you to dynamically (typically through the property
     * setter) change the names of the file to which it logs error, access
     * and traces.
     * @param name The name of the property that has changed.
     * @return A boolean, <strong>true</strong> if the change was made, 
     *    <strong>false</strong> otherwise.
     */

    public boolean propertyChanged (String name) {
	if ( name.equals(LOGNAME_P) ) {
	    try {
		openLogFile () ;
	    } catch (Exception e) {
		e.printStackTrace() ;
		return false ;
	    }
	    return true ;
	} else if ( name.equals(ERRLOGNAME_P) ) {
	    try {
		openErrorLogFile() ;
	    } catch (Exception e) {
		e.printStackTrace() ;
		return false ;
	    } 
	    return true ;
	} else if ( name.equals(TRACELOGNAME_P) ) {
	    try {
		openTraceFile() ;
	    } catch (Exception e) {
		e.printStackTrace() ;
		return false ;
	    }
	    return true ;
	} else if ( name.equals(LOGDIRNAME_P) ) {
	    try {
		openLogFile () ;
		openErrorLogFile() ;
		openTraceFile() ;
	    } catch (Exception e) {
		e.printStackTrace() ;
		return false ;
	    }
	    return true ;
	} else if ( name.equals(BUFSIZE_P) ) {
	    synchronized (this) {
		bufsize = props.getInteger(name, bufsize);
		// Reset buffer before resizing:
		if ( bufptr > 0 ) {
		    try {
			log.write(buffer, 0, bufptr);
			bufptr = 0;
		    } catch (IOException ex) {
		    }
		}
		// Set new buffer:
		buffer = (bufsize > 0) ? new byte[bufsize] : null;
		return true;
	    }
	} else if (name.equals(ROTATE_LEVEL_P) ) {
	    int newLevel = props.getInteger(name, rotateLevel);
	    if (newLevel != rotateLevel) {
		synchronized (this) {
		    sync();
		    rotateLevel = newLevel;
		    openLogFile();
		}
	    }
	    return true;
	} else {
	    return true ;
	}
    }
    
    /**
     * Output the given message to the given RandomAccessFile.
     * This method makes its best effort to avoid one byte writes (which you
     * get when writing the string as a whole). It first copies the string 
     * bytes into a private byte array, and than, write them all at once.
     * @param f The RandomAccessFile to write to, which should be one of
     *    log, errlog or trace.
     * @param msg The message to be written.
     * @exception IOException If writing to the output failed.
     */

    protected synchronized void output (RandomAccessFile f, String msg)
	throws IOException
    {
	int len = msg.length() ;
	if ( len > msgbuf.length ) 
	    msgbuf = new byte[len] ;
	msg.getBytes (0, len, msgbuf, 0) ;
	f.write (msgbuf, 0, len) ;
    }

    protected synchronized void appendLogBuffer(String msg)
	throws IOException
    {
	int msglen = msg.length();
	if ( bufptr + msglen > buffer.length ) {
	    // Flush the buffer:
	    log.write(buffer, 0, bufptr);
	    bufptr = 0;
	    // Check for messages greater then buffer:
	    if ( msglen > buffer.length ) {
		byte huge[] = new byte[msglen];
		msg.getBytes(0, msglen, huge, 0);
		log.write(huge, 0, msglen);
		return;
	    }
	} 
	// Append that message to buffer:
	msg.getBytes(0, msglen, buffer, bufptr);
	bufptr += msglen;
    }

    protected void logmsg (String msg) {
	if ( log != null ) {
	    try {
		if ( buffer == null ) {
		    output (log, msg) ;
		} else {
		    appendLogBuffer(msg);
		}
	    } catch (IOException e) {
		throw new HTTPRuntimeException (this,"logmsg",e.getMessage()) ;
	    }
	}
    }
    
    protected void errlogmsg (String msg) {
	if ( errlog != null ) {
	    try {
		output (errlog, msg) ;
	    } catch (IOException e) {
		throw new HTTPRuntimeException (this
						, "errlogmsg"
						, e.getMessage()) ;
	    }
	}
    }
    
    protected void tracemsg (String msg) {
	if ( trace != null ) {
	    try {
		output (trace, msg) ;
	    } catch (IOException e) {
		throw new HTTPRuntimeException (this
						, "tracemsg"
						, e.getMessage()) ;
	    }
	}
    }

    protected void checkLogFile(Date now) {
	int nowYear = now.getYear()+1900;
	if (rotateLevel == 1) {
	    // rotate every year
	    if (nowYear != year) {
		synchronized (this) {
		    if (log != null) {
			sync();
		    }
		    this.year = nowYear;
		    openLogFile(year);
		}
	    }
	} else {
	    int nowMonth = now.getMonth();
	    if (rotateLevel == 2) {
		// rotate every month
		if ((nowYear != year) || (nowMonth != month)) {
		    synchronized (this) {
			if (log != null) {
			    sync();
			}
			this.year = nowYear;
			this.month = nowMonth;
			openLogFile(year, month);
		    }
		}
	    } else {
		int nowDay = now.getDate();
		if (rotateLevel == 3) {
		    // rotate every day
		    if ((nowYear != year) || (nowMonth != month) 
			|| (nowDay != day)) {
			synchronized (this) {
			    if (log != null) {
				sync();
			    }
			    this.year  = nowYear;
			    this.month = nowMonth;
			    this.day   = nowDay;
			    openLogFile(year, month, day);
			}
		    } 
		}
	    }
	}
    }

    protected void openLogFile(int year, int month, int day) {
	this.year = year;
	this.month = month;
	this.day = day;

	String ext = null;
	if (month < 10) {
	    if (day < 10) {
		ext = "_"+year+"_0"+(month+1)+"_0"+day;
	    } else {
		ext = "_"+year+"_0"+(month+1)+"_"+day;
	    }
	} else {
	    if (day < 10) {
		ext = "_"+year+"_"+(month+1)+"_0"+day;
	    } else {
		ext = "_"+year+"_"+(month+1)+"_"+day;
	    }
	}
	String logname = getFilename(LOGNAME_P, "log") + ext;
	try {
	    RandomAccessFile old = log ;
	    log = new RandomAccessFile (logname, "rw") ;
	    log.seek (log.length()) ;
	    if ( old != null )
		old.close () ;
	} catch (IOException e) {
	    throw new HTTPRuntimeException (this.getClass().getName()
					    , "openLogFile"
					    , "unable to open "+logname);
	}
    }

    protected void openLogFile(int year, int month) {
	this.year = year;
	this.month = month;
       
	String ext = null;
	if (month < 10)
	    ext = "_"+year+"_0"+(month+1);
	else
	    ext = "_"+year+"_"+(month+1);

	String logname = getFilename(LOGNAME_P, "log") + ext;
	try {
	    RandomAccessFile old = log ;
	    log = new RandomAccessFile (logname, "rw") ;
	    log.seek (log.length()) ;
	    if ( old != null )
		old.close () ;
	} catch (IOException e) {
	    throw new HTTPRuntimeException (this.getClass().getName()
					    , "openLogFile"
					    , "unable to open "+logname);
	}
    }

    protected void openLogFile(int year) {
	this.year = year;
	
	String logname = getFilename(LOGNAME_P, "log") + "_" + year;
	try {
	    RandomAccessFile old = log ;
	    log = new RandomAccessFile (logname, "rw") ;
	    log.seek (log.length()) ;
	    if ( old != null )
		old.close () ;
	} catch (IOException e) {
	    throw new HTTPRuntimeException (this.getClass().getName()
					    , "openLogFile"
					    , "unable to open "+logname);
	}
    }

    /**
     * Log the given HTTP transaction.
     * This is shamelessly slow.
     */
    public void log (Request request, Reply reply, int nbytes, long duration) {
	Client client = request.getClient() ;
	String entry  = null ;
	long   date   = reply.getDate();
	if ( tz == -1 ) {
	    tz = new Date().getTimezoneOffset();
	}
	Date   now;
	if (date < 0 ) {
	    now = new Date((new Date()).getTime() + tz*60*1000);
	} else {
	    now = new Date(date + (tz*60*1000));
	}

	if (rotateLevel > 0) {
	    checkLogFile(now);
	}

	String user = (String) request.getState(AuthFilter.STATE_AUTHUSER);

	entry = client.getInetAddress().getHostAddress()
	    + " " + "-"			  	   // user name
	    + " " + ((user == null ) ? "-" : user) // auth user name
	    + ((now.getDate() < 10) ? " [0" : " [")
	    + (now.getDate() 		   	   // current date
	       + "/" + monthnames[now.getMonth()]
	       + "/" + (now.getYear() + 1900)
	       + ((now.getHours() < 10)
		  ? (":0" + now.getHours())
		  : (":" + now.getHours()))
	       + ((now.getMinutes() < 10)
		  ? (":0" + now.getMinutes())
		  : (":" + now.getMinutes()))
	       + ((now.getSeconds() < 10)
		  ? (":0" + now.getSeconds())
		  : (":" + now.getSeconds()))
	       + " +0000]")
	    + " \"" + request.getMethod()	// request line
	    + " " + request.getURL()
	    + " " + request.getVersion()
	    + "\" " + reply.getStatus()		// reply status
	    + " " + ((nbytes < 0 ) ? "-" :
		     Integer.toString(nbytes)) // # of emited bytes
	    + "\n" ;
	logmsg (entry) ;
    }

    public void log(String msg) {
	logmsg(msg);
    }

    public void errlog (Client client, String msg) {
	errlogmsg (client + ": " + msg + "\n") ;
    }

    public void errlog (String msg) {
	errlogmsg (msg + "\n") ;
    }

    public void trace (Client client, String msg) {
	tracemsg (client + ": " + msg + "\n") ;
    }

    public void trace (String msg) {
	tracemsg (msg + "\n") ;
    }

    /**
     * Get the name for the file indicated by the provided property.
     * This method first looks for a property value. If none is found, it
     * than constructs a default filename from the server root, by 
     * using the provided default name.
     * <p>This method shall either succeed in getting a filename, or throw
     * a runtime exception.
     * @param propname The name of the property.
     * @param def The default file name to use.
     * @exception HTTPRuntimeException If no file name could be deduced from
     *     the provided set of properties.
     */

    protected String getFilename (String propname, String def) {
	String filename = props.getString (propname, null) ;
	File flogdir = null;
	if ( filename == null ) {
	    String logdirname = props.getString(LOGDIRNAME_P, null);
	    if ( logdirname == null) {
		File root_dir = server.getRootDirectory();
		if ( root_dir == null ) {
		    String msg = "unable to build a default value for the \""
			+ propname + "\" value." ;
		    throw new HTTPRuntimeException (this.getClass().getName()
						    , "getFilename"
						    , msg) ;
		}
		flogdir = new File(root_dir, "logs") ;
	    } else {
		try {
		    flogdir = new File(logdirname);
		} catch (RuntimeException ex) {
		    String msg = "unable to access log directory "+logdirname;
		    throw new HTTPRuntimeException (this.getClass().getName()
						    , "getFilename"
						    , msg) ;
		}
	    }
	    return (new File(flogdir, def)).getAbsolutePath() ;
	} else {
	    String logdirname = props.getString(LOGDIRNAME_P, null);
	    if ( logdirname == null)
		return filename ;
	    try {
		flogdir = new File(logdirname);
	    } catch (RuntimeException ex) {
		String msg = "unable to access log directory "+logdirname;
		throw new HTTPRuntimeException (this.getClass().getName()
						, "getFilename"
						, msg) ;
	    }
	    return (new File(flogdir, filename)).getAbsolutePath() ;
	}
    }
    
    /**
     * Open this logger log file.
     */

    protected void openLogFile () {
	if (rotateLevel > 0) {
	    if ( tz == -1 ) {
		tz = new Date().getTimezoneOffset();
	    }
	    Date now = new Date();
	    this.year = -1;
	    checkLogFile(now);
	} else {
	    String logname = getFilename(LOGNAME_P, "log") ;
	    try {
		RandomAccessFile old = log ;
		log = new RandomAccessFile (logname, "rw") ;
		log.seek (log.length()) ;
		if ( old != null )
		    old.close () ;
	    } catch (IOException e) {
		throw new HTTPRuntimeException (this.getClass().getName()
						, "openLogFile"
						, "unable to open "+logname);
	    }
	}
    }

    /**
     * Open this logger error log file.
     */

    protected void openErrorLogFile () {
	String errlogname = getFilename (ERRLOGNAME_P, "errlog") ;
	try {
	    RandomAccessFile old = errlog ;
	    errlog = new RandomAccessFile (errlogname, "rw") ;
	    errlog.seek (errlog.length()) ;
	    if ( old != null )
		old.close() ;
	} catch (IOException e) {
	    throw new HTTPRuntimeException (this.getClass().getName()
					    , "openErrorLogFile"
					    , "unable to open "+errlogname);
	}
    }

    /**
     * Open this logger trace file.
     */

    protected void openTraceFile () {
	String tracename = getFilename (TRACELOGNAME_P, "traces");
	try {
	    RandomAccessFile old = trace ;
	    trace = new RandomAccessFile (tracename, "rw") ;
	    trace.seek (trace.length()) ;
	    if ( old != null )
		old.close() ;
	} catch (IOException e) {
	    throw new HTTPRuntimeException (this.getClass().getName()
					    , "openTraceFile"
					    , "unable to open "+tracename);
	}
    }

    /**
     * Save all pending data to stable storage.
     */
    
    public synchronized void sync() {
	try {
	    if ((buffer != null) && (bufptr > 0)) {
		log.write(buffer, 0, bufptr);
		bufptr = 0;
	    }
	} catch (IOException ex) {
	    server.errlog(getClass().getName()
			  + ": IO exception in method sync \""
                          + ex.getMessage() + "\".");
	}
    }

    /**
     * Shutdown this logger.
     */

    public synchronized void shutdown () {
	server.getProperties().unregisterObserver (this) ;
	try {
	    // Flush any pending output:
	    if ((buffer != null) && (bufptr > 0)) {
		log.write(buffer, 0, bufptr);
		bufptr = 0;
	    }
	    log.close() ; 
	    log = null ;
	    errlog.close() ;
	    errlog = null ;
	    trace.close() ;
	    trace = null ;
	} catch (IOException ex) {
	    server.errlog(getClass().getName()
			  + ": IO exception in method shutdown \""
                          + ex.getMessage() + "\".");
	}
    }
		
    /**
     * Initialize this logger for the given server.
     * This method gets the server properties describe above to
     * initialize its various log files.
     * @param server The server to which thiss logger should initialize.
     */

    public void initialize (httpd server) {
	this.server = server ;
	this.props  = server.getProperties() ;
	// Register for property changes:
	props.registerObserver (this) ;
        // init the rotation level
        rotateLevel = props.getInteger(ROTATE_LEVEL_P, 0);
	// Open the various logs:
	openLogFile () ;
	openErrorLogFile() ;
	openTraceFile() ;
	// Setup the log buffer is possible:
	if ((bufsize = props.getInteger(BUFSIZE_P, bufsize)) > 0 ) 
	    buffer = new byte[bufsize];
	return ;
    }
	
    /**
     * Construct a new Logger instance.
     */
     
    CommonLogger () {
	this.msgbuf = new byte[128] ;
    }
}

