/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.javatest.httpd;

import java.util.Enumeration;
import java.util.Hashtable;
import java.io.PrintWriter;

import com.sun.javatest.util.DynamicArray;
import com.sun.javatest.util.I18NResourceBundle;

/**
 * Registry for JT Harness classes which wish to publish pages for the http
 * server.  Each handler is associated with a particular URL.  As a policy,
 * only the first directory name can be used as a key.  For example,
 * you cannot register <tt>/foo/bar/</tt>, you can only register <tt>/foo/</tt>.
 *
 * <p>
 * Rules about registration:
 * <ul>
 * <li>Leading and trailing slashes are stripped before they are looked up.
 * </ul>
 */

public class ProviderRegistry {
    /**
     * @param url Root relative path that this handler should be associated with.
     * @param descrip Informative description of this handler/url
     * @param obj The handler itself (JThttpProvider)
     * @throws IllegalStateException Thrown if the url is already associated with
     *         a handler.
     */
    public void addHandler(String url, String descrip, JThttpProvider obj) {
	// validate url
	//    does it contain any /
	//    strip length
	//    strip trailing /
	//    check for collisions
	if (debug) {
	    System.out.println("PR-Adding Handler: " + descrip);
	    System.out.println("   PR-Adding URL: " + url);
	    System.out.println("   PR-Adding OBJ: " + obj);
	}

	// used to easily file away this entry
	//httpURL path = new httpURL(url);
	try {
	    boolean result = insertHandler(disassembleURL(url), descrip, obj, false);
	    if (result) obj.addRegistredURL(url);
	}
	catch (IllegalArgumentException e) {
	    if (debug) {
		System.out.println("   PR-Error while inserting " + obj);
		System.out.println("   PR-Ignoring insert of URL " + url);
		e.printStackTrace();
	    }
	}   // catch
    }

    public void addPrivateHandler(String url, String descrip, JThttpProvider obj) {
	if (debug) {
	    System.out.println("PR-Adding Private Handler: " + descrip);
	    System.out.println("   PR-Adding URL: " + url);
	    System.out.println("   PR-Adding OBJ: " + obj);
	}

	// used to easily file away this entry
	//httpURL path = new httpURL(url);

	try {
	    boolean result = insertHandler(disassembleURL(url), descrip, obj, true);
	    if (result) obj.addRegistredURL(url);
	}
	catch (IllegalArgumentException e) {
	    if (debug) {
		System.out.println("   PR-Error while inserting " + obj);
		System.out.println("   PR-Ignoring insert of URL " + url);
		e.printStackTrace();
	    }
	}   // catch
    }

    /**
     * @return Whether or not the url was resolved and removed.
     */
    public boolean removeHandler(String url, JThttpProvider obj) {
	if (debug) {
	    System.out.println("PR-Removing Handler by URL: " + url);
	    System.out.println("   PR-Removing OBJ : " + obj);
	}

	return deleteHandler(disassembleURL(url), obj);
    }

    /**
     * Remove all URL registrations that point to the supplied handler.
     */
    public void removeHandler(JThttpProvider obj) {
	if (debug) {
	    System.out.println("PR-Removing Handler by Object: " + obj);
	}

	String[] urls = obj.getRegisteredURLs();

	for (int i = 0; i < urls.length; i++) {
	    deleteHandler(disassembleURL(urls[i]), obj);
	}
    }

    /**
     * @param url The file portion of the url.  Ex: <tt>/harness/foo</tt>
     * @return The handler associated with the url.  A default provider is given
     *         if the requested one cannot be found.
     */
    public JThttpProvider getHandler(httpURL url) {
	if (url == null) return null;

	/*
	// preprocess the url, remove whitespace and trailing slash
	String processedURL = url.trim();

	// XXX may need modification for key-value pairs encoded in URL
	int nextSlash = url.indexOf('/', 1);

	// grab only the first directory name
	if (nextSlash != -1)
	    processedURL = processedURL.substring(0, nextSlash);
	
	if (processedURL.endsWith("/") && processedURL.length() > 1)
	    processedURL = processedURL.substring(0, processedURL.length()-1);
        */
	String nextDir = url.getNextFile();

	JThttpProvider prov;

	if (nextDir == null) {
	    if (debug) System.out.println("PR-End of URL, no handler, using default.");
	    prov = null;
	}
	else {
	    if (debug) System.out.println("PR-Looking up: " + nextDir);

	    Object target = url2prov.get(nextDir);

	    if (target == null)
		prov = null;
	    else if (target instanceof HandlerEntry) {
		HandlerEntry he = (HandlerEntry)(target);
		//prov = (he == null ? null : he.getProvider());
		prov = he.getProvider();
	    }
	    else {
		prov = (((ProviderRegistry)target).getHandler(url));
	    }
	}

	if (debug) System.out.println("PR-URL resolved to: " + prov);
	
	// we always return a valid handler
	return (prov == null ? getIndexProvider() : prov);
    }

