// ForwardDirectory.java
// $Id: ForwardDirectory.java,v 1.20 1997/12/19 16:58:46 ylafon Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package w3c.jigsaw.proxy ;

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

import w3c.util.*;
import w3c.tools.store.*;
import w3c.www.http.*;
import w3c.jigsaw.http.*;
import w3c.jigsaw.resources.*;

import w3c.www.protocol.http.cache.CacheFilter;

/**
 * A forwarder module for Jigsaw.
 * This resource is the base class for proxying and mirroring in Jigsaw, it
 * just provide default methods for converting server-side requests into
 * client-side requests, and client-side replies into server-side replies.
 */

public class ForwardDirectory extends FilteredResource {
    private static final boolean debug = true;
    private static boolean inited = false;
    private static final String nocache[] = { };

    private static final 
    String micp = w3c.www.protocol.http.micp.MICPFilter.STATE_HOW;

    /**
     * Attribute index - The local resource, if server-wide request.
     */
    protected static int ATTR_LOCAL_ROOT = -1;
    /**
     * Attribute index - The received by attribute of that proxy.
     */
    protected static int ATTR_RECEIVED_BY = -1;
    /**
     * Attribute index - Try to trace how the request has been processed.
     */
    protected static int ATTR_TRACEREQ = -1;

    static {
	Attribute a = null;
	Class     c = null;
	try {
	    c = Class.forName("w3c.jigsaw.proxy.ForwardDirectory");
	} catch (Exception ex) {
	    ex.printStackTrace();
	    System.exit(1);
	}
	// Declare the local root attribute:
	a = new StringAttribute("local-root"
				, null
				, Attribute.EDITABLE);
	ATTR_LOCAL_ROOT = AttributeRegistry.registerAttribute(c, a);
	// Declare the received by attribute:
	a = new StringAttribute("received-by"
				, null
				, Attribute.EDITABLE);
	ATTR_RECEIVED_BY = AttributeRegistry.registerAttribute(c, a);
	// Declare the received by attribute:
	a = new BooleanAttribute("trace-request"
				, null
				, Attribute.EDITABLE);
	ATTR_TRACEREQ = AttributeRegistry.registerAttribute(c, a);
    }

    /**
     * The set of properties we inited from.
     */
    ObservableProperties props = null;
    /**
     * The HttpManager we use.
     */
    protected w3c.www.protocol.http.HttpManager manager = null;
    /**
     * Statistics - Number of hits.
     */
    public int cache_hits = 0;
    /**
     * Statistics - Number of misses.
     */
    public int cache_misses = 0;
    /**
     * Statistics - Number of successfull revalidations.
     */
    public int cache_revalidations = 0;
    /**
     * Statistics - Number of successfull revalidations.
     */
    public int cache_retrievals = 0;
    /**
     * Statistics - Number of requests that didn't use cache.
     */
    public int cache_nocache = 0;
    /**
     * Statistics - Number of requests handled.
     */
    public int reqcount = 0;
    /**
     * Statistics - Number of ICP redirects.
     */
    public int cache_icps = 0;
    /**
     * Statistics - Errors.
     */
    public int reqerred = 0;

    /**
     * Get the local root resource name.
     */

    public String getLocalRoot() {
	return getString(ATTR_LOCAL_ROOT, null);
    }

    /**
     * Get the received by attribute value.
     * <p>If this attribute is not defined, it will default to the name of the
     * host running the proxy.
     * @return A String.
     */

    public String getReceivedBy() {
	String value = getString(ATTR_RECEIVED_BY, null);
	if ( value == null )
	    if (getServer().getPort() == 80)
		value = getServer().getHost();
	    else
		value = getServer().getHost() + ":" + getServer().getPort();
	return value;
    }

    /**
     * Should we try to trace request path ?
     * @return A boolean.
     */

    public boolean getTraceRequest() {
	return getBoolean(ATTR_TRACEREQ, false);
    }

    /**
     * Get the value of the <code>via</code> header to be added.
     * @return A String encoded value for the header.
     */

    private String via = null;
    public synchronized String getVia() {
	if ( via == null ) 
	    via = "1.1 "+getReceivedBy()+" ("+getServer().getSoftware()+")";
	return via;
    }

