// DirectoryResource.java
// $Id: DirectoryResource.java,v 1.40 1997/07/29 15:41:00 abaird Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package w3c.jigsaw.resources ;

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

import w3c.tools.store.*;
import w3c.tools.sorter.*;
import w3c.jigsaw.http.* ;
import w3c.jigsaw.indexer.* ;
import w3c.jigsaw.html.* ;
import w3c.www.http.*;

/**
 * A simple, and reasonably efficient directory resource.
 * This directory resource embeds its own resource store object to keep
 * track of its next children (wich might themselves be DirectoryResource). It
 * is reasonably efficient in the sense that it won't overload the memory with 
 * unused informations. However, stay tuned for a <em>really</em> efficient
 * file based directory resource (tuned to serve only files).
 */

public class DirectoryResource extends StoreContainer {
    /**
     * Attribute index - The index for our directory attribute.
     */
    protected static int ATTR_DIRECTORY = -1 ;
    /**
     * Attribute index - The icon directory to use in dir listing.
     */
    protected static int ATTR_ICONDIR = -1 ;
    /**
     * Attribute index - The last we we physically visited the directory.
     */
    protected static int ATTR_DIRSTAMP = -1 ;
    /**
     * Attribute index - Should this directory support content negotiation.
     */
    protected static int ATTR_NEGOTIABLE = -1 ;
    /**
     * Attribute index - The indexer to use for that directory, if any.
     */
    protected static int ATTR_INDEXER = -1;

    static {
	Attribute a   = null ;
	Class     cls = null ;
	// Get a pointer to our class.
	try {
	    cls = Class.forName("w3c.jigsaw.resources.DirectoryResource") ;
	} catch (Exception ex) {
	    ex.printStackTrace() ;
	    System.exit(1) ;
	}
	// The directory attribute.
	a = new FileAttribute("directory"
			      , null
			      , Attribute.COMPUTED|Attribute.DONTSAVE);
	ATTR_DIRECTORY = AttributeRegistry.registerAttribute(cls, a) ;
	// Our icon directory.
	a = new StringAttribute("icondir"
				, null
				, Attribute.EDITABLE) ;
	ATTR_ICONDIR = AttributeRegistry.registerAttribute(cls,a);
	// The last time we visited the directory
	a = new DateAttribute("dirstamp"
			      , null
			      , Attribute.COMPUTED) ;
	ATTR_DIRSTAMP = AttributeRegistry.registerAttribute(cls, a) ;
	// The negotiate flag
	a = new BooleanAttribute("negotiable"
				 , Boolean.FALSE
				 , Attribute.EDITABLE) ;
	ATTR_NEGOTIABLE = AttributeRegistry.registerAttribute(cls, a) ;
	// Our indexer name (optional).
	a = new StringAttribute("indexer"
				, null
				, Attribute.EDITABLE) ;
	ATTR_INDEXER = AttributeRegistry.registerAttribute(cls, a) ;
    }

    /**
     * Our current (cached) directory listing.
     */
    protected HtmlGenerator listing = null ;
    /**
     * The time at which we generated the directory index.
     */
    protected long listing_stamp = -1 ;


    private String getUnextendedName(String name) {
	int strlen = name.length() ;
	for (int i = 0 ; i < strlen ; i++) {
	    // FIXME: Should use the system props to get the right sep
	    if ( name.charAt(i) == '.' ) {
		if ( i == 0 )
		    return null ;
		return name.substring(0, i) ;
	    }
	}
	return null ;
    }
		
    /**
     * Update a negotiable resource.
     * Given the name of a resource that exists, create or update the 
     * attributes of a resource that allows to negotiate its content.
     * <p>I hate this part here: it has nothing to do within the directory
     * resource itself, and the indexer shouldn't know that much about
     * directory resource, so I am stuck.
     * @param name The name of the newly created resource.
     */

    public synchronized void updateNegotiableResource(String name) {
	// Does the maintainer really wants us to perform this ugly hack ?
	if ( ! getNegotiableFlag() )
	    return ;
	// Check for the corresponding negotiable resource:
	String noext = getUnextendedName(name) ;
	if ( noext == null ) {
	    return ;
	} else {
	    HTTPResource r = null;
	    try {
		r = (HTTPResource) lookup(noext) ;
	    } catch (InvalidResourceException ex) {
		r = null;
	    }
	    if ((r != null) && ! (r instanceof NegotiatedResource) )
		// I know I will pay for this
		return ;
	    // Okay, continue :-(
	    NegotiatedResource negotiated = (NegotiatedResource) r ;
	    if ( negotiated == null ) {
		String variants[] = new String[1] ;
		variants[0] = name ;
		negotiated  = new NegotiatedResource() ;
		Hashtable defs = new Hashtable(10) ;
		defs.put("identifier", noext) ;
		defs.put("context", getContext()) ;
		defs.put("url", getURLPath()+noext) ;
		defs.put("parent", this) ;
		defs.put("resource-store", children) ;
		defs.put("variants", variants) ;
		try {
		    negotiated.initialize(defs) ;
		} catch (Exception ex) {
		    ex.printStackTrace() ;
		    return ;
		}
		addResource(negotiated) ;
	    } else {
		String variants[]  = negotiated.getVariantNames() ;
		String nvariants[] = new String[variants.length+1] ;
		System.arraycopy(variants, 0, nvariants, 0, variants.length);
		nvariants[variants.length] = name ;
		negotiated.setValue(NegotiatedResource.ATTR_VARIANTS
				    , nvariants) ;
	    }
	}
    }