    public JThttpProvider getIndexProvider() {
	if (myProvider == null)
	    myProvider = new IndexHandler(url2prov);

	return myProvider;
    }

    /**
     * Find out how many providers or sub-providers are available.
     */
    public int getSize() {
	return url2prov.size();
    }

    public boolean isEmpty() {
	return (url2prov.size() == 0);
    }

// ---------- Protected or better -----------

    /**
     * Find out if there is a handler or sub-registry with the given name.
     */
    protected Object lookupByName(String name) {
	Object obj = url2prov.get(name);

	if (debug)
	    System.out.println("PR-lookupName(" + name + "): " + obj);

	return obj;
    }

    protected void deleteByName(String name) {
	Object obj = url2prov.remove(name);

	if (debug)
	    System.out.println("PR-Removed " + name + " from hashtable." + obj);
    }

    /**
     * Process a directory name before it is stored or looked up.
     *
     * The default implementation of this method removes the leading and
     * trailing slashes and whitespace.  A null in will result in null out.
     */
    static String stripDirName(String in) {
	if (in == null) return null;

	int startTrim = 0;
	if (in.charAt(0) == '/') startTrim = 1;

	int nextSlash = in.indexOf('/', startTrim);
	String processedURL = null;
	// grab only the first directory name
	if (nextSlash != -1)
	    processedURL = in.substring(startTrim, nextSlash);
	else
	    processedURL = in.substring(startTrim, in.length());
	
	return processedURL;
    }

    /**
     * @return Whether or not the insertion was sucessful.
     * @exception IllegalArgumentException Thrown if the suppled url/object
     *	    combination cannot be properly inserted.  This is most likely
     *      a name conflict.
     */
    protected final boolean insertHandler(String[] url, String descrip,
					  JThttpProvider obj, boolean hidden)
			    throws IllegalArgumentException {
	// XXX may need this check, needs thought because of recursion
	//if (url == null) return;

	boolean result;
	String file = url[0];
	url = (String[])DynamicArray.remove(url, 0);

	if (debug) System.out.println("    PR-Partial insert of " + file);

	Object lookup = lookupByName(file);

	if (lookup == null) {	    // no prexisting entry name
	    if (url == null || url.length == 0) {    // file obj here
		url2prov.put(file, new HandlerEntry(file, descrip, obj));
		if (debug) System.out.println("    Inserted " + file + " into " + this);
		result = true;
	    }
	    else {				    // recurse
		ProviderRegistry pr = new ProviderRegistry();
		url2prov.put(file, pr);
		result = pr.insertHandler(url, descrip, obj, hidden);
		if (debug) System.out.println("    Created new registry and recursed, " + file);
	    }
	}
	else if (lookup instanceof ProviderRegistry) {
	    if (debug) System.out.println("    Found existing registry and recursed, " + file);
	    result = ((ProviderRegistry)lookup).insertHandler(url, descrip, obj, hidden);
	}
	else {
	    throw new IllegalArgumentException(
		"Supplied URL collides with another registred handler.  Ignoring new handler.");
	}

	return result;
    }

    /**
     * @return False if the URL cannot be matched.
     * @throws IllegalArgumentException If the url does not match an entry or
     *		the obj that it maps to does not match the supplied one.
     */
    protected final boolean deleteHandler(String[] url, JThttpProvider obj) {
	if (url.length == 0) {
	    if (debug) System.out.println("    PR-(del) Reached end of URL before done.");
	    return false;
	}

	String file = url[0];
	url = (String[])DynamicArray.remove(url, 0);

	if (debug) System.out.println("    PR-(del)Partial remove of " + file);

	Object lookup = lookupByName(file);

	if (lookup == null) {	    // no such entry
	    if (debug) System.out.println("    PR-(del) Unable to match URL at: " + file);

	    return false;
	}
	else if (lookup instanceof HandlerEntry) {
	    HandlerEntry entry = (HandlerEntry)lookup;

	    if (entry.getProvider() == obj) {
		deleteByName(file);
		return true;
	    }
	    else {
		throw new IllegalArgumentException(
		    "Given Provider ref. does not match URL Provider.  Unable to complete handler removal.");
	    }
	}
	else if (lookup instanceof ProviderRegistry) {
	    if (debug) System.out.println("    PR-(del) Found existing registry and recursed, " + file);
	    ProviderRegistry pr = (ProviderRegistry)lookup;

	    boolean result = pr.deleteHandler(url, obj);

	    if (result && pr.isEmpty()) {
		// only remove a registry if it is empty
		deleteByName(file);
		return true;
	    }
	    else {
		// delete didn't work for some reason
		return false;	// == result
	    }
	}
	else {
	    throw new IllegalArgumentException(
		"Unknown object found in registry.  Unable to complete handler removal.");
	} 
    }

