// ICPFilter.java
// $Id: ICPFilter.java,v 1.13 2000/08/16 21:38:04 ylafon Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// please first read the full copyright statement in file COPYRIGHT.HTML

package org.w3c.www.protocol.http.icp;

import java.util.Hashtable;
import java.util.StringTokenizer;

import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.URL;
import java.net.UnknownHostException;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.PrintStream;

import org.w3c.util.ObservableProperties;
import org.w3c.util.PropertyMonitoring;

import org.w3c.www.protocol.http.HttpException;
import org.w3c.www.protocol.http.HttpManager;
import org.w3c.www.protocol.http.PropRequestFilter;
import org.w3c.www.protocol.http.PropRequestFilterException;
import org.w3c.www.protocol.http.Reply;
import org.w3c.www.protocol.http.Request;

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

public class ICPFilter implements PropRequestFilter, PropertyMonitoring {
    /**
     * Properties - Our debug flag.
     */
    public static final String
    DEBUG_P = "org.w3c.www.protocol.http.icp.debug";
    /**
     * Properties - Our configuration file.
     */
    public static final String
    CONFIG_P = "org.w3c.www.protocol.http.icp.config";
    /**
     * Properties - Our own UDP port number.
     */
    public static final String
    PORT_P = "org.w3c.www.protocol.http.icp.port";
    /**
     * Properties - Our default timeout value.
     */
    public static final String
    TIMEOUT_P = "org.w3c.www.protocol.http.icp.timeout";
    /**
     * Properties - disable caching when fetching from a neighbour proxy.
     */
    public static final String
    DISABLE_CACHE_P = "org.w3c.www.protocol.http.icp.disable-cache";
    /**
     * The properties we are initialized from.
     */
    protected ObservableProperties props = null;
    /**
     * Our ICP engine.
     */
    protected ICPReceiver icp = null;
    /**
     * Our ICP neighbors.
     */
    ICPSender senders[] = null;
    /**
     * Our senders, indexed by InetAddress.
     */
    protected Hashtable friends = null;
    /**
     * Our default timeout value for waiting for replies (in ms).
     */
    protected long timeoutValue = 500;
    /**
     * Our we in debug mode ?
     */
    protected boolean debug = false;
    /**
     * Our sending and source port.
     */
    int port = -1;
    /**
     * Should we disablecaching when fetching through a proxy ?
     */
    protected boolean disableCache = true;

    public boolean propertyChanged(String name) {
	System.out.println("ICPFilter:"+name+": property changed.");
	return true;
    }

    protected DatagramSocket getSocket() {
	return icp.getSocket();
    }

    protected void createICPSender(String host, int dstport, String http) 
	throws UnknownHostException, MalformedURLException, SocketException
    {
	InetAddress addr   = InetAddress.getByName(host);
	URL         url    = new URL(http);
	ICPSender   sender = new ICPSender(this, port, addr, dstport, url);
	// Add it to the array of senders:
	if ( senders == null ) {
	    senders    = new ICPSender[1];
	    senders[0] = sender;
	} else {
	    ICPSender ns[]     = new ICPSender[senders.length+1];
	    System.arraycopy(senders, 0, ns, 0, senders.length);
	    ns[senders.length] = sender;
	    senders            = ns;
	}
	// Add it to our hashtable of hosts:
	byte    baddr[] = addr.getAddress();
	Long    key     = new Long((((long) dstport) << 32)
				    + ((baddr[0] & 0xff) << 24)
				    + ((baddr[1] & 0xff) << 16)
				    + ((baddr[2] & 0xff) << 8)
				    + (baddr[3] & 0xff));
	friends.put(key, sender);
	if ( debug )
	    System.out.println("icp: friend "+key+" http="+http);
    }

    /**
     * Parse the configuration file.
     */

    protected void parseConfiguration() {
	DataInputStream in = null;
	String          host = null;
	int             port = -1;
	String          http = null;
	File            file = props.getFile(CONFIG_P, new File("icp.conf"));
	try {
	    in = (new DataInputStream
		  (new BufferedInputStream
		   (new FileInputStream(file))));
	} catch (IOException ex) {
	    System.out.println("*** ICP, unable to read config file "
			       + file.getAbsolutePath());
	    return;
	}
	// Parse the file in:
	try {
	    for (String line = null; (line = in.readLine()) != null; ) {
		// Syntax (FIXME) 
		//   host udp-port http-location
		// | '#' comments
		if ( line.startsWith("#") || line.length() == 0 )
		    continue;
		StringTokenizer st = new StringTokenizer(line, " \t");
		host = st.nextToken();
		port = Integer.parseInt(st.nextToken());
		http = st.nextToken();
		createICPSender(host, port, http);
	    }
	} catch (Exception ex) {
	    System.out.println("*** ICP, unable to create "
			       + host + "@" + port + "[" + http + "]: "
			       + ex.getMessage());
	} finally {
	    try { if ( in != null) in.close(); } catch (IOException ex) {}
	}
    }

    /**
     * Get the sender object for the given InetAddress instance.
     * @param addr The InetAddress of the sender.
     * @return An ICPSender instance, if available, <strong>null</strong>
     * otherwise.
     */

    public ICPSender getSender(InetAddress addr, int port) {
	byte    baddr[] = addr.getAddress();
	Long    key     = new Long((((long) port) << 32)
				   + ((baddr[0] & 0xff) << 24)
				   + ((baddr[1] & 0xff) << 16)
				   + ((baddr[2] & 0xff) << 8)
				   + (baddr[3] & 0xff));
	return (ICPSender) friends.get(key);
    }

