// CvsFrame.java
// $Id: CvsFrame.java,v 1.31 1998/06/10 15:13:44 bmahe Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.jigsaw.cvs ;

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

import org.w3c.jigsaw.frames.*;
import org.w3c.cvs.* ;
import org.w3c.util.*;
import org.w3c.www.mime.*;
import org.w3c.www.http.*;
import org.w3c.jigsaw.http.* ;
import org.w3c.jigsaw.auth.AuthFilter;
import org.w3c.tools.resources.ProtocolException;
import org.w3c.tools.resources.*;
import org.w3c.jigsaw.forms.*;
import org.w3c.jigsaw.html.* ;
import org.w3c.tools.sorter.*;

public class CvsFrame extends PostableFrame {

    protected static HttpCacheControl CACHE_CONTROL_NOCACHE = null;
    protected static HttpTokenList    PRAGMA_NOCACHE       = null;

    static {
	// Pre-compute the no cache directives:
	CACHE_CONTROL_NOCACHE = HttpFactory.makeCacheControl();
	CACHE_CONTROL_NOCACHE.setNoCache();
	// Pre-compute the no cache directives:
	String nocache[] = { "no-cache" };
	PRAGMA_NOCACHE = HttpFactory.makeStringList(nocache);
    }

    /**
     * Have we already computed our CVS environment ?
     */
    private static boolean inited   = false ;
     
    private CvsDirectory        cvs       = null ;
    private CvsHandlerInterface handler   = null ;

    /**
     * Emit an HTML error message.
     * @param request The request that trigered the error.
     * @param msg The error message.
     * @param ex The CvsException that happened while processing the request.
     * @return An HTTP reply.
     */
    protected static Reply error(Request request,
				 String msg,
				 CvsException ex) 
    {
	Reply reply = request.makeReply(HTTP.INTERNAL_SERVER_ERROR);
	HtmlGenerator g = new HtmlGenerator(msg);
	g.append("<h1>", msg, "</h1>\n");
	g.append("<p>Error details:<p><em>", ex.getMessage(), "</em>\n");
	g.append("<hr>You may wish to reload that page, to cope with a "
		 + " well known Java bug.\n");
	reply.setStream(g);
	return reply;
    }

    /**
     * Emit an HTML error message.
     * @param request The request that trigered the error.
     * @param msg The error message.
     * @return An HTTP reply.
     */
    protected static Reply error(Request request,
				 String msg,
				 String details)
    {
	Reply reply = request.makeReply(HTTP.INTERNAL_SERVER_ERROR);
	HtmlGenerator g = new HtmlGenerator(msg);
	g.append("<h1>", msg, "</h1>\n");
	g.append("<p>Error details:<p><em>", details, "</em>\n");
	g.append("<hr>You may wish to reload that page, to cope with a "
		 + " well known Java bug.\n");
	reply.setStream(g);
	return reply;
    }


    /**
     * Get a suitable FramedResource to display the given cvs'ed file.
     * @param name The name of the file.
     * @return A CvsEntryResource, or <strong>null</strong> if none was
     * found.
     */
    protected FramedResource getResourceFor(String name) {
	return new CvsEntryResource(getFrameReference(), name) ;
    }

    /**
     * Get the CVS manager associated with this resource, or create it.
     */
    protected synchronized CvsDirectory getCvsManager() {
	if (cvs == null) {
	    ResourceReference rrp = resource.getParent();
	    if (rrp != null) {
		try {
		    Resource parent = rrp.lock();
		    if (! (parent instanceof DirectoryResource)) {
			getServer().errlog(resource, 
					"not a child of a DirectoryResource");
			throw new RuntimeException(
					"The server is misconfigured.");
		    }
		    // CVS will only work within a (filesystem) 
		    // directory resource
		    File d = ((DirectoryResource) parent).getDirectory();
		    try {
			cvs = CvsDirectory.getManager(d, 
						  getServer().getProperties());
		    } catch (CvsException ex) {
			String msg = ("unable to create a cvs manager for \""+
				      d.getAbsolutePath()+
				      "\".");
			getServer().errlog(this, msg);
			throw new RuntimeException("CVS failed.");
		    }
		    handler = new CvsDirectoryHandler(cvs);
		} catch (InvalidResourceException ex) {
		    getServer().errlog(resource, "Invalid parent");
		    throw new RuntimeException("The server is misconfigured.");
		} finally {
		    rrp.unlock();
		}
	    }
	}
	return cvs;
    }