    /**
     * Get the indexer out of the given context.
     * @return A ResourceIndexer instance, guaranteeed not to be <strong>
     * null</strong>.
     */

    protected ResourceIndexer getIndexer(ResourceContext c) {
	IndexerModule   m = (IndexerModule) c.getModule(IndexerModule.NAME);
	ResourceIndexer p = m.getIndexer(c);
	return p;
    }
     
    public void setValue(int idx, Object value) {
	super.setValue(idx, value);
	if ( idx == ATTR_INDEXER ) {
	    String indexer = getString(ATTR_INDEXER, null);	    
	    if ( indexer != null ) {
		ResourceContext c = null;
		IndexerModule   m = null;
		c = getContext();
		m = (IndexerModule) c.getModule(IndexerModule.NAME);
		m.registerIndexer(c, indexer);
	    }
	}
    }

    /**
     * Try creating a default resource having the given name.
     * This method will make its best effort to create a default resource
     * having this name in the directory. If a file with this name exists,
     * it will check the pre-defined admin extensions and look for a match.
     * If a directory with this name exists, and admin allows to do so, it
     * will create a sub-directory resource.
     * @param name The name of the resource to try to create.
     * @return A HTTPResource instance, if possible, <strong>null</strong>
     *    otherwise.
     */

    public synchronized HTTPResource createDefaultResource(String name) {
	// Don't automagically create resources of name '..' or '.'
	if (name.equals("..") || name.equals("."))
	    return null ;
	// Is there a file with such a name ?
	File file = new File(getDirectory(), name) ;
	if ( ! file.exists() )
	    return null ;
	// If the file system is not case sensitive, emulate it :-(
	if ( getServer().checkFileSystemSensitivity() ) {
	    File directory = getDirectory();
	    if ( directory == null )
		return null;
	    String  files[] = directory.list();
	    boolean found   = false;
	    for (int i = 0 ; i < files.length ; i++) {
		if (found = files[i].equals(name))
		    break;
	    }
	    if ( ! found )
		return null;
	}
	// Prepare a set of default parameters for the resource:
	Hashtable defs = new Hashtable(10) ;
	defs.put("identifier", name);
	updateDefaultChildAttributes(defs);
	// Try to get the indexer to create the resource:
	ResourceContext context  = getContext();
	HTTPResource    resource = null;
	ResourceIndexer indexer  = null;
	ResourceIndexer lastidx  = null;
	while ( context != null ) {
	    // Lookup for next indexer in hierarchy:
	    do {
		indexer = getIndexer(context);
		context = context.getParent();
	    } while ((indexer == lastidx) && (context != null));
	    // Is this a usefull indexer ?
	    if ((lastidx = indexer) != null ) {
		resource = indexer.createResource(this
						  , null
						  , getDirectory()
						  , name
						  , defs) ;
		if ( resource != null ) 
		    break;
	    }
	}
	// Did we finally create a resource ?
	if ( resource != null ) {
	    // Register this child in our store:
	    addResource(resource) ;
	    // Update or create any relevant negotiable resource:
	    if ( getNegotiableFlag() ) 
		updateNegotiableResource(name) ;
	    markModified() ;
	}
	return resource ;
    }

    /**
     * Initialize and register a new resource into this directory.
     * @param resource The uninitialized resource to be added.
     */

    protected void updateDefaultChildAttributes(Hashtable attrs) {
	super.updateDefaultChildAttributes(attrs);
	String name = (String) attrs.get("identifier");
	if ( name != null )
	    attrs.put("directory", new File(getDirectory(), name));
    }

    /**
     * Get the physical directory exported by this resource.
     * @return A non-null File object giving the directory of this resource.
     */

    public File getDirectory() {
	return (File) getValue(ATTR_DIRECTORY, null) ;
    }

    /**
     * Get the optional icon directory.
     */

    public String getIconDirectory() {
	return getString(ATTR_ICONDIR, "/icons") ;
    }