    /**
     * Locate the HTTP service of the proxy that has emitted that reply.
     * @param reply The reply emitted by the host that alos host the HTTP
     * service we are looking for.
     * @return The URL of the proxy, or <strong>null</strong> if no matching
     * proxy was found.
     */

    protected URL locateProxy(ICPReply reply) {
	ICPSender sender = getSender(reply.getSenderAddress()
				     , reply.getSenderPort());
	return (sender != null) ? sender.getProxyLocation() : null;
    }

    /**
     * Send the given query to all our neighbors.
     * @return The number of times we emitted the query.
     */

    protected int sendQuery(ICPQuery query) {
	int count = 0;
	if ( senders != null ) {
	    for (int i = 0 ; i < senders.length ; i++) {
		if ( debug )
		    System.out.println("icp: query@"
				       + senders[i].getAddress()
				       + "/" + senders[i].getPort()
				       + " for " + query.getURL());
		if ( senders[i].send(query) )
		    count++;
	    }
	}
	return count;
    }
	
    /**
     * Run the ICP query, and return the proxy we should go to.
     * @param url The URL we are looking for.
     * @return The URL of the proxy we should go to for that URL, or <strong>
     * null</strong> if none was found.
     */

    protected URL runQuery(ICPQuery query) {
	// Create a new waiter block for this query, and register it:
	ICPWaiter waiter = new ICPWaiter(query.getIdentifier());
	icp.addReplyWaiter(waiter);
	// Emit the query, and wait for a suitable reply:
	try {
	    long curtime = -1;
	    long nxttime = -1;
	    long timeout = timeoutValue;
	    int  sent    = sendQuery(query);
	    while ((sent > 0) && (timeout > 0)) {
		ICPReply reply = waiter.getNextReply(timeout);
		if (reply != null) {
		    sent--;
		    if ( reply.isHit() ) {
			return locateProxy(reply);
		    }
		} else {
		    // Our timeout has expired, notify failure
		    return null;
		}
		nxttime  = System.currentTimeMillis();
		timeout -= (nxttime -curtime);
		curtime  = nxttime;
	    }
	} finally {
	    icp.removeReplyWaiter(waiter);
	}
	return null;
    }

    /**
     * This filter doesn't handle exceptions.
     * @param request The request that triggered the exception.
     * @param ex The triggered exception.
     * @return Always <strong>false</strong>.
     */

    public boolean exceptionFilter(Request request, HttpException ex) {
	return false;
    }

    /**
     * Our ingoingFilter method.
     * This method emits (only for GET requestst currently) an ICP query
     * to all our neighbors, and wait for either one of them to
     * reply with a hit, or, our timeout value to expire.
     * <p>If a hit reply is received, we then use the corresponding proxy
     * to fullfill the request.
     * @param request The request that is about to be emitted.
     * @return Always <strong>null</strong>.
     */

    public Reply ingoingFilter(Request request) {
	if ( request.getMethod().equals("GET") 
	     && ( ! request.hasState(CacheFilter.STATE_NOCACHE) )
	     && ( ! request.hasState(CacheFilter.STATE_REVALIDATION)) ) {
	    ICPQuery query = icp.createQuery(request.getURL());
	    URL      proxy = runQuery(query);
	    if ( proxy != null ) {
		if ( debug )
		    System.out.println("*** routing "+request.getURL()
				       + " to "+proxy);
		// Disable caching and set proxy:
		if ( disableCache ) {
		    request.setState(CacheFilter.STATE_NOCACHE, Boolean.TRUE);
		}
		request.setProxy(proxy);
	    }
	}
	return null;
    }

    /**
     * Our outgoingFilter does nothing (at all).
     * @param request The request that has been processed.
     * @param reply The original reply (from origin server)
     * @return Always <strong>null</strong>.
     */

    public Reply outgoingFilter(Request request, Reply reply) {
	return null;
    }

    /**
     * This filter doesn't maintain dynamic state. 
     */

    public void sync() {
	return ;
    }

    /**
     * Initialize the ICP filter.
     * This is where we parse the configuration file in order to know
     * about our neighbors. We then register ourself to the HTTP manager.
     * @param manager The HTTP manager.
     * @exception PropRequestFilterException If the filter cannot 
     * launch its server part (listening for incomming ICP requests)
     */

    public void initialize(HttpManager manager) 
	throws PropRequestFilterException
    {
	// Setup our properties:
	props = manager.getProperties();
	props.registerObserver(this);
	port = props.getInteger(PORT_P, 2005);
	// Get property values:
	this.friends = new Hashtable(10);
	if ( debug = props.getBoolean(DEBUG_P, false) )
	    System.out.println("["+getClass().getName()+"]: debugging on");
	parseConfiguration();
	timeoutValue = props.getInteger(TIMEOUT_P, (int) timeoutValue);
	disableCache = props.getBoolean(DISABLE_CACHE_P, disableCache);
	// Initialize our ICPReceiver:
	try {
	    icp = new ICPReceiver(manager, this, port);
	} catch (SocketException ex) {
	    ex.printStackTrace();
	    throw new PropRequestFilterException(ex.getMessage());
	}
	if ( debug )
	    System.out.println("icp: listening on port "+port);
	manager.setFilter(this);
    }

}
