// SalvagerResource.java
// $Id: SalvagerResource.java,v 1.9 1997/07/30 13:59:30 ylafon Exp $  
// (c) COPYRIGHT MIT and INRIA, 1997.
// Please first read the full copyright statement in file COPYRIGHT.html

package w3c.jigsaw.contrib;

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

import w3c.tools.store.*;
import w3c.jigsaw.resources.*;
import w3c.jigsaw.http.*;
import w3c.www.http.HTTP;
import w3c.jigsaw.html.*;
import w3c.jigsaw.forms.*;

class StoreListFilter implements FilenameFilter {

    public boolean accept(File dir, String name) {
	if ( name.endsWith(".bak") )
	    return false;
	if ( name.equals("state") )
	    return false;
	return name.startsWith("st-");
    }

}

class DummyResourceStoreHolder implements ResourceStoreHolder {
    
    public synchronized boolean acceptStoreUnload(ResourceStore store) {
	return true;
    }

    public synchronized void notifyStoreShutdown(ResourceStore store) {
	return;
    }

    public synchronized void notifyStoreStabilize(ResourceStore store) {
	return;
    }

}

class SalvagerReport {
    /**
     * Date stamp of that report
     */
    long stamp = -1;
    /**
     * Number of store cycles detected.
     */
    int storecycle = 0 ;
    /**
     * Number of stores deleted (unused).
     */
    int storedelete = 0;
    /**
     * Number of resources that couldn't be restored.
     */
    int exception = 0;
    /**
     * Number of <em>hard</em> delete of resources due to cycles.
     */
    int harddelete = 0;
    /**
     * Total number of resources visited.
     */
    int counter = 0;
    /**
     * Has the walker been interrupted ?
     */
    boolean interrupted = false;

    /**
     * Does this report indicates completion of the walk.
     * If the walk ended up successfully, the handler can now proceed to do
     * some real cleanup, otherwise, don't !
     */

    public boolean isSafe() {
	return (( ! interrupted)		// walker wasn't interrupted
		&& (storecycle == 0)		// no more cycles
		&& (exception == 0)		// no exception occured
		&& (harddelete == 0));		// no hard delete of resources
    }

    SalvagerReport() {
	this.stamp = System.currentTimeMillis();
    }

}

class SalvageWalker implements ResourceWalker {
    SalvagerResource reporter = null;
    int              counter  = 0;
    int              maxid    = -1;
    PrintStream      log      = null;
    boolean          doverify = false;
    SalvagerReport   report   = null;
    boolean          doremove = true;

    protected final void log(String msg) {
	if ( log != null ) 
	    log.println(msg);
	else
	    System.out.println(msg);
    }

    protected String pathToString(String p[]) {
	StringBuffer sb = new StringBuffer();
	for (int i = 0 ; i < p.length ; i++) {
	    sb.append('.');
	    sb.append(p[i]);
	}
	return sb.toString();
    }

    protected Resource lookup(String path[], int off, int len)
	throws InvalidResourceException
    {
	httpd             s = reporter.getServer();
	ContainerResource c = (ContainerResource) s.getEditRoot();
	Resource          r = null;
	for (int i = off ; true ; i++) {
	    r = c.lookup(path[i]);
	    if ( i >= len-1 ) {
		return r;
	    } else if ( r instanceof ContainerResource ) {
		c = (ContainerResource) r;
	    } else {
		return null;
	    }
	}
    }

    protected Resource lookup(String path[]) 
	throws InvalidResourceException
    {
	return lookup(path, 0, path.length);
    }

    protected void hardDelete(Resource r, String reason) {
	if ( r != null ) {
	    log(r.getValue("url", "no-url")+": deleted, "+reason);
	    report.harddelete++;
	    r.getResourceStore().removeResource(r.getIdentifier());
	}
    }

    protected void hardDelete(Resource parent, String id, String reason) {
	if ((parent != null) && (parent instanceof ContainerInterface)) {
	    ContainerInterface c = (ContainerInterface) parent;
	    // Hold the store, for deleting the children:
	    DummyResourceStoreHolder h = new DummyResourceStoreHolder();
	    log(parent.getValue("url", "no-url")
		+ "[" + id + "]"
		+": deleted, "+reason);
	    report.harddelete++;
	    ResourceStore store = null;
	    try {
		store = c.getResourceStore(h);
		store.removeResource(id);
	    } finally {
		if ( store != null )
		    store.removeHolder(h);
	    }
	    parent.markModified();
	}
    }