    /**
     * Get the absolute time at which we examined the physicall directory.
     * @return The date (as a long number of ms since Java epoch), or
     * <strong>-1</strong> if we never examined it before.
     */

    public long getDirStamp() {
	return getLong(ATTR_DIRSTAMP, -1) ;
    }

    /**
     * Get the negotiable flag for this directory.
     * When turned to <strong>true</strong>, this flag indicates to the
     * directory resource that it should automatically build negotiated 
     * resources ont op of all existing resources.
     * <p>You should know, at least, that turning this flag on has some
     * not so small cost in terms of the size of the index files, and some
     * not so small costs in CPU time when detecting not found documents. 
     * Otherwise, in all other situations its cost is probably negligible.
     * @return A boolean, <strong>true</strong> if the directory is extensible
     * <strong>false</strong> otherwise.
     */

    public boolean getNegotiableFlag() {
	return getBoolean(ATTR_NEGOTIABLE, false) ;
    }

    /**
     * Initialize this directory resource with the given set of attributes.
     * @param values The attribute values.
     */

    public void initialize(Object values[]) {
	super.initialize(values) ;
	// Get our parent resource and compute our directory:
	File dir = null ;
	if ( ! definesAttribute(ATTR_DIRECTORY) ) {
	    // Get our parent:
	    HTTPResource parent = getParent() ;
	    if ((parent != null) && parent.definesAttribute("directory") ) {
		File pdir = (File) parent.getValue("directory", null);
		if ( pdir != null ) {
		    // Compute and set our directory attribute:
		    dir = new File(pdir, getIdentifier()) ;
		    setValue(ATTR_DIRECTORY, dir) ;
		}
	    }
	} else {
	    dir = getDirectory() ;
	}
	// Create that container context:
	ResourceContext c = new ResourceContext(getContext());
	setValue(ATTR_CONTEXT, c);
	// Register our specific indexer, if any:
	String indexer = getString(ATTR_INDEXER, null);
	if ( indexer != null ) {
	    IndexerModule   m = (IndexerModule)c.getModule(IndexerModule.NAME);
	    m.registerIndexer(c, indexer);
	}
    }

    /**
     * Enumerate all available children resource identifiers. 
     * This method <em>requires</em> that we create all our pending resources
     * if we are in the extensible mode...too bad !
     * @return An enumeration of all our resources.
     */
    
    public Enumeration enumerateResourceIdentifiers(boolean all) {
	// If extensible, update if needed:
	if (all && getExtensibleFlag() ) {
	    File directory = getDirectory() ;
	    if ( directory != null ) {
	        synchronized(this) {
		    long dirstamp  = directory.lastModified() ;
		    if ( dirstamp > getDirStamp() ) {
		        String lst[] = directory.list() ;
			if ( lst != null ) {
			    for (int i = 0 ; i < lst.length ; i++) {
			        if (lst[i].equals(".") || lst[i].equals(".."))
				    continue ;
				try {
				    if ( lookupStore(lst[i]) == null )
					createDefaultResource(lst[i]) ;
				} catch (InvalidResourceException ex) {
				}
			    }
			}
			setLong(ATTR_DIRSTAMP, dirstamp) ;
		    }
		}
	    }
	}
	return super.enumerateResourceIdentifiers(all);
    }

    /**
     * Reply with an HTML doc listing the resources of this directory.
     * This function takes special care not to regenerate a directory listing
     * when one is available. It also caches the date of the directory 
     * listing, so that it can win big with NOT_MODIFIED.
     * <p>Using a modem, I know that each place I can reply with an 
     * NOT_MODIFIED, <strong>is</strong> a big win.
     * @param request The request to handle.
     * @exception HTTPException If processsing the request failed.
     */