    /**
     * Repacks /foo/bar/index.html as an array with three elements.
     * Slashes separate items, a question mark or space terminates scanning.
     */
    final String[] disassembleURL(String url) {
	String[] result = new String[0];

	if (url == null || url.length() == 0) return result;

	// ignore the leading slash
	int urlLen = url.length();	// just an optimization
	int start = (url.charAt(0) == '/' ? 1 : 0);
	int pos = start + 1;
	char currC;
	boolean done = false;
	while (pos < urlLen && !done) {
	    currC = url.charAt(pos);
	    switch (currC) {
		case '/':   if (pos > start+1)
				result = (String[])DynamicArray.append(result, url.substring(start, pos));
			    start = pos + 1;
			    pos++;
			    break;
		case ' ':   // fall thru
		case '?':   done = true;
			    break;
	    }   // switch

	    pos++;
	}

	if (!done && pos >= urlLen && (pos - start) >= 1)
	    result = (String[])DynamicArray.append(result, url.substring(start, pos));

	if (debug)
	    System.out.println("PR-Dissolved url " + url + " into " + result.length + " files.");

	return result;
    }

    /**
     * Maps the url to a HandlerEntry
     */
    protected Hashtable url2prov = new Hashtable();

    private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(ProviderRegistry.class);
    protected static boolean debug = Boolean.getBoolean("debug." + ProviderRegistry.class.getName());
    protected JThttpProvider myProvider;

    private static class HandlerEntry {
	HandlerEntry(String url, String descrip, JThttpProvider obj) {
	    this.url = url;
	    this.descrip = descrip;
	    this.obj = obj;
	}

	public void setPrivate(boolean state) {
	    hidden = state;
	}

	public String getURL() {
	    return url;
	}

	public String getDescription() {
	    return descrip;
	}

	public JThttpProvider getProvider() {
	    return obj;
	}

	/**
	 * Of the form "/harness" or "/harness/", relative to the root.
	 */
	private String url;
	private String descrip;
	private JThttpProvider obj;
	private boolean hidden;
    }

    protected static class IndexHandler extends JThttpProvider {
	IndexHandler(Hashtable ht) {
	    urlMap = ht;
	}

	public void serviceRequest(httpURL requestURL, PrintWriter out) {
	    PageGenerator.generateDocType(out, PageGenerator.HTML32);
	    PageGenerator.writeBeginDoc(out);
	    PageGenerator.writeHeader(out, "JT Harness Webserver Root Page");
	    PageGenerator.startBody(out);

	    // headline
	    out.print("<h2>JT Harness");
	    out.print("&#8482; ");
	    print(out, i18n.getString("provider.index.hdr"));
	    out.println("</h2>");

	    // note line
	    println(out, i18n.getString("provider.index.note"));
	    out.println("<br>");

	    writeWebAvailPages(out);

	    out.println("<hr>");
	    PageGenerator.writeFooter(out);
	    PageGenerator.endBody(out);
	    PageGenerator.writeEndDoc(out);

	    out.close();
	}

	private void writeWebAvailPages(PrintWriter out) {
	    out.println("<h3>Available Web Pages</h3>");
	    out.println("<ul>");

	    HandlerEntry entry;

	    // XXX should use set of unique entries
	    Enumeration enum_ = urlMap.elements();
	    while(enum_.hasMoreElements()) {
		Object next = enum_.nextElement();
		if (next instanceof HandlerEntry) {
		    entry = (HandlerEntry)next;

		    out.print("<li><a href=\"");
		    out.print(entry.getURL());
		    out.print("\">");
		    out.print(entry.getDescription());
		    out.println("</a>");
		}
		else {
		    //System.out.println("Located sub-Registry");
		}
	    }

	    out.println("</ul>");
	    out.println();
	}

	private Hashtable urlMap;
    }
}

