// ForwardFrame.java
// $Id: ForwardFrame.java,v 1.29 2003/04/16 13:40:12 ylafon Exp $
// (c) COPYRIGHT MIT and INRIA, 1998.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.jigsaw.proxy ;

import java.io.IOException;
import java.util.Enumeration;
import org.w3c.tools.resources.Attribute;
import org.w3c.tools.resources.AttributeRegistry;
import org.w3c.tools.resources.BooleanAttribute;
import org.w3c.tools.resources.FramedResource;
import org.w3c.tools.resources.ReplyInterface;
import org.w3c.tools.resources.RequestInterface;
import org.w3c.tools.resources.ResourceReference;
import org.w3c.tools.resources.StringAttribute;
import org.w3c.jigsaw.http.HTTPException;
import org.w3c.jigsaw.http.Reply;
import org.w3c.jigsaw.http.Request;
import org.w3c.jigsaw.http.httpd;
import org.w3c.jigsaw.frames.HTTPFrame;
import org.w3c.util.ObservableProperties;
import org.w3c.www.http.HTTP;
import org.w3c.www.http.HeaderDescription;
import org.w3c.www.http.HeaderValue;
import org.w3c.www.http.HttpFactory;
import org.w3c.www.http.HttpWarning;
import org.w3c.www.http.HttpInvalidValueException;
import org.w3c.www.protocol.http.HttpException;

public class ForwardFrame extends HTTPFrame {
    private static final boolean debug = false;
    private static boolean inited = false;
    private static final String nocache[] = { };

 //   private static final 
//    String micp = org.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;
    /**
     * The HTTP warning used to indicate a heuristic expiration time.
     */
    protected static HttpWarning WARN_HEURISTIC = null;