    public boolean handleResource(Resource resource) {
	report.counter++;
	if (doverify && resource instanceof HTTPResource ) {
	    return ((HTTPResource) resource).verify();
	} else {
	    return true;
	}
    }

    public void handleException(String path[], int err, Exception ex) {
	report.exception++;
	// Compute the whole path:
	log("Unable to load resource \""+path[err]+"\" of " 
	    + pathToString(path)
	    + ": "+ex.getMessage());
	if ( doremove ) {
	    // Remove unloadable resource:
	    try {
		Resource r = lookup(path, 0, err);
		if ( r != null )
		    hardDelete(r, path[err], "unloadable");
	    } catch (InvalidResourceException e) {
		// Should try to remove one level further
	    }
	} else {
	    // Interrupt the walker, since we will not be able to cleanup:
	    WalkerEngine engine = reporter.getEngine();
	    if ( engine != null ) 
		engine.interrupt();
	}
    }
    
    public void handleInterruption() {
	log("*** walker interrupted !");
	report.interrupted = true;
	reporter.walkerDone(this, report);
    }
    
    public void handleCycle(String storeid, String from[], String target[]) {
	report.storecycle++;
	log("cycle on "+storeid+" fixed by removing resources:");
	log("\t"+pathToString(from));
	log("\t"+pathToString(target));
	// Delete the longest path to the store:
	String todelete[] = (from.length > target.length) ? from : target;
	try {
	    hardDelete(lookup(todelete), "cycle on "+storeid);
	} catch (Exception ex) {
	    ex.printStackTrace();
	}
    }
    
    public void handleDone(Hashtable stores) {
	log("walker: done, visited "+report.counter+" resources.");
	if ( ! report.isSafe() ) {
	    log("*** walker: didn't proceed to cleanup, rerun it !");
	} else {
	    // Garbage collect the stores:
	    File   dir      = reporter.getServer().getStoreDirectory();
	    String files[] = dir.list(new StoreListFilter());
	    for (int i = 0 ; i < files.length ; i++) {
		// Get the store id, and check against maxid:
		try {
		    int id = Integer.parseInt(files[i].substring(3));
		    if (id >= maxid )
			continue;
		} catch (Exception ex) {
		    continue;
		}
		// Should we delete that store ?
		File file = new File(dir, files[i]);
		if ( stores.get(file.getAbsolutePath()) == null ) {
		    report.storedelete++;
		    log("delete: "+file.getAbsolutePath());
		    if ( file.exists() )
			file.delete();
		    File bak = new File(dir, files[i]+".bak");
		    log("delete: "+bak.getAbsolutePath());
		    if ( bak.exists() )
			bak.delete();
		}
	    }
	}
	reporter.walkerDone(this, report);
    }
    
    SalvageWalker(SalvagerResource reporter
		  , PrintStream log
		  , boolean doverify
		  , boolean doremove) {
	this.reporter = reporter;
	this.doverify = doverify;
	this.doremove = doremove;
	this.report   = new SalvagerReport();
	// Setup the log:
	this.log = log;
	// Get the current store id (beyond which we cannot collect):
	httpd                server  = reporter.getServer();
	ResourceStoreManager manager = server.getResourceStoreManager();
	this.maxid = manager.getCurrentStoreIdentifier();
    }
    
}

public class SalvagerResource extends PostableResource {
    /**
     * Attribute index - The salvager log.
     */
    protected static int ATTR_LOGNAME = -1;

    static {
	Attribute a = null;
	Class     c = null;
	try {
	    c = Class.forName("w3c.jigsaw.contrib.SalvagerResource");
	} catch (Exception ex) {
	    ex.printStackTrace();
	}
	// Register the log attribute:
	a = new FilenameAttribute("log"
				  , null
				  , Attribute.EDITABLE);
	ATTR_LOGNAME = AttributeRegistry.registerAttribute(c, a);
    }