    protected boolean isIndexed(String name) {
	ResourceReference rrp = resource.getParent();
	if (rrp != null) {
	    try {
		Resource parent = rrp.lock();
		if (! (parent instanceof DirectoryResource)) {
		    getServer().errlog(resource, 
				       "not a child of a DirectoryResource");
		    throw new RuntimeException("The server is misconfigured.");
		}
		return (((DirectoryResource)parent).lookup(name) != null);
	    } catch (InvalidResourceException ex) {
		getServer().errlog(resource, "Invalid parent");
		throw new RuntimeException("The server is misconfigured.");
	    } finally {
		rrp.unlock();
	    }
	} else {
	    getServer().errlog(resource, "No parent!");
	    throw new RuntimeException("The server is misconfigured.");
	}
    }

    /**
     * Perform the given action on the underlying directory as a whole.
     * @param action The action to perform.
     * @param request The request that triggered the action.
     * @param data The decoded form data.
     * @return A suitable HTTP reply.
     */
    protected Reply performDirectoryAction(String action,
					   Request request,
					   org.w3c.jigsaw.forms.URLDecoder 
					   data)
	throws ProtocolException
    { 
	if ( action.equals("refresh") ) {
	    // Following command will cause a refresh if needed:
	    try {
		getCvsManager().listFiles();
	    } catch (CvsException ex) {
		return error(request, "Error while refreshing directory", ex);
	    }
	} else if ( action.equals("commit") ) {
	    // Commit the whole directory:
	    try {
		String comment = data.getValue("comment");
		String u = (String)request.getState(AuthFilter.STATE_AUTHUSER);
		String env[] = {"USER="+u , "LOGNAME="+u };
		comment = ((comment == null)
			   ? "Changed through Jigsaw."
			   : comment);
		comment = ((u != null) ? ("("+u+") "+comment) : comment);
		getCvsManager().commit(comment,env);
	    } catch (CvsException ex) {
		return error(request, "Error while commiting directory", ex);
	    }
	} else if ( action.equals("update") ) {
	    // Update the whole directory:
	    try {
		getCvsManager().update();
	    } catch (CvsException ex) {
		return error(request, "Error while updating directory", ex);
	    }
	} else {
	    // Unknown directory command:
	    Reply error = request.makeReply(HTTP.INTERNAL_SERVER_ERROR);
	    error.setContent("Unknwon action \""+action+"\" for directories");
	    throw new HTTPException(error);
	}
	return get(request);
    }

    /**
     * Register the resource and add CvsProperties in httpd.
     * @param resource The resource to register.
     */
    public void registerResource(FramedResource resource) {
	super.registerOtherResource(resource);
	if ( ! inited ) {
	    synchronized (this.getClass()) {
		httpd s = (httpd) getServer();
		if ( s != null ) {
		    // Register the CVS property sheet if not done yet:
		    ObservableProperties props = s.getProperties() ;
		    s.registerPropertySet(new CvsProp("cvs", s));
		    inited = true ;
		}
	    }
	}
    }
    /**
     * Lookup method for the CVS manager.
     * Lookup for a cvs entry object having the given name, if found, wrap it
     * into a CvsEntryResource object and return it.
     * @param ls The current lookup state.
     * @param lr The (under construction) lookup result.
     */
    protected boolean lookupOther(LookupState ls, LookupResult lr) 
	throws ProtocolException
    {
	if (super.lookupOther(ls,lr)) 
	    return true;
	String name = ls.getNextComponent();
	try {
	    if ( getCvsManager().getDirectoryStatus(name) == CVS.DIR_NCO ) {
		// Checkout directory, and relocate:
		getCvsManager().updateDirectory(name);
		Request request = (Request)ls.getRequest();
		if ( request != null ) {
		    Reply relocate = request.makeReply(HTTP.FOUND);
		    try {
			// URL myloc    = (ls.hasRequest()
			//     ? getURL(ls.getRequest())
			//     : new URL(getServer().getURL(), getURLPath()));
			URL myloc    = getURL(request);
			URL location = new URL(myloc, name+"/CVS");
			relocate.setLocation(location);
			lr.setReply(relocate);
		    } catch (Exception ex) {
		    }
		}
		lr.setTarget(null);
		return true;
	    } else if ( getCvsManager().status(name) != CVS.FILE_Q ) {
		FramedResource target = getResourceFor(name);
		lr.setTarget( target.getResourceReference() );
		return target.lookup(ls, lr);
	    } else {
		lr.setTarget(null);
		return true;
	    }
	} catch (CvsException ex) {
	    throw new HTTPException("status failed in CVS directory.");
	}
    }

