// CookieFilter.java
// $Id: CookieFilter.java,v 1.5 1998/01/22 14:38:08 bmahe Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

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

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

import org.w3c.util.*;
import org.w3c.www.protocol.http.*;
import org.w3c.www.http.*;


class DomainNode {

  Hashtable nodes       = null; // < String , DomainNode >
  HttpSetCookie cookies [] = null;
  int nbcookies         = 0;

  protected boolean sameCookies(HttpSetCookie c1, HttpSetCookie c2) {
    if (c2.getPath() == null && c1.getPath() == null)
      return (c1.getName().equals(c2.getName()));
    else  if (c2.getPath() == null || c1.getPath() == null)
      return false;
    else return ((c1.getName().equals(c2.getName())) &&
		 (c1.getPath().equals(c2.getPath())));
  }

  protected void addCookie(HttpSetCookie cookie) {
    int i = 0;
    while (i < nbcookies) {
      if (sameCookies(cookie,cookies[i])) {
	cookies[i] = cookie;
	return;
      }
      i++;
    }
    if (nbcookies < cookies.length) {
      cookies[nbcookies++] = cookie;
    } else {
      HttpSetCookie ncookies [] = new HttpSetCookie[cookies.length +1];
      System.arraycopy(cookies,0,ncookies,0,cookies.length);
      ncookies[nbcookies++] = cookie;
      cookies = ncookies;
    }
  }

  protected void sync(FileWriter writer) 
    throws IOException
  {
    if (nbcookies > 0) {
      for (int i = 0; i < nbcookies; i++) {
	if (cookies[i].getMaxAge() > 0)
	  writer.write(DomainTree.cookie2String(cookies[i]));
      }
    }
    Enumeration e = nodes.elements();
    DomainNode node = null;
    while (e.hasMoreElements()) {
      node = (DomainNode)e.nextElement();
      node.sync(writer);
    }
  }

  DomainNode() {
    nodes     = new Hashtable(2);
    cookies   = new HttpSetCookie[1];
    nbcookies = 0;
  }

}

class DomainTree {

  Hashtable nodes        = null; // < String , DomainNode >

  protected static String cookie2String(HttpSetCookie cookie) {
    Date date = new Date();
    return (cookie.getDomain()+ //0
	    "\t"+
	    String.valueOf(cookie.getSecurity()).toUpperCase()+ //1
	    "\t"+
	    cookie.getPath()+ //2
	    "\t"+
	    (cookie.getMaxAge()*1000+date.getTime())+ //3
	    "\t"+
	    cookie.getVersion()+ //4
	    "\t"+
	    cookie.getName()+ //5
	    "\t"+
	    cookie.getValue()+ //6
	    "\n");
  }

  protected HttpSetCookie string2Cookie(String cookie[]) {
    HttpSetCookie cook = new HttpSetCookie();
    cook.setDomain(cookie[0]);
    cook.setSecurity(Boolean.getBoolean(cookie[1].toLowerCase()));
    cook.setPath(cookie[2]);
    long expire = Long.parseLong(cookie[3]);
    long now = (new Date()).getTime();
    cook.setMaxAge((int) ((expire - now) / 1000));
    cook.setVersion(Integer.parseInt(cookie[4]));
    cook.setName(cookie[5]);
    cook.setValue(cookie[6]);
    return cook;
  }

  protected synchronized void loadCookies(File file) 
    throws FileNotFoundException
  {
    try {
      FileReader reader = new FileReader(file);
      String cookie[] = new String[8];
      int i = 0;
      int ch;
      StringBuffer buffer = new StringBuffer(30);
      while ((ch = reader.read()) != -1) {
	switch (ch) {
	case '#':
	  while ((ch = reader.read()) != '\n')
	    if (ch == -1) return;
	  break;
	case '\n':
	  if (i > 0) {
	    cookie[i++] = buffer.toString();
	    HttpSetCookie setcookie = string2Cookie(cookie);
	    if (setcookie.getMaxAge() > 0)
	      insertCookie(setcookie);
	    buffer = new StringBuffer(30);
	    cookie = new String[8];
	    i = 0;
	  }
	  break;
	case '\t':
	case ' ':
	  cookie[i++] = buffer.toString();
	  buffer = new StringBuffer(30);
	  break;
	default:
	  buffer.append((char) ch);
	}
      }
    } catch (IOException ex) {
      System.out.println(ex.getMessage());
      ex.printStackTrace();
    }
  }

