LogFilter.java
Author: Benoit Mahe

    // LogFilter.java
    // $Id: LogFilter.html,v 1.2 1999/10/27 22:10:35 ylafon Exp $
    // (c) COPYRIGHT MIT and INRIA, 1996.
    // Please first read the full copyright statement in file COPYRIGHT.html

    package org.w3c.jigsaw.filters;

    import java.io.*;
    import java.net.*;
    import java.util.*;

    import org.w3c.tools.resources.*;
    import org.w3c.tools.resources.ProtocolException;
    import org.w3c.www.http.*;
    import org.w3c.jigsaw.http.*;

    /**
     * This filter provides a very flexible logger.
     * It is not designed as a logger, in order to be plugable only on
     * a sub-tree of the URL space (a logger would log all site accesses).
     * It provides as much details as you want, and uses a very simple format:
     * each log entry (or <em>record</em> is made of several lines having
     * the folowing format:
     * <pre>variable=value</pre>
     * A record starts with the special <code>url</code> variable value which
     * provides the requested URL. The for each header that is to be logged,
     * a variable is added in the record, prefixed by its <em>scope</em>. The
     * scope can be either:
     * <dl><dt>request<dd> to specify a request header,
     * <dt>reply<dd> to specify a reply header,
     * <dt>server<dd> to specify global server samples.
     * </dl>
     * As an example, if you configure that filter to log the request's referer
     * and the reply content length, a sample record will look like:
     * <pre>
     * url=http://www.w3.org/pub/WWW/Jigsaw/
     * request.referer=http://www.w3.org/pub/WWW
     * reply.content-length=10
     * </pre>
     */

    public class LogFilter extends ResourceFilter {
        /**
         * Name of the state that when set on the request will prevent logging.
         */
        public static final
    	String DONT_LOG = "org.w3c.jigsaw.filters.LogFilter.nolog";

        /**
         * Attribute index - The HTTP request headers to dump
         */
        protected static int ATTR_REQUEST_HEADERS = -1;
        /**
         * Attribute index - The HTTP reply headers to dump
         */
        protected static int ATTR_REPLY_HEADERS = -1;
        /**
         * Attribute index - The log file to use to emit log record.
         */
        protected static int ATTR_LOGFILE = -1;

        static {
    	Class     c = null;
    	Attribute a = null;

    	try {
    	    c = Class.forName("org.w3c.jigsaw.filters.LogFilter");
    	} catch (Exception ex) {
    	    ex.printStackTrace();
    	    System.exit(1);
    	}
    	// Register the request headers to be logged:
    	a = new StringArrayAttribute("request-headers"
    				     , null
    				     , Attribute.EDITABLE);
    	ATTR_REQUEST_HEADERS = AttributeRegistry.registerAttribute(c, a);
    	// Register the reply headers to be logged:
    	a = new StringArrayAttribute("reply-headers"
    				     , null
    				     , Attribute.EDITABLE);
    	ATTR_REPLY_HEADERS = AttributeRegistry.registerAttribute(c, a);
    	// Register the log file name:
    	a = new FileAttribute("logfile"
    			      , null
    			      , Attribute.EDITABLE);
    	ATTR_LOGFILE = AttributeRegistry.registerAttribute(c, a);
        }

        /**
         * Compiled index of the request headers to dump.
         */
        protected HeaderDescription reqheaders[] = null;
        /**
         * Compiled index of the reply headers to dump.
         */
        protected HeaderDescription repheaders[] = null;
        /**
         * Open log descriptor, to write to the log.
         */
        protected RandomAccessFile log = null;

        /**
         * Compile the given set of header names into header indexes.
         * @param kind An instance of the class whose headers are to be dumped.
         * @param headers The name of headers to compile.
         * @return An array of header description, which will allow fast fetch
         * of header values.
         */

        protected HeaderDescription[] compileHeaders(HttpMessage kind
    						 , String headers[]) {
    	Enumeration       e   = kind.enumerateHeaderDescriptions(true);
    	HeaderDescription r[] = new HeaderDescription[headers.length];
    	while ( e.hasMoreElements() ) {
    	    HeaderDescription d = (HeaderDescription) e.nextElement();
    	    if ( d == null )
    		continue;
    	    for (int i = 0 ; i < headers.length ; i++) {
    		if (headers[i].equals(d.getName())) {
    		    r[i] = d;
    		    break;
    		}
    	    }
    	}
    	return r;
        }

        /**
         * Write the given string to the log file.
         * @param record The string to write.
         */

        protected synchronized void writelog(String record) {
    	try {
    	    if ( log != null )
    		log.writeBytes(record);
    	} catch (IOException ex) {
    	    FramedResource target = (FramedResource) getTargetResource();
    	    if (target != null) {
    		String msg = ("IO error while writing to log file \""+
    			      ex.getMessage() + "\".");
    		target.getServer().errlog(this, msg);
    	    }
    	}
        }

        /**
         * Open the log stream, and make it available through <code>log</code>.
         * If opening the stream failed, an appropriate error message is emitted
         * and <code>log</code> remains set to <strong>null</strong>. If a log
         * stream was already opened, it is first closed.
         */

        protected synchronized void openLog() {
    	// Close first if needed:
    	if ( log != null ) {
    	    try {
    		log.close();
    	    } catch (IOException ex) {
    	    }
    	    log = null;
    	}
    	// Try opening the log:
    	File logfile = getLogfile();
    	try {
    	    if (logfile != null) {
    		log = new RandomAccessFile(logfile, "rw");
    		log.seek(log.length());
    	    }
    	} catch (Exception ex) {
    	}
    	// If failed, emit error message:
    	if ( log == null ) {
    	    FramedResource target = (FramedResource) getTargetResource();
    	    if (target != null) {
    		String msg = ("unable to open log file \""+
    			      logfile
    			      + "\".");
    		target.getServer().errlog(this, msg);
    	    }
    	}
        }

        /**
         * Get the log file.
         * @return A File instance, or <strong>null</strong> if not set.
         */

        public File getLogfile() {
    	return (File) getValue(ATTR_LOGFILE, null);
        }

        /**
         * Get the list of request headers to dump.
         * @return An array of String containing the name of headers to dump, or
         * <strong>null</strong> if undefined.
         */

        public String[] getRequestHeaders() {
    	return (String[]) getValue(ATTR_REQUEST_HEADERS, null);
        }

        /**
         * Get the list of reply headers to dump.
         * @return An array of String containing the name of headers to dump,
         * or <strong>null</strong> if undefined.
         */

        public String[] getReplyHeaders() {
    	return (String[]) getValue(ATTR_REPLY_HEADERS, null);
        }

        /**
         * Traop setValue calls.
         * We maintain a compiled version of both the <code>request-headers</code>
         * and the <code>reply-headers</code> attributes, make sure they stay in
         * sync even when modified.
         */

        public void setValue(int idx, Object value) {
    	super.setValue(idx, value);
    	if ( idx == ATTR_REQUEST_HEADERS ) {
    	    synchronized(this) {
    		reqheaders = compileHeaders(new HttpRequestMessage()
    					    , (String[]) value);
    	    }
    	} else if (idx == ATTR_REPLY_HEADERS ) {
    	    synchronized(this) {
    		repheaders = compileHeaders(new HttpReplyMessage()
    					    , (String[]) value);
    	    }
    	} else if (idx == ATTR_LOGFILE ) {
    	    openLog();
    	}
        }

        /**
         * Log the given request/reply transaction.
         * Dump a record for the given transaction.
         * @param request The request to log.
         * @param reply It's associated reply.
         */

        protected void log(Request request, Reply reply) {
    	StringBuffer sb = new StringBuffer();
    	// Start a new log record:
    	sb.append("url=");
    	sb.append(request.getURL().toString());
    	sb.append('\n');
    	// Then log all request headers:
    	if ( reqheaders != null ) {
    	    for (int i = 0 ; i < reqheaders.length ; i++) {
    		HeaderDescription d = reqheaders[i];
    		if ( d == null )
    		    continue;
    		HeaderValue v = request.getHeaderValue(d);
    		if ( v != null ) {
    		    sb.append("request.");
    		    sb.append(d.getName());
    		    sb.append('=');
    		    sb.append(v.toString());
    		    sb.append('\n');
    		}
    	    }
    	}
    	// The log all reply headers:
    	if ( repheaders != null ) {
    	    for (int i = 0 ; i < repheaders.length ; i++) {
    		HeaderDescription d = repheaders[i];
    		if ( d == null )
    		    continue;
    		HeaderValue v = reply.getHeaderValue(d);
    		if ( v != null ) {
    		    sb.append("reply.");
    		    sb.append(d.getName());
    		    sb.append('=');
    		    sb.append(v.toString());
    		    sb.append('\n');
    		}
    	    }
    	}
    	writelog(sb.toString());
        }

        /**
         * Log the request.
         * @param request The request that has been handled.
         * @param reply It's associated reply.
         */

        public ReplyInterface outgoingFilter(RequestInterface req,
    					 ReplyInterface rep)
    	throws ProtocolException
        {
    	Reply   reply   = (Reply) rep;
    	Request request = (Request) req;
    	if ( ! reply.hasState(DONT_LOG) )
    	    log(request, reply);
    	return null;
        }

        /**
         * Initialize the filter.
         */

        public void initialize(Object values[]) {
    	super.initialize(values);
    	// Open the log file, ready for working:
    	if ( log == null )
    	    openLog();
    	// Compile request and reply headers to dump:
    	if ( reqheaders == null ) {
    	    String headers[] = getRequestHeaders();
    	    if ( headers != null )
    		reqheaders = compileHeaders(new HttpRequestMessage(), headers);
    	}
    	if ( repheaders == null ) {
    	    String headers[] = getReplyHeaders();
    	    if ( headers != null )
    		repheaders = compileHeaders(new HttpReplyMessage(), headers);
    	}
        }

    }