    /**
     * Dump one CVS entry into HTML. 
     * The generated HTML is expected to insert itself in a table.
     * @param g The HTML generator to use.
     * @param name The entry to be dumped.
     * @exception CvsException If the CVS access failed to this entry.
     */
    private void dumpFileEntry (HtmlGenerator g, String name)
	throws CvsException
    {
	String eurl     = resource.getURLPath() + "/" + name;
	int    status   = getCvsManager().status(name);
	String revision = getCvsManager().revision(name);
	boolean indexed = isIndexed(name);

	// Entry toggle:
	g.append("<tr><th><input type=\"checkbox\" name=\""+ name+
		 "\" value=\"mark\">" );
	// Dump the entry name (link only if checked out):
	if ( status == CVS.FILE_NCO || (!indexed)) 
	    g.append("<th align=left>" + name);
	else 
	    g.append("<th align=left><a href=\""+name+"\">"+ name + "</a>\n");
	// Dump the revision number
	if (revision == null)
	    g.append("<th>\n");
	else {
	    int index = revision.indexOf(".");
	    if (index != -1) {
		g.append("<th align=left><select name=\""+name+".rev\">\n");
		String minor = revision.substring(index+1);
		String major = revision.substring(0, index);
		int max = 0;
		try {
		    max = Integer.parseInt(minor);
		} catch (NumberFormatException ex) {
		    max = 0;
		}
		for (int i = max; i > 0; i--) {
		    g.append("<option value=\""+major+"."+i+"\">"+
			     major+"."+i+"</option>\n");
		}
		g.append("</select>\n");
	    } else {
		g.append("<th>\n");
	    }
	}
	// Dump the entry staus:
	g.append ("<th>\n") ;
	if (indexed)
	    g.append(getCvsManager().statusToString(status));
	else 
	    g.append(getCvsManager().statusToString(status)," (not indexed)");
	// Emit a diff/log hyper-link only if this makes sense (entry is known)
	if ( status != CVS.FILE_Q )
	    g.append ("<th><a href=\""+eurl+"?log\">log</a>\n") ;
	if ( status == CVS.FILE_M )
	    g.append ("<th><a href=\""+eurl+"?diff\">diff</a>\n") ;
    }

    /**
     * Dump one CVS Directory entry into HTML. 
     * The produced HTML is expected to insert itself into a table.
     * @param g The HTML generator to use.
     * @param name The name of the directory to dumped.
     * @exception CvsFailure If the CVS access failed to this entry.
     */
    private void dumpDirectoryEntry(HtmlGenerator g, String name)
	throws CvsException
    {
	int    status = getCvsManager().getDirectoryStatus(name);
	String eurl   = name + "/CVS";

	// Dump the toggle:
	g.append("<tr><th><input type=\"checkbox\" name=\"" + name
		 + "\" value=\"mark\">" );
	// Dump the entry name (and link if available):
	if (status == CVS.DIR_NCO)
	    g.append("<th align=left>" + name);
	else 
	    g.append("<th align=left><a href=\""+name+"\">" + name + "</a>\n");
	// Dump more links:
	if (status != CVS.DIR_Q) {
	    if ( status != CVS.DIR_NCO )
		g.append("<th align=left><a href=\""
			 , eurl
			 , "\">CVS</a>\n");
	    else
		g.append("<th align=left><a href=\"" + resource.getURLPath() + 
			 "/" + name+ "\">CVS (checkout)</a>\n");
	} else {
	    g.append("<th aligne=left>\n");
	}
	// dump one line for the status:
	g.append ("<th>\n") ;
	g.append(getCvsManager().statusToString(status));
	g.append ("<th>\n");
    }