    /**
     * The engine currently running, or <strong>null</strong>.
     */
    protected WalkerEngine engine = null;
    /**
     * The power for the engine, when needed.
     */
    protected Thread thread = null;
    /**
     * The walk handler.
     */
    protected ResourceWalker walker = null;
    /**
     * The log, as a RandomAccessFile
     */
    protected PrintStream log = null;
    /**
     * The last walker's report.
     */
    protected SalvagerReport report = null;
    /**
     * Did we run a verify stage since last safe report ?
     */
    protected boolean verified = false;
    /**
     * Date at which the currernt salvage was started.
     */
    protected long started = -1;
    /**
     * The log file to use (if any)
     */
    protected File logfile = null;

    public void setValue(int idx, Object value) {
	super.setValue(idx, value);
	if ( idx == ATTR_LOGNAME ) {
	    // Reset the log file:
	    HTTPResource p = getParent();
	    if ( p instanceof DirectoryResource ) {
		DirectoryResource d = (DirectoryResource) p;
		logfile = new File(d.getDirectory(), getLogname());
	    }
	}
    }

    /**
     * Get the walker engine currently running.
     * @return A WalkerEngine instance, or <strong>null</strong> if no
     * engine is currently running.
     */

    protected synchronized WalkerEngine getEngine() {
	return engine;
    }

    /**
     * Callback, for the walker to notify the end of salvaging.
     * @param walker The walker that is done.
     * @param report The walker's report
     */

    protected synchronized void walkerDone(ResourceWalker walker
					   , SalvagerReport report) {
	if ( log != null ) 
	    log.close(); 
	this.verified = ((SalvageWalker) walker).doverify;
	this.walker   = null;
	this.engine   = null;
	this.thread   = null;
	this.log      = null;
	this.report   = report;
	this.started  = -1;
    }

    /**
     * Start the salvage process.
     * @return A boolean, <strong>true</strong> if salvager launched, 
     * <strong>false</strong> if salvager was already running.
     */

    protected synchronized boolean startSalvage(boolean doverify
						, boolean doremove) {
	if ( thread == null ) {
	    // Open the log:
	    if ( logfile != null ) {
		try {
		    log = (new PrintStream
			   (new BufferedOutputStream
			    (new FileOutputStream (logfile))));
		} catch (IOException ex) {
		    log = null;
		}
	    }
	    // Create the walker, start the engine:
	    this.walker    = new SalvageWalker(this, log, doverify, doremove);
	    this.engine    = new WalkerEngine(getServer().getEditRoot()
					      , walker
					      , false);
	    this.thread = new Thread(engine);
	    thread.setDaemon(true);
	    thread.setPriority(1);
	    thread.start();
	    this.started = System.currentTimeMillis();
	    return true;
	} else {
	    return false;
	}
    }

    /**
     * Interrupt the current walker, if any.
     */

    protected synchronized void interruptSalvage() {
	if ( engine != null )
	    engine.interrupt();
    }

    /**
     * Get the salvager's resource log name.
     * @return A String filename.
     */

    public String getLogname() {
	return getString(ATTR_LOGNAME, null);
    }

    /**
     * This resource should never be unloaded.
     */

    public synchronized boolean acceptUnload() {
	return false;
    }

    public void notifyUnload() {
	interruptSalvage();
    }

    /**
     * Dump the last report, if any into given stream.
     * @param g The HtmlGenerator to dump to.
     */

    protected void dumpReport(HtmlGenerator g) {
	g.append("<h2>Last salvager's report</h2>");
	if ( report != null ) {
	    g.append("<p>That report was started at: "+new Date(report.stamp));
	    g.append("<ul>");
	    if ( report.interrupted )
		g.append("<li>The salvager was interrupted"
			 , "<strong> partial results !</strong>");
	    g.append("<li><strong>"
		     , Integer.toString(report.exception)
		     , "</strong> exceptions occured.");
	    g.append("<li><strong>"
		     , Integer.toString(report.counter)
		     , "</strong> visited resources.");
	    g.append("<li><strong>"
		     , Integer.toString(report.storecycle)
		     , "</strong> store cycles detected.");
	    g.append("<li><strong>"
		     , Integer.toString(report.harddelete)
		     , "</strong> resources deleted (to break cycles).");
	    g.append("<li><strong>"
		     , Integer.toString(report.storedelete)
		     , "</strong> stores deleted (unused).");
	    g.append("</ul>");
	    if ( report.isSafe() && verified ) {
		g.append("<p>Your configuration looks clean !");
	    } else if ( ! verified ) {
		g.append("<p>You should run the walker again !");
	    }
	} else {
	    g.append("<p>No report available yet.");
	}
    }

