// CommonLogger.java
// $Id: CommonLogger.java,v 1.23 1998/07/01 11:44:43 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/hypertext/WWW/Daemon/User/Config/Logging.html#common-logfile-format">common log format</a>).
 * @see org.w3c.jigsaw.core.Logger
 */

public class CommonLogger extends Logger implements PropertyMonitoring {
    private 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 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";

    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 String               logdir   = "logs" ;
    protected int                  bufsize  = 8192;
    protected int                  bufptr   = 0;
    protected byte                 buffer[] = null;

    /**
     * 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(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 {
		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;
	    }
	} else {
	    // 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()) ;
	    }
	}
    }

    /**
     * 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();
	Date   now    = (date < 0) ? new Date() : new Date(date);

	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()))
	       + ((now.getTimezoneOffset() < 0)
		  ? " " + (now.getTimezoneOffset() / 60)
		  : " +" + (now.getTimezoneOffset() / 60))
	       + "]")
	    + " \"" + request.getMethod()	// request line
	    + " " + request.getURL()
	    + " " + request.getVersion()
	    + "\" " + reply.getStatus()		// reply status
	    + " " + 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) ;
	if ( filename == 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) ;
	    }
	    File flogdir = new File(root_dir, logdir) ;
	    return (new File(flogdir, def)).getAbsolutePath() ;
	} else {
	    return filename ;
	}
    }

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

    protected void openLogFile () {
	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) ;
	// 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] ;
    }
}