    /**
     * Get the local root resource to use for internal requests.
     */

    protected HTTPResource lroot = null;
    public synchronized HTTPResource getLocalRootResource() {
	if ( lroot == null ) {
	    String lname = getLocalRoot();
	    if ( lname != null ) 
		lroot = getServer().loadResource(lname);
	}
	return lroot;
    }

    /**
     * Update relevant statistics (kind of a hack).
     */

    protected void updateStatistics(w3c.www.protocol.http.Request r) {
	reqcount++;
	Integer how = (Integer) r.getState(CacheFilter.STATE_HOW);
	if ( how != null ) {
	    synchronized(this) {
		switch(how.intValue()) {
		  case 1:
		      cache_hits++;
		      break;
		  case 2:
		      cache_misses++;
		      break;
		  case 3:
		      cache_revalidations++;
		      break;
		  case 4:
		      cache_retrievals++;
		      break;
		}
	    }
	} else if ( ! r.hasState(micp) ) {
	    synchronized(this) {
		cache_nocache++;
	    }
	} else {
	    synchronized(this) {
		cache_icps++;
	    }
	}
    }

    /**
     * Duplicate a server side request into a client side request.
     * @param request The server side request.
     * @return A Client side request.
     */

    protected w3c.www.protocol.http.Request dupRequest(Request request) 
	throws HTTPException, IOException
    {
	w3c.www.protocol.http.Request     req = null;
	String                            mth = request.getMethod();
	// Create a client request, and initialize its target & method:
	req = manager.createRequest();
	req.setURL(request.getURL());
	req.setMethod(mth);
	// If this is a two stage method, setup an observer:
	if ((request.getMajorVersion() >= 1)
	    && (request.getMinorVersion() >= 1)
	    && (mth.equals("PUT") || mth.equals("POST"))) 
	    req.setObserver(new ProxyRequestObserver(request));
	// Update the client request fields:
	Enumeration e = request.enumerateHeaderDescriptions();
	while ( e.hasMoreElements() ) {
	    HeaderDescription d = (HeaderDescription) e.nextElement();
	    HeaderValue       v = request.getHeaderValue(d);
	    if ( v != null )
		req.setHeaderValue(d, v);
	}
	// Get rid of all hop-by-hop headers:
	req.setHeaderValue(Reply.H_CONNECTION, null);
	req.setHeaderValue(Reply.H_PROXY_CONNECTION, null);
	req.setHeaderValue(Reply.H_PUBLIC, null);
	req.setHeaderValue(Reply.H_PROXY_AUTHENTICATE, null);
	req.setHeaderValue(Reply.H_TRANSFER_ENCODING, null);
	req.setHeaderValue(Reply.H_UPGRADE, null);
	req.removeHeader("keep-alive");
	// By removing that HOST, we make sure the real Host header gets
	// computed by the client side API
	req.setHeaderValue(Request.H_HOST,null);
	// Get rid of more hop by hop headers:
	String conn[] = request.getConnection();
	if ( conn != null ) {
	    for (int i = 0 ; i < conn.length ; i++) 
		req.removeHeader(conn[i]);
	}
	// Fix versions mismatches:
	if ( request.hasPragma("no-cache") )
	    req.setNoCache(nocache);
	// Add the via clause:
	req.addVia(getVia());
	// Update the request output stream:
	req.setOutputStream(request.getInputStream());
	return req;
    }

    /**
     * Duplicate the given client side reply into a server side one.
     * Perform any actions requested by HTTP/1.1.
     * @param request The request ebing processed.
     * @param reply The reply to clone.
     * @return A server-side Reply instance.
     * @exception HTTPException If some HTTP errors occured in the process.
     * @exception IOException If setting the streams failed.
     */