    /**
     * Dump a suitable form for display report and running the salvager.
     * @param g The Htmlgenerator to dump infos into.
     */

    public void dump(HtmlGenerator g) {
	boolean running = (thread != null);

	// Some usefull controls first:
	if ( running ) {
	    g.append("<p>Salvager has been running for "
		     + (System.currentTimeMillis()-started) 
		     + " ms.");
	} 
	// Link to last log, if available:
	if ( ! running && (getLogname() != null))
	    g.append("<p>Last run log is available <a href=\""
		     , getLogname()
		     , "\">here</a>");
	// Emit the latest available report:
	dumpReport(g);
	// Emit the control form:
	g.append("<form action=\"", getURLPath(), "\" method=\"POST\">");
	if ( running ) {
	    g.append("<p>Stop the salvager:");
	    g.append("<input type=\"radio\" name=\"control\" "
		     , " checked=\"yes\""
		     , " value=\"stop\">");
	} else {
	    g.append("<p>Start the salvager:");
	    g.append("<input type=\"radio\" name=\"control\""
		     , " checked=\"yes\""
		     , " value=\"run\">");
	    // Should we verify also ?
	    g.append("<p>Verify ? <select name=\"verify\">");
	    if ((report != null) && report.isSafe() && ! verified) {
		g.append("<option selected value=\"true\">true</option>");
		g.append("<option value=\"false\">false</option></select>");
	    } else {
		g.append("<option  value=\"true\">true</option>");
		g.append("<option selected value=\"false\">false</option>"
			 , "</select>");
	    }
	    // Should we remove unloadable resources ?
	    g.append("Remove ? <select name=\"remove\">");
	    g.append("<option selected value=\"false\">false</option>");
	    g.append("<option value=\"true\">true</option></select>");
	}
	g.append ("<p><input type=\"submit\" name=\"submit\" " +
		  "value=\" Do It ! \">" );
	g.append("</form>");
	// This has proven to be usefull:
	g.append("<hr>");
	g.append("<p><a href=\"", getURLPath(), "\">Refresh</a>");
    }

    /**
     * Redirect all requests to the log, when possible.
     * @return A Reply instance.
     * @exception HTTPException If performing the request failed.
     */

    public Reply get(Request request) 
	throws HTTPException
    {
	String  cmd     = request.getQueryString();
	HtmlGenerator g = new HtmlGenerator("Resource Salvager");

	dump(g);
	// Emit a reply:
	Reply reply = createDefaultReply(request, HTTP.OK);
	reply.setExpires(0);
	reply.setStream(g);
	return reply;
    }

    public Reply handle(Request request, URLDecoder data) 
	throws HTTPException 
    {
	String cmd = data.getValue("control");
	// Apply command if any:
	if ( cmd == null ) {
	    ;
	} else if (cmd.equals("run")) {
	    boolean dov = data.getValue("verify").equalsIgnoreCase("true");
	    boolean dor = data.getValue("remove").equalsIgnoreCase("true");
	    startSalvage(dov, dor);
	} else if (cmd.equals("stop")) {
	    interruptSalvage();
	}
	// Emit a suitable reply:
	Reply         reply = createDefaultReply(request, HTTP.OK);
	HtmlGenerator g     = new HtmlGenerator("Resource Salvager");
	dump(g);
	reply.setExpires(0);
	reply.setStream(g);
	return reply;
    }


    public void initialize(Object values[]) {
	super.initialize(values);
	// Precompute the log file:
	HTTPResource p = getParent();
	if ( p instanceof DirectoryResource ) {
	    DirectoryResource d = (DirectoryResource) p;
	    logfile = new File(d.getDirectory(), getLogname());
	}
    }

}