    public synchronized Reply getDirectoryListing(Request request)
	throws HTTPException
    {
	// Have we already an up-to-date computed a listing ?
	if ((listing == null) 
	    || (getDirectory().lastModified() > listing_stamp)
	    || (getLastModified() > listing_stamp)) {
	    Enumeration   enum      = enumerateResourceIdentifiers() ;
	    Vector        resources = Sorter.sortStringEnumeration(enum) ;
	    HtmlGenerator g         = new HtmlGenerator("Directory of "
							+ getIdentifier());
	    g.append("<h1>"+getIdentifier()+"</h1>");
	    // Link to the parent, when possible:
	    if ( getParent() != null )
		g.append("<p><a href=\"..\">Parent</a><br>");
	    // List the children:
	    for (int i = 0 ; i < resources.size() ; i++) {
		String       name     = (String) resources.elementAt(i);
		HTTPResource resource = null;
		// The resource may be null, if some loadResource failed:
		try {
		    resource = lookupStore(name);
		} catch (InvalidResourceException ex) {
		    g.append(name+" cannot be loaded (server misconfigured)");
		    g.append("<br>");
		    continue;
		}
		// Icon first, if available
		String icon = resource.getIcon() ;
		if ( icon != null ) 
		    g.append("<img src=\""
			     + getIconDirectory() +"/" + icon
			     + "\">");
		// Resource's name with link:
		g.append("<a href=\"" 
                         , resource.getURLPath()
			 , "\">"+name+"</a>");
		// resource's title, if any:
		String title = resource.getTitle();
		if ( title != null )
		    g.append(" "+title);
		g.append("<br>");
	    }
	    g.close() ;
	    listing_stamp = getLastModified() ;
	    listing       = g ;
	} else if ( checkIfModifiedSince(request) == COND_FAILED ) {
            // Is it an IMS request ?
	    return createDefaultReply(request, HTTP.NOT_MODIFIED) ;
	} 
	// New content or need update:
	Reply reply = createDefaultReply(request, HTTP.OK) ;
	reply.setLastModified(listing_stamp) ;
	reply.setStream(listing) ;
	return reply ;
    }

    /**
     * Check the <code>If-Modified-Since</code> condition of that request.
     * @param request The request to check.
     * @return An integer, either <code>COND_FAILED</cond> if condition
     * was checked, but failed, <code>COND_OK</code> if condition was checked
     * and succeeded, or <strong>0</strong> if the condition was not checked
     * at all (eg because the resource or the request didn't support it).
     */

    public int checkIfModifiedSince(Request request) {
	long ims = request.getIfModifiedSince();
	if (ims >= 0) 
	    return (((listing_stamp > 0) && (listing_stamp - 1000 <= ims))
		    ? COND_FAILED
		    : COND_OK);
	return 0;
    }

    /**
     * GET on a directory, generate a directory listing.
     * @param request The request to handle.
     */

    public Reply get(Request request)
	throws HTTPException
    {
	return getDirectoryListing(request) ;
    }

    /**
     * Delete this directory resource, for ever.
     * This method will delete the directory resource, and its associated 
     * store, <strong>along</strong> with any of the sub-resources it contains.
     * Deleting the root directory of your server might take sometime...
     * <p>Once the resource is deleted, it is removed from its inital store
     * and will not be unpickleable any more.
     */

    public synchronized void delete() {
	// Remove all the defined resources in this directory
	// Set the extensible flag to false, otherwise, the directory grows
	// as we shrink it :-)
        setBoolean(ATTR_EXTENSIBLE, false);
	Enumeration enum = enumerateResourceIdentifiers();
	while (enum.hasMoreElements()) {
	    delete((String) enum.nextElement());
	}
	super.delete();
    }

    public synchronized boolean verify() {
	File dir = getDirectory();
	if ( ! dir.exists() ) {
	    // Is the parent extensible:
	    HTTPResource p = getParent() ;
	    while ((p != null) && ! (p instanceof DirectoryResource) )
		p = p.getParent() ;
	    if ( p == null )
		return false;
	    DirectoryResource d = (DirectoryResource) p;
	    if ( ! d.getExtensibleFlag() ) 
		return false;
	    // Emit an error message, and delete the resource:
	    String msg = dir+": deleted, removing the FileResource.";
	    getServer().errlog(this, msg);
	    delete();
	    return false;
	} else {
	    return true;
	}
    }

    /**
     * Create an empty resource store, and store a default resource directory
     * in it.
     */

    public static void main(String args[]) 
	throws Exception
    {
	String repository = args[0];
	File   directory  = new File(args[1]) ;
	String url        = args[2] ;
	String identifier = args[3] ;
	Class  cls        = null ;

	// Get a pointer to the class:
	try {
	    cls = Class.forName("w3c.jigsaw.resources.DirectoryResource") ;
	} catch (Exception ex) {
	    ex.printStackTrace() ;
	    System.exit(1) ;
	}
				 
	// Create a fresh resource store:
	ResourceStore store = new SimpleResourceStore() ;
	store.initialize(null, null, new File(repository));

	// Create the Directoryresource itself:
	Attribute         attrs[] = AttributeRegistry.getClassAttributes(cls);
	DirectoryResource resource = new DirectoryResource() ;
	
	// Create a default attribute value set for the directory resource:
	Hashtable defs = new Hashtable(11) ;
	defs.put("directory", directory) ;
	defs.put("identifier", identifier) ;
	defs.put("url", url) ;
	defs.put("resource-store", store) ;
	defs.put("repository", repository);
	// Initialize the directory resource:
	resource.initialize(defs) ;

	// Add it to the store:
	store.addResource(resource) ;


	store.shutdown() ;
    }

}