    protected Reply dupReply(Request request
			     , w3c.www.protocol.http.Reply rep) 
	throws HTTPException, IOException
    {
	Reply reply = request.makeReply(rep.getStatus());
	// get rid of "by default" headers wchich SHOULD NOT be modified
	reply.setHeaderValue(Reply.H_SERVER, null);
        // Duplicate reply header values:
	Enumeration e = rep.enumerateHeaderDescriptions();
	while ( e.hasMoreElements() ) {
	    HeaderDescription d = (HeaderDescription) e.nextElement();
	    HeaderValue       v = rep.getHeaderValue(d);
	    if ( v != null )
		reply.setHeaderValue(d, v);
	}
	// Get rid of hop by hop headers:
	reply.setHeaderValue(Reply.H_CONNECTION, null);
	reply.setHeaderValue(Reply.H_PROXY_CONNECTION, null);
	reply.setHeaderValue(Reply.H_PUBLIC, null);
	reply.setHeaderValue(Reply.H_TRANSFER_ENCODING, null);
	reply.setHeaderValue(Reply.H_UPGRADE, null);
	reply.removeHeader("keep-alive");
	// Get rid of the fields enumerated in the connection header:
	String conn[] = rep.getConnection();
	if (conn != null) {
	    for (int i = 0 ; i < conn.length ; i++) 
		reply.removeHeader(conn[i]);
	}
	// Update the via route:
	reply.addVia(getVia());
	// Update the reply output stream:
	reply.setStream(rep.getInputStream());
	reply.setProxy(true);
	return reply;
    }

    /**
     * Perform the given proxied request.
     * @param request The request to perform.
     * @param filters The set of filters to apply.
     * @return A Reply instance.
     */

    public Reply perform(Request request) 
	throws HTTPException, ClientException
    {
	Reply   reply  = null;
	boolean stated = false;
	// Perform the request:
	if (request.getMaxForwards() != -1) { // 14.31 decrement the value
	    if (request.getMaxForwards() == 0) {
		if (request.getMethod().equals("TRACE") ||
		    request.getMethod().equals("OPTIONS"))
		    return super.perform(request);
	    } else {
		request.setMaxForwards(request.getMaxForwards()-1);
	    }
	} 
	try {
	    w3c.www.protocol.http.Request     req = dupRequest(request);
	    w3c.www.protocol.http.Reply       rep = null;
	    // Perform the request
	    rep = manager.runRequest(req);
	    // Dump back the client reply into a server reply:
	    reply = dupReply(request, rep);
	    // Update statistics:
	    updateStatistics(req);
	    stated = true;
	    // Trace the request (debug):
	    if ( getTraceRequest() ) {
		if ( req.hasState(CacheFilter.STATE_HOW) ) {
		    System.out.println(request.getURL()+": "+
				       CacheFilter.getHow(req));
		}
		if ( req.hasState(micp) )
		    System.out.println(req.getURL()+": (icp) "+
				       req.getState(micp));
	    }
	} catch (Exception ex) {
	    // Debug trace (when debuggging):
	    if ( debug ) {
		System.out.println("Exception while running request:");
		ex.printStackTrace();
	    }
	    // Make sure the request is accounted:
	    if ( ! stated ) {
		synchronized(this) {
		    reqcount++;
		    reqerred++;
		}
		stated = true;
	    }
	    // Send an appropriate error message back:
	    reply = request.makeReply(HTTP.GATEWAY_TIMEOUT);
	    reply.setContent("An HTTP error occured while getting: "
			     + "<p><strong>"+request.getURL()+"</strong>"
			     + "<p>Details \""+ex.getMessage()+"\"."
			     + "<hr>Generated by <i>"
			     + getServer().getURL());
	}
	return reply;
    }

    /**
     * This resource is being unloaded.
     * Tell the HttpManager to save any pending data to stable storage.
     */

    public synchronized void notifyUnload() {
        manager.sync();
	super.notifyUnload();
    }

    public void initialize(Object values[]) {
	super.initialize(values) ;
	// Initialize our HttpManager:
	ObservableProperties props = getServer().getProperties();
	synchronized(this.getClass()) {
	    if (!inited) {
		// Register the client side properties (if still needed):
		httpd server = getServer();
		server.registerPropertySet(new ProxyProp("proxy", server));
		server.registerPropertySet(new CacheProp("cache", server));
		// Make sure the cache get some correct default directory:
		File c = props.getFile(CacheFilter.CACHE_DIRECTORY_P, null);
		if ( c == null ) {
		    c = new File(getServer().getConfigDirectory(), "cache");
		    props.putValue(CacheFilter.CACHE_DIRECTORY_P,c.toString());
		}
		inited = true;
	    }
	}
	manager = w3c.www.protocol.http.HttpManager.getManager(props);
    }
}