  protected synchronized void sync(File file) {
    Enumeration e     = nodes.elements();
    DomainNode node   = null;
    FileWriter writer = null;
    try {
      writer = new FileWriter(file);
      writer.write("# Jigsaw client HTTP Cookie File\n");
      writer.write("# This is a generated file!  Do not edit.\n\n");
      while (e.hasMoreElements()) {
	node = (DomainNode)e.nextElement();
	node.sync(writer);
      } 
      writer.close();
    } catch (IOException ex) {
      System.out.println(ex.getMessage());
      ex.printStackTrace();
    } finally {
      try {if (writer != null) writer.close();} catch (Exception ex2) {}
    }
  }

  protected boolean isIp(String domain) {
    int last = domain.length()-1;
    return ((domain.charAt(last)>='0') && (domain.charAt(last)<='9'));
  }

  protected String[] domainParts(String dom) {
    if (dom == null)
      return null;
    String domain = new String(dom);
    Vector V = new Vector(5);
    int i = 0; int j = 0; int max = domain.length(); 
    j = domain.indexOf('.');
    if (j == 0)
      i = 1;
    while (j != -1) {
      j = domain.indexOf('.',i);
      if (j != -1)
	V.addElement(domain.substring(i,j));
      else
	V.addElement(domain.substring(i));
      i = j+1;
    }
    if (V.size() == 0)
      return null;
    String parts [] = new String[V.size()];
    V.copyInto(parts);
    return parts;
  }


  protected HttpCookie setCookie2Cookie(HttpSetCookie setcookie) {
    HttpCookie cookie = new HttpCookie();
    if (setcookie != null) {
      cookie.setName(setcookie.getName());
      cookie.setValue(setcookie.getValue());
      cookie.setDomain(setcookie.getDomain());
      cookie.setPath(setcookie.getPath());
      cookie.setVersion(setcookie.getVersion());
      return cookie;
    }
    return null;
  }

  protected void addMatchingPathCookiesInVector(HttpSetCookie cookieArray[],
						String path,
						Vector V) {
    int i = 0;
    while ( i < cookieArray.length ) {
      if (path.equals("/")) 
	V.addElement(setCookie2Cookie(cookieArray[i]));
      else  if (cookieArray[i].getPath() != null) {
        if (path.startsWith(cookieArray[i].getPath())) {
	  // transform SetCookie in Cookie
	  V.addElement(setCookie2Cookie(cookieArray[i]));
	}
      }
      i++;
    }
  }

  public HttpCookieList getCookies(URL url) {
    String domain   = url.getHost();
    String path     = url.getFile();
    String parts [] = domainParts(domain);
    if (parts == null) //FIXME Exception
      return null;
    Vector V = new Vector(5);
    int i = 0;
    DomainNode node = null;
    Hashtable childs = nodes;
    
    if (isIp(domain)) {
      node = (DomainNode) childs.get(parts[i]);
      while ( i < parts.length ) {
	if (node == null)
	  return null;
	if (node.nbcookies > 0) {
	  addMatchingPathCookiesInVector(node.cookies, path, V);
	}
	node   = (DomainNode)childs.get(parts[++i]);
	if (node == null)
	  break;
	childs = node.nodes;
      }
    } else {
      i = parts.length - 1;      
      node = (DomainNode) childs.get(parts[i]);
      while ( i >= 0 ) {
	if (node == null)
	  return null;
	if (node.nbcookies > 0) {
	  addMatchingPathCookiesInVector(node.cookies, path, V);
	}
	if (i > 0)
	  node = (DomainNode)childs.get(parts[--i]);
	else 
	  node = null;
	if (node == null)
	  break;
	childs = node.nodes;
      }
    }
    if (V.size() == 0)
      return  null;
    HttpCookie cookieArray[] = new HttpCookie[V.size()];
    V.copyInto(cookieArray);
    return HttpFactory.makeCookieList(cookieArray);
  }

  public void insertCookie(HttpSetCookie cookie) {
    String domain  = cookie.getDomain();
    String parts[] = domainParts(domain);
    if (parts == null) //FIXME Exception
      return;
    int i = 0;
    DomainNode node = null;
    Hashtable childs = nodes;
    if (isIp(domain)) {
      node = (DomainNode) childs.get(parts[i]);
      while ( true ) {
	if (node == null) {
	  node = new DomainNode();
	  childs.put(parts[i], node);
	}
	if (i == parts.length - 1) {
	  node.addCookie(cookie);
	  return;
	}
	node   = (DomainNode)childs.get(parts[++i]);
	if (node == null) {
	  node = new DomainNode();
	  childs.put(parts[i], node);
	}
	childs = node.nodes;
      }
    } else {
      i = parts.length - 1;      
      node = (DomainNode) childs.get(parts[i]);
      while ( true ) {
	if (node == null) {
	  node = new DomainNode();
	  childs.put(parts[i], node);
	}
	if (i == 0) {
	  node.addCookie(cookie);
	  return;
	}
	node   = (DomainNode)childs.get(parts[--i]);
	if (node == null) {
	  node = new DomainNode();
	  childs.put(parts[i], node);
	}
	childs = node.nodes;
      }
    }
  }