    /**
     * Dump the content of the directory as a CVS form.
     * The resulting form allows for trigerring actions on the various files.
     */
    public Reply get (Request request) 
	throws ProtocolException
    {
	boolean no_entries = true;
	HtmlGenerator g = new HtmlGenerator ("CVS for "+ 
					     getCvsManager().getDirectory());
	addStyleSheet(g);
	g.addStyle("BODY {color: black; background: white; "+
		   "font-family: serif;}\n");
	g.addStyle("CAPTION {color: red; font-weight: bold;  border: solid; "+
		   "border-width: thin; border-color: teal; padding: 3pt }\n");
	g.addStyle(".warning { color: red; font-weight: bold }\n");
	g.addStyle("P.ERROR {color: red }\n");
	g.addStyle("A {color: darkblue; font-weight: bold}\n");
	g.addStyle("H1.center {color:white;font-weight:bold;"+
		   "text-align:center}"); 
	g.addLink(new HtmlLink(null, "made", "jigsaw@w3.org"));

	String parentpath = null;
	ResourceReference rr_parent = resource.getParent();
	try {
	    Resource parent = rr_parent.lock();
	    parentpath = parent.getURLPath();
	} catch (InvalidResourceException ex) {
	    getServer().errlog(resource, "Invalid parent");
	    throw new RuntimeException("The server is misconfigured.");
	} finally {
	    rr_parent.unlock();
	}

	// Dump all file entries:
	Enumeration enum  = null;
	try {
	    enum = getCvsManager().listFiles() ;
	} catch (CvsException ex) {
	    throw new HTTPException(error(request,"unable to list files",ex));
	}
	Vector names = Sorter.sortStringEnumeration(enum);
	if( names.size() > 0 ) {
	    no_entries = false;
	    g.append ("<table width=\"100%\"><td align=\"left\">\n[ Up to \n");
	    g.append ("<a href=\"./\">Directory</A> ]</TD>\n");
	    g.append ("<td align=\"right\">[ The Error log is down ]" );
	    g.append (" </td></table>\n\n");
	    // now generate the form
	    g.append ("<form action=\"", resource.getURLPath(), 
		      "\" method=\"post\">\n");
	    g.append ("<table width=\"100%\">\n") ;
	    g.append ("<caption>FILES in ", parentpath, "</caption>\n");
	    // Dump entries, sorted:
	    for (int i = 0 ; i < names.size() ; i++) {
		String   name  = (String)   names.elementAt(i);
		try {
		    dumpFileEntry (g, name) ;
		} catch (CvsException ex) {
		    g.append ("<td>" + name + 
			      "<strong>CVS Failed</strong>\n") ;
		}
	    }
	    g.append ("</table><p>\n") ;
	    // Dump the files command area:
	    g.append ("<table align=\"center\" width=\"100%\"><tr>" 
		      + "<td colspan=2 align=\"center\">\n");
	    g.append ("<p align=\"center\"><strong>Comments for " +
		      "add/remove/commit files<br>\n");
	    g.append ("<textarea name=\"comment\" rows=\"3\" cols=\"50\">\n") ;
	    g.append ("</textarea></td></tr>\n") ;
	    // add proposed actions
	    g.append ("<tr valign=\"top\"><td><strong>Action:</strong><br>\n");
	    g.append("<input type=\"radio\" name=\"action\" value=\"add\">"
		     + "Add <br>\n");
	    g.append("<input type=\"radio\" name=\"action\" value=\"update\" "
		     + "checked = \"yes\">Update <br>\n");
	    g.append("<input type=\"radio\" name=\"action\" value=\"commit\">"
		     + "Commit <br>\n");
	    g.append("<input type=\"radio\" name=\"action\" value=\"revert\">"
		     + "Revert <br><br>\n");
	    g.append("<input type=\"radio\" name=\"action\" value=\"remove\">"
		     + "Remove (<SPAN CLASS=\"warning\">Use this carefully!"+
		     "</SPAN>)<br>\n");
	    g.append("</td>\n<td>\n");
	    // add proposed scopes:
	    g.append("<strong>Perform action on:</strong><br>\n\n");
	    g.append("<input type=\"radio\" name=\"scope\" value=\"mark\"" 
		     + "checked=\"yes\">Marked files<br>\n");
	    g.append("<input type=\"radio\" name=\"scope\" "
		     + "value=\"directory\">Directory<br>\n");
	    g.append("<input type=\"radio\" name=\"scope\" value=\"regexp\">"+
		     "Matching files <input type=\"text\" name=\"regval\">"+
		     "<br>\n");
	    // and close the table and the first form
	    g.append ("</td></tr><tr valign=\"top\"><td align=\"center\" " +
		      "colspan=\"2\">\n");
	    g.append ("<input type=\"submit\" name=\"submit\" " +
		      "value=\" Perform Action \">" );
	    g.append ("</table></form>\n\n");
	}
	// get a vector of directory entries
	try {
	    enum = getCvsManager().listDirectories();
	} catch (CvsException ex) {
	    throw new HTTPException(error(request
					  , "unable to list directories"
					  , ex));
	}
	Vector dirnames = Sorter.sortStringEnumeration(enum);
	if( dirnames.size() > 0 ) {
	    no_entries = false;
	    // the next one is for stephan
	    g.append ("<hr><p>\n");
	    //add the DIRECTORY section
	    g.append("<table width=\"100%\"><td align=\"left\">\n[ Up to ");
	    // new version

	    g.append("<a href=\"./\">Directory</A> ]</TD>\n");
	    g.append ("<td align=\"right\">[ The Error log is down ]" );
	    g.append (" </td></table>\n");
	    // now generate the form
	    g.append("<form action=\"", resource.getURLPath(), 
		     "\" method=\"post\">\n");
	    g.append("<table width=\"100%\">\n") ;
	    g.append("<caption>SUBDIRECTORIES in ", parentpath, 
		     "</caption>\n");
	    // Dump entries, sorted:
	    for (int i = 0 ; i < dirnames.size() ; i++) {
		String name = (String) dirnames.elementAt(i);
		try {
		    dumpDirectoryEntry (g, name) ;
		} catch (CvsException e) {
		    g.append ("<td>" + name + 
			      "<strong>CVS Failed</strong>\n") ;
		}
	    }
	    g.append ("</table><p>\n");
	    // Dump the dirs command area:
	    g.append ("<table align=\"center\" width=\"100%\"><tr>" 
		      + "<td colspan=2 align=\"center\">\n");
	    g.append ("<p align=\"center\"><strong>Comments for " +
		      "add directories<br>\n");
	    g.append ("<textarea name=\"comment\" rows=\"3\" cols=\"50\">") ;
	    g.append ("</textarea></td></tr>\n") ;
	    // add proposed actions
	    g.append ("<tr valign=\"top\"><td><strong>Action:</strong><br>\n");
	    g.append("<input type=\"radio\" name=\"action\" value=\"add\">"
		     + "Add <br>\n");
	    g.append("</td>\n<td>\n");
	    //hidden scope
	    g.append("<input type=\"hidden\" name=\"scope\""+
		     " value=\"subdir\">\n");
	    // and close the table and the first form
	    g.append ("</td></tr><tr valign=\"top\"><td align=\"center\" " +
		      "colspan=\"2\">\n");
	    g.append ("<input type=\"submit\" name=\"submit\" " +
		      "value=\" Perform Action \">" );
	    g.append ("</table></form>\n\n");
	}
	if (no_entries) {
	    g.append("<h1 class=\"center\">No entries.</h1>\n");
	}
	g.close() ;
	// Send back the reply:
	Reply reply = request.makeReply(HTTP.OK) ;
	reply.setHeaderValue(reply.H_CACHE_CONTROL, CACHE_CONTROL_NOCACHE);
	reply.setHeaderValue(reply.H_PRAGMA, PRAGMA_NOCACHE);
	reply.setStream(g);
	return reply ;
    }