    static {
	HttpWarning w = null;
	Attribute a = null;
	Class     c = null;
	try {
	    c = Class.forName("org.w3c.jigsaw.proxy.ForwardFrame");
	} 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);
	// Build the heuristic expiration warning:
	w = HttpFactory.makeWarning(HttpWarning.HEURISTIC_EXPIRATION);
	w.setAgent("Jigsaw");
	w.setText("Heuristic expiration time used on this entry but not set"
		  +" by the upstream agent.");
	WARN_HEURISTIC = w;	
    }

    /**
     * The set of properties we inited from.
     */
    ObservableProperties props = null;
    /**
     * The HttpManager we use.
     */
    protected  org.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 ResourceReference lroot = null;

    public synchronized ResourceReference 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(org.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.
     * @exception HTTPException if processing the request failed.
     * @exception IOException if an IO error occurs.
     */

    protected org.w3c.www.protocol.http.Request dupRequest(Request request) 
	throws HTTPException, IOException
    {
	org.w3c.www.protocol.http.Request     req = null;
	String                            mth = request.getMethod();
	// Create a client request, and initialize its target & method:
	req = manager.createRequest();
	// reset user agentm as it will be set (or not set) by
	// the upstream request
	req.setUserAgent(null);
	req.setAccept(null);
	req.setURL(request.getURL());
	req.setMethod(mth);
	// If we can have 1xx headers to send back, setup an observer:
	if ((request.getMajorVersion() >= 1)
	    && (request.getMinorVersion() >= 1)) {
	    req.setObserver(new ProxyRequestObserver(request, this));
	}
	// 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(Request.H_PROXY_AUTHORIZATION, null);
	req.setHeaderValue(Reply.H_TRANSFER_ENCODING, null);
	req.setHeaderValue(Request.H_TE, null);
	req.setHeaderValue(Request.H_TRAILER, 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]);
	}
	// check if nasty people are mixing Content-Length and chunking
	if (req.getContentLength() >= 0 ) {
	    String te[] = request.getTransferEncoding() ;
	    if ( te != null ) {
		for (int i = 0 ; i < te.length ; i++) {
		    if (te[i].equals("chunked")) {
			req.setContentLength(-1);
		    }
		}
	    }
	}
	// 14.31 decrement the value of MaxForward for TRACE and OPTIONS
	if ((request.getMaxForwards() != -1) && 
	    (request.getMethod().equals("TRACE") || 
	     request.getMethod().equals("OPTIONS"))) {
	    req.setMaxForwards(request.getMaxForwards()-1);
	}
	// 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
			     , org.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_PROXY_AUTHENTICATE, null);
	reply.setHeaderValue(Reply.H_PUBLIC, null);
	reply.setHeaderValue(Reply.H_TRANSFER_ENCODING, null);
	reply.setHeaderValue(Reply.H_UPGRADE, null);
	reply.setHeaderValue(Reply.H_TRAILER, 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]);
	}
	// check if nasty people are mixing Content-Length and chunking
	if (reply.getContentLength() >= 0 ) {
	    String te[] = rep.getTransferEncoding() ;
	    if ( te != null ) {
		for (int i = 0 ; i < te.length ; i++) {
		    if (te[i].equals("chunked")) {
			reply.setContentLength(-1);
		    }
		}
	    }
	}
	// Update the via route:
	reply.addVia(getVia());
	// Update the reply output stream:
	try {
	    reply.setStream(rep.getInputStream());
	} catch (Exception ex) {};
	// if reply is using bad HTTP version we should close it
	if (rep.getMajorVersion() == 0) {
	    reply.setKeepConnection(false);
	}
	// if HTTP/1.0 and no Content-Length also...
	if ((rep.getMajorVersion() == 1) && 
	    (rep.getMajorVersion() == 0) &&
	    (reply.getContentLength() == -1)) {
	    reply.setKeepConnection(false);
	}
	// check the age
	int age = rep.getAge();
	if (age >= 0 ) {
	    // check if it is an heuristic expiration without a warning
	    if (age > 86400) {
		if ((rep.getExpires() == -1) && (rep.getMaxAge() == -1) &&
		    (rep.getSMaxAge() == -1)) {
		    // rfc2616: 13.2.4 on behalf of a bad upstream server
		    HttpWarning w[] = rep.getWarning();
		    boolean doit = true;
		    if (w != null) {
			for (int i=0; doit && (i<w.length); i++) {
			    doit = (w[i].getStatus() != 113);
			}
		    }
		    if (doit) {
			reply.addWarning(WARN_HEURISTIC);
		    }
		}
	    }
	}
	reply.setProxy(true);
	return reply;
    }
    /**
     * Perform the given proxied request.
     * @param request The request to perform.
     * @return A Reply instance.
     * @exception org.w3c.tools.resources.ProtocolException if processing
     * the request failed.
     * @exception org.w3c.tools.resources.ResourceException if the resource
     * got a fatal error.
     */

    public ReplyInterface perform(RequestInterface ri) 
	throws org.w3c.tools.resources.ProtocolException,
	       org.w3c.tools.resources.ResourceException
    {
	Request request = (Request) ri;
	Reply   reply  = null;
	boolean stated = false;
	// check the expectations
	if (!checkExpect(request)) {
	    reply = createDefaultReply(request, HTTP.EXPECTATION_FAILED);
	    reply.setContent("The requested expectation could not be"+
			     " met by the resource");
	    return reply;
	}
	// Perform the request:
	try {
	    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);
		}
	    } 

	    org.w3c.www.protocol.http.Request     req = dupRequest(request);
	    org.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 (HTTPException shex) { 
	    // An server-side HTTP-related error happened
	    if (debug) {
		System.out.println("HTTP Server Exception while running"
				   +" request:");
		shex.printStackTrace();
	    }
	    // Make sure the request is accounted:
	    if ( ! stated ) {
		synchronized(this) {
		    reqcount++;
		    reqerred++;
		}
		stated = true;
	    }
	    Reply r = (Reply) shex.getReply();
	    if (reply == null) {
		// Send an appropriate error message back:
		reply = request.makeReply(HTTP.BAD_GATEWAY);
		boolean showerr = 
		    props.getBoolean(httpd.DISPLAY_URL_ON_ERROR_P, false);
		
		if (showerr) {
		    reply.setContent("An HTTP error occured while getting: "
				     + "<p><strong>"+request.getURL()
				     + "</strong>"
				     + "<p>Details \""+shex.getMessage()+"\"."
				     + "<hr>Generated by <i>"
				     + getServer().getURL());
		} else {
		    reply.setContent("An HTTP error occured while getting the "
				     + "requested URI.</p>"
				     + "<hr>Generated by <i>"
				     + getServer().getURL());
		}
		reply.setContentType(org.w3c.www.mime.MimeType.TEXT_HTML);
	    }
	} catch (HttpInvalidValueException iex) { 
	    // An server-side HTTP-related error happened
	    if (debug) {
		System.out.println("Parsing Exception while running"
				   +" request:");
		iex.printStackTrace();
	    }
	    // Make sure the request is accounted:
	    if ( ! stated ) {
		synchronized(this) {
		    reqcount++;
		    reqerred++;
		}
		stated = true;
	    }
	    reply = request.makeReply(HTTP.BAD_GATEWAY);
	    boolean showerr = 
		props.getBoolean(httpd.DISPLAY_URL_ON_ERROR_P, false);
	    
	    if (showerr) {
		reply.setContent("An HTTP error occured while getting: "
				 + "<p><strong>"+request.getURL()
				 + "</strong>"
				 + "<p>Details \""+iex.getMessage()+"\"."
				 + "<hr>Generated by <i>"
				 + getServer().getURL());
	    } else {
		reply.setContent("An HTTP error occured while getting the "
				 + "requested URI.</p>"
				 + "<hr>Generated by <i>"
				 + getServer().getURL());
	    }
	    reply.setContentType(org.w3c.www.mime.MimeType.TEXT_HTML);
	} catch (HttpException hex) { 
	    // An HTTP-related error happened
	    if (debug) {
		System.out.println("HTTP Exception while running request:");
		hex.printStackTrace();
	    }
	    // Make sure the request is accounted:
	    if ( ! stated ) {
		synchronized(this) {
		    reqcount++;
		    reqerred++;
		}
		stated = true;
	    }
	    org.w3c.www.protocol.http.Reply rep = hex.getReply();
	    if (rep != null) {
		try {
		    reply = dupReply(request, rep);
		} catch (IOException ioex) {
		    reply = null;
		}
	    }
	    if (reply == null) {
		// Send an appropriate error message back:
		String msg = hex.getMessage();
		if (msg.startsWith("Unable to contact target server ")) {
		    reply = request.makeReply(HTTP.GATEWAY_TIMEOUT);
		} else {
		    reply = request.makeReply(HTTP.BAD_GATEWAY);
		}
		boolean showerr = 
		    props.getBoolean(httpd.DISPLAY_URL_ON_ERROR_P, false);
		
		if (showerr) {
		    reply.setContent("An HTTP error occured while getting: "
				     + "<p><strong>"+request.getURL()
				     + "</strong>"
				     + "<p>Details \""+hex.getMessage()+"\"."
				     + "<hr>Generated by <i>"
				     + getServer().getURL());
		} else {
		    reply.setContent("An HTTP error occured while getting the "
				     + "requested URI.</p>"
				     + "<hr>Generated by <i>"
				     + getServer().getURL());
		}
		reply.setContentType(org.w3c.www.mime.MimeType.TEXT_HTML);
	    }
	} 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);
	    boolean showerr = props.getBoolean(httpd.DISPLAY_URL_ON_ERROR_P, 
					       false);
	    
	    if (showerr) {
		reply.setContent("An HTTP error occured while getting: "
				 + "<p><strong>"+request.getURL()+"</strong>"
				 + "<p>Details \""+ex.getMessage()+"\"."
				 + "<hr>Generated by <i>"
				 + getServer().getURL());
	    } else {
		reply.setContent("An HTTP error occured while getting the "
				 + "requested URI.</p>"
				 + "<hr>Generated by <i>"
				 + getServer().getURL());
	    }
	    reply.setContentType(org.w3c.www.mime.MimeType.TEXT_HTML);
	}
	return reply;
    }

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

    public synchronized void notifyUnload() {
	if (manager != null) {
	    manager.sync();
	}
	super.notifyUnload();
    }

    /**
     * companion to initialize, called after the register
     */

    public void registerResource(FramedResource resource) {
	super.registerOtherResource(resource);
	this.props = getResource().getServer().getProperties();
	synchronized(this.getClass()) {
	    if (!inited) {
		// Register the client side properties (if still needed):
		httpd server = (httpd) getServer();
		server.registerPropertySet(new ProxyProp("proxy", server));
		inited = true;
	    }
	}
	manager = org.w3c.www.protocol.http.HttpManager.getManager(props);
    }

    public void initialize(Object values[]) {
	super.initialize(values) ;
    }
}