  DomainTree() {
    this.nodes = new Hashtable(10);
  }

}

/**
 * Client side CookieFilter :
 * @author Benoit Mahe <bmahe@sophia.inria.fr>
 */


public class CookieFilter implements PropRequestFilter {

  /**
   * The absolute Path of the file use to store cookies.
   */
  public static final 
  String COOKIES_FILE_P = "org.w3c.www.protocol.http.cookie.file";

  private static final String defaultFileName = "cookie";

  private static DomainTree root = null;

  static {
    root = new DomainTree();
  }

  protected HttpManager manager       = null;

  private static File cookiefile    = null;

  /**
   * The request pre-processing hook.
   * Before each request is launched, all filters will be called back through
   * this method. They will generally set up additional request header
   * fields to enhance the request.
   * @param request The request that is about to be launched.
   * @return An instance of Reply if the filter could handle the request,
   * or <strong>null</strong> if processing should continue normally.
   * @exception HttpException If the filter is supposed to fulfill the
   * request, but some error happened during that processing.
   */
  public Reply ingoingFilter(Request request) 
    throws HttpException
  {
    HttpCookieList cookielist = root.getCookies(request.getURL());
    if (cookielist != null)
      request.setCookie(cookielist);
    return null;
  }


  /**
   * The request post-processing hook.
   * After each request has been replied to by the target server (be it a 
   * proxy or the actual origin server), each filter's outgoingFilter
   * method is called.
   * <p>It gets the original request, and the actual reply as a parameter,
   * and should return whatever reply it wants the caller to get.
   * @param request The original (handled) request.
   * @param reply The reply, as emited by the target server, or constructed
   * by some other filter.
   * @exception HttpException If the reply emitted by the server is not
   * a valid HTTP reply.
   */
  public Reply outgoingFilter(Request request, Reply reply) 
    throws HttpException
  {
    HttpSetCookieList list = reply.getSetCookie();
    if (list != null) {
      HttpSetCookie [] cooks = list.getSetCookies();
      if (cooks != null) {
 	int i = 0;
	while (i < cooks.length) {
	  if (cooks[i].getDomain() == null)
	    cooks[i].setDomain(request.getURL().getHost());
	  root.insertCookie(cooks[i++]);
	}
      }
    }
    return reply;
  }

  /**
   * An exception occured while talking to target server.
   * This method is triggered by the HttpManager, when the target server
   * (which can be a proxy for that request) was not reachable, or some
   * network error occured while emitting the request or reading the reply
   * headers.
   * @param request The request whose processing triggered the exception.
   * @param ex The exception that was triggered.
   * @return A boolean, <strong>true</strong> if that filter did influence
   * the target server used to fulfill the request, and it has fixed the 
   * problem in such a way that the request should be retried.
   */
  public boolean exceptionFilter(Request request, HttpException ex) {
    return false;
  }

  /** 
   * Synchronized any pending state into stable storage.
   * If the filter maintains some in-memory cached state, this method
   * should ensure that cached data are saved to stable storage.
   */
  public void sync() {
    root.sync(cookiefile);
  }

  /**
   * Initialize this filter, using the provided manager.
   * During initialization, it is up to the filter to install itself
   * in the manager, by invoking the appropriate <code>setFilter</code>
   * method.
   * @param manager The HttpManager initializing the filter.
   * @exception FilterInitException If the filter couldn't be 
   * initialized properly.
   */
  public void initialize(HttpManager manager) {
    this.manager = manager;
    ObservableProperties props = manager.getProperties();
    String filepath = (String)props.get(COOKIES_FILE_P);
    if (filepath == null) {
      cookiefile = new File(defaultFileName);
    } else {
      cookiefile = new File(filepath);
    }
    if (cookiefile.exists()) {
      // load all the Cookie filter and register them in the manager
      try {
	root.loadCookies(cookiefile);
      } catch (FileNotFoundException ex) {
	System.out.println(ex.getMessage());
	ex.printStackTrace();
      }
    }
    manager.setFilter(this);
  }

}