    /**
     * This is were we handle the big post request.
     */
    public Reply handle (Request request, org.w3c.jigsaw.forms.URLDecoder data)
	throws ProtocolException
    {
	String action  = data.getValue("action") ;
	String scope   = data.getValue("scope");

	// no action, is a bug in the generated form (see get)
	if ( action == null ) {
	    Reply error = request.makeReply(HTTP.INTERNAL_SERVER_ERROR) ;
	    error.setContent("No action selected !");
	    throw new HTTPException (error) ;
	}
	// Check action's scope:
	Enumeration enum    = null;
	Vector      targets = new Vector() ;

	if ( scope.equals("directory") ) {
	    // Apply action to the whole directory:
	    return performDirectoryAction(action, request, data);
	} else if ( scope.equals("subdir") ) {
	    // Apply action to the subdirectories
	    // Get the list of targets to act on:
	    try {
		enum = getCvsManager().listDirectories() ;
	    } catch (CvsException ex) {
		error(request, "unable to list directories", ex);
	    }
	} else { 
	    // Apply action to the files
	    // Get the list of targets to act on:
	    try {
		enum = getCvsManager().listFiles() ;
	    } catch (CvsException ex) {
		error(request, "unable to list files", ex);
	    }
	}

	if ( scope.equals("regexp") ) {
	    // direct perform
	    String regval = (String) data.getValue("regval") ;
	    String comment = (String) data.getValue("comment") ;
	    System.out.println("regexp : "+regval);
	    if (action.equals("remove") || action.equals("revert")) {
		throw new HTTPException(error(request,"Can't perform "+action+
					      " with regular expression.",
					      regval));
	    }
	    // direct perform
	    if ( comment != null )
		handler.perform (request, action, regval, comment);
	    else
		handler.perform (request, action, regval);
	} else {
	    while ( enum.hasMoreElements() ) {
		String name = (String) enum.nextElement();
		if ( data.getValue (name) != null ) 
		    targets.addElement(name) ;
	    }

	    // Perform that action:
	    int size = targets.size();
	    if( size > 0 ) {
		String names[] = new String[size];
		String revs[]  = new String[size];
		targets.copyInto(names);
		for (int i = 0; i < size; i++ )
		    revs[i] = data.getValue(names[i]+".rev");
		// Perform the comand :
		String comment = data.getValue("comment") ;

		if (action.equals("remove")) {
		    //get the parent resource of our own resource
		    ResourceReference rr = getResource().getParent();
		    try {
			Resource res = rr.lock();
			DirectoryResource dirres = null;
			if (! (res instanceof DirectoryResource)) {
			    getServer().errlog(res, 
			       "CvsFrame: not a child of a DirectoryResource");
			    return error(request, 
				 "The server is misconfigured.",
				 "The CVS Directory is not a children of a"+
				 "Directory Resource.");
			}
			dirres = (DirectoryResource) res;
			for (int i=0; i < size; i++ ) {
			    String name = names[i];
			    ResourceReference childref = dirres.lookup(name);
			    if (childref != null) {
				try {
				    Resource children = childref.lock();
				    if (children instanceof FileResource) {
					FileResource fres = 
					    (FileResource) children;
					// delete the file
					fres.getFile().delete();
					// delete the resource
					fres.delete();
				    }
				} catch(MultipleLockException mex) {
				    mex.printStackTrace();
				    return error(request,
						 "MultipleLockException: "+
						 mex.getMessage(),
						 "Resource "+name+" in use"+
						 ", can't be deleted now.");
				} catch(InvalidResourceException iex) {
				    iex.printStackTrace();
				    return error(request,
						 "InvalidResourceException. ",
						 iex.getMessage());
				} catch (Exception ex) {
				    ex.printStackTrace();
				    return error(request,
						 "Exception occurs.",
						 ex.getMessage());
				} finally {
				    childref.unlock();
				}
			    } else {
				//no resource, remove the file only
				File file = new File( dirres.getDirectory(),
						      name );
				file.delete();
			    }
			}
		    } catch(InvalidResourceException ex) {
			ex.printStackTrace();
		    } finally {
			rr.unlock();
		    }
		}

		if ( comment != null )
		    handler.perform (request, action, names, revs, comment);
		else
		    handler.perform (request, action, revs, names);
	    }
	}
	return get(request) ;
    }

}

