// StoreContainer.java
// $Id: StoreContainer.java,v 1.13 1998/01/23 11:58:15 bmahe 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 java.net.*;

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

/**
 * The abstract, store container resource class.
 * <p>This class extends the basic ResourceContainer class to provide 
 * the management of the storage of children resources.
 * <p>It provides means to add/remove children resources, and implement
 * a basic lookup functionality. If marked extensible it will callback
 * the <code>createDefaultResource</code> method to create them.
 */

public abstract class StoreContainer extends ContainerResource
    implements ResourceStoreHolder
{
    private static final boolean debugstore = false;

    /**
     * Attribute index - The index of our store identifier.
     */
    protected static int ATTR_REPOSITORY = -1 ;
    /**
     * Attribute index - The index of wether we are extensible.
     */
    protected static int ATTR_EXTENSIBLE = -1 ;
    /**
     * Attribute index - The index for our relocate attribute.
     */
    protected static int ATTR_RELOCATE = -1 ;
    /**
     * Attribute index - our index resource name.
     */
    protected static int ATTR_INDEX = -1 ;

    static {
	Attribute a   = null ;
	Class     cls = null ;
	// Get a pointer to our class.
	try {
	    cls = Class.forName("w3c.jigsaw.resources.StoreContainer") ;
	} catch (Exception ex) {
	    ex.printStackTrace() ;
	    System.exit(1) ;
	}
	// Our store identifier attribute (relative to our directory)
	a = new StringAttribute("repository"
				, null
				, Attribute.EDITABLE|Attribute.MANDATORY);
	ATTR_REPOSITORY = AttributeRegistry.registerAttribute(cls, a) ;
	// Are we extensible (can we create resources on the fly):
	a = new BooleanAttribute("extensible"
				 , Boolean.TRUE
				 , Attribute.EDITABLE) ;
	ATTR_EXTENSIBLE = AttributeRegistry.registerAttribute(cls, a) ;
	// Should we relocate invalid request to this directory ?
	a = new BooleanAttribute("relocate"
				 , Boolean.TRUE
				 , Attribute.EDITABLE);
	ATTR_RELOCATE = AttributeRegistry.registerAttribute(cls, a) ;
	// Our index resource name (optional).
	a = new StringAttribute("index"
				, null
				, Attribute.EDITABLE) ;
	ATTR_INDEX = AttributeRegistry.registerAttribute(cls, a) ;
    }

    /**
     * Our children resource store.
     */
    protected ResourceStore children = null ;

    // Our real repository file, as an absolute file.
    private File repository = null;

    /**
     * Acquire the resource store.
     * Should only be called from a synchronized method.
     */

    protected synchronized void acquireChildren() {
	if ( children == null ) {
	    ResourceStoreManager m = getServer().getResourceStoreManager();
	    // If I don't have an associated resource store, create one:
	    if ( getValue(ATTR_REPOSITORY, null) == null ) {
		String               f = m.createResourceStoreRepository();
		setValue(ATTR_REPOSITORY, f);
	    }
	    if ( debugstore )
		System.out.println(getURLPath()
				   + ": acquireChildren ["
                                   + getRepository() + "]");
	    // Now load this store into memory:
	    children = m.loadResourceStore(this, getRepository());
	}
	return ;
    }

    /**
     * Add an initialized resource into this store container instance.
     * @param resource The resource to be added to the store.
     */

    protected synchronized void addResource(Resource resource) {
        acquireChildren();
	children.addResource(resource);
	markModified() ;
    }

    /**
     * Save the current store to disk.
     * This store container relies on its resource store for this operation.
     */

    protected synchronized void save() {
	if ( children != null ) 
	    children.save();
    }

    /**
     * ContainerInterface implementation - Do we manage a store ?
     * @return Always <strong>true</strong>.
     */

    public final boolean hasResourceStore() {
	return true;
    }

    /**
     * ContainerInterface implementation - Hold underlying resource store.
     * @param h The resource store holder.
     * @return A pointer to our underlying store.
     */

    public synchronized ResourceStore getResourceStore(ResourceStoreHolder h) {
	acquireChildren();
	children.addHolder(h);
	return children;
    }

    /**
     * Clone a store container.
     * Cloning a store container doesn't clone the associated resource store.
     * The clone gets a new, empty resource store by default.
     * @param values The initial attribute values for the clone.
     * @return A StoreContainer resource, having an empty store, but otherwise
     * similar to its master.
     */

    public Object getClone(Object values[]) {
	StoreContainer sc = (StoreContainer) super.getClone(values);
	// Get rid of the shared store reference:
	sc.setValue(ATTR_REPOSITORY, null);
	return sc;
    }

    /**
     * Keep our cached repository value in sync.
     * @param idx The index of the attribute to set.
     * @param value The new value for the attribute.
     */

    public synchronized void setValue(int idx, Object value) {
	super.setValue(idx, value);
	if ( idx == ATTR_REPOSITORY )
	    repository = null;
    }

    /**
     * Get our children resource store file.
     * If we haven't created yet our backup file, we create it right now.
     * @return A non-null File instance giving the location of the file
     * used to dump our store.
     */

    public synchronized File getRepository() {
	if ( repository == null ) {
	    String rpath = (String) getValue(ATTR_REPOSITORY, null);
	    if ( rpath == null )
		return null;
	    repository = (File) new File(rpath);
	    if ( ! repository.isAbsolute() )
		repository = new File(getServer().getStoreDirectory(), rpath);
	}
	return repository;
    }
  
    /**
     * Get the extensible flag value.
     * A DirectoryResource is extensible, if it is allowed to create new
     * resources out of the file system knowledge on the fly.
     * <p>Setting this flag might slow down the server. It unfortunatelly
     * defaults to <strong>true</strong> until I have a decent admin
     * program.
     * @return A boolean <strong>true</strong> if the directory is
     *    extensible.
     */

    public boolean getExtensibleFlag() {
	return getBoolean(ATTR_EXTENSIBLE, true) ;
    }

    /**
     * Should we relocate invalid requests to this directory.
     * @return A boolean <strong>true</strong> if we should relocate.
     */

    public boolean getRelocateFlag() {
	return getBoolean(ATTR_RELOCATE, true) ;
    }

    /**
     * Get the optinal index name for this directory listing.
     * @return The name of the resource responsible to list that container.
     */

    public String getIndex() {
	return (String) getValue(ATTR_INDEX, null) ;
    }

    /**
     * ResourceStoreHolder implementation - Get rid of our store.
     * The resource store manager has decided that our store hasn't been
     * used enough in the past to be worth keeping around. 
     * <p>We can still defer this operation by returning <strong>false</strong>
     * in case a user is editing the store for example.
     * @param store The store that to be freed.
     * @return A boolean <strong>true</strong> if the resource store has been
     * shutdown properly, <strong>false</strong> otherwise.
     */

    public synchronized boolean acceptStoreUnload(ResourceStore store) {
	if ( debugstore )
	    System.out.println(getURLPath()
			       +" acceptStoreUnload "+getRepository());
	if ( store != children )
	    throw new RuntimeException("Inconsistency in storage manager.");
	return true ;
    }

    /**
     * ResourceStoreHolder implementation - Shutdown our associated store.
     * @param store The store to shutdown.
     */

    public synchronized void notifyStoreShutdown(ResourceStore store) {
	if ( debugstore )
	    System.out.println(getURLPath()
			       +" notifyStoreShutdown "+getRepository());
	if ( store != children ) 
	    throw new RuntimeException("Inconsistency in storage manager:");
	children = null ;
    }

    /**
     * ResourceStoreHolder implementation - Save our store.
     * Our store has probably been modified recently, save it.
     * @param store The store to save.
     * @return A boolean <strong>true</strong> if success.
     */

    public void notifyStoreStabilize(ResourceStore store) {
	if ( debugstore )
	    System.out.println(getURLPath()
			       +" notifyStoreStabilize "+getRepository());
	return;
    }

    /**
     * Update default child attributes.
     * A parent can often pass default attribute values to its children,
     * such as a pointer to itself (the <em>parent</em> attribute).
     * <p>This is the method to overide when you want your container
     * to provide these kinds of attributes. By default this method will set
     * the following attributes:
     * <dl><dt>name<dd>The name of the child (it's identifier) - 
     * String instance.
     * <dt>parent<dd>The parent of the child (ie ourself here) - 
     * a ContainerResource instance.
     * <dt>resource-store<dd>This child store - a ResourceStore instance.
     * <dt>server<dd>The server context of the child - a httpd instance.
     * <dt>url<dd>If a <em>identifier</em> attribute is defined, that
     * attribute is set to the full URL path of the children.
     * </dl>
     */

    protected void updateDefaultChildAttributes(Hashtable attrs) {
        acquireChildren();
	attrs.put("parent", this) ;
	attrs.put("resource-store", children) ;
	attrs.put("context", getContext()) ;
	String name = (String) attrs.get("identifier");
	if ( name != null )
	    attrs.put("url", getURLPath()+name);
    }

    public synchronized void registerResource(String identifier
					      , Resource resource
					      , Hashtable defs) {
	acquireChildren();
	// Create a default set of attributes:
	if ( defs == null )
	    defs = new Hashtable(11) ;
	defs.put("identifier", identifier);
	updateDefaultChildAttributes(defs);
	// Initialize and register theresource to the store:
	resource.initialize(defs) ;
	addResource(resource);
    }

    /**
     * Lookup our store for a child of the given name.
     * This methods may trigger, on its first call, the creation of the
     * children ResourceStore wich is done is a lazy way, so that areas
     * not often visited don't get their store loaded.
     * @param name The name of the child to lookup.
     * @param defs A set of default attribute values.
     * @return A Resource instance, or <strong>null</strong> if no
     *     match was found.
     * @exception InvalidResourceException If the resource exists, but cannot
     * be unloaded from the store.
     */

    protected synchronized HTTPResource lookupStore(String name 
						    , Hashtable defs) 
	throws InvalidResourceException
    {
	// Check that our children repository is still valid:
	acquireChildren() ;
	// Is this child already loaded ?
	HTTPResource child = (HTTPResource) children.lookupResource(name) ;
	if ( child != null )
	    return child ;
	// Set mandatory default children attributes:
	if ( defs == null ) 
	    defs = new Hashtable(5) ;
	defs.put("identifier", name);
	updateDefaultChildAttributes(defs);
	// Restore the child:
	try {
	    return (HTTPResource) children.loadResource(name, defs) ;
	} catch (InvalidResourceException ex) {
	    // Emit an error in the server log:
	    String msg ="cannot restore \""+name+"\" ("+ex.getMessage()+")";
	    getServer().errlog(this, msg);
	    throw ex;
	}
    }

    /**
     * Lookup our store for a child of the given name.
     * This methods may trigger, on its first call, the creation of the
     * children ResourceStore wich is done is a lazy way, so that areas
     * not often visited don't get their store loaded.
     * @param name The name of the child to lookup.
     * @return A HTTPResource instance, or <strong>null</strong> if no
     *     match was found.
     */

    protected HTTPResource lookupStore(String name)
	throws InvalidResourceException
    {
	return lookupStore(name, null);
    }

    /**
     * Lookup the resource having the given name in this directory.
     * @param name The name of the resource.
     * @return A resource instance, or <strong>null</strong>.
     */

    public Resource lookup(String name) 
	throws InvalidResourceException
    {
	// Try our store:
	HTTPResource resource = lookupStore(name) ;
	if ( resource != null )
	    return resource ;
	// If allowed, than try a default fallback:
	return getExtensibleFlag() ? createDefaultResource(name) : null ;
    }

    /**
     * Lookup the next component of this lookup state in here.
     * @param ls The current lookup state.
     * @param lr The lookup result under construction.
     * @exception HTTPException If an error occurs.
     * @return A boolean, <strong>true</strong> if lookup has completed, 
     * <strong>false</strong> if it should be continued by the caller.
     */

    public boolean lookup(LookupState ls, LookupResult lr) 
	throws HTTPException
    {
	// Give a chance to our super-class to run its own lookup scheme:
	if ( super.lookup(ls, lr) ) {
	    if ( ! ls.isDirectory() && ! ls.isInternal() ) {
		// The directory lookup URL doesn't end with a slash:
		Request request = ls.getRequest() ;
		if ( request == null ) {
		    lr.setTarget(null);
		    return true;
		}
		// Emit an appropriate relocation or error
		URL url = null;
		try {
		    url = (ls.hasRequest() 
			   ? getURL(request)
			   : new URL(getServer().getURL(), getURLPath()));
		} catch (MalformedURLException ex) {
		    getServer().errlog(this, "unable to build full URL.");
		    throw new HTTPException("Internal server error");
		}
		String msg = "Invalid requested URL: the directory resource "
		    + " you are trying to reach is available only through "
		    + " its full URL: <a href=\""
		    + url + "\">" + url + "</a>.";
		if ( getRelocateFlag() ) {
		    // Emit an error (with reloc if allowed)
		    Reply reloc = request.makeReply(HTTP.MOVED_TEMPORARILY);
		    reloc.setContent(msg) ;
		    reloc.setLocation(url);
		    lr.setTarget(null);
		    lr.setReply(reloc);
		    return true;
		} else {
		    Reply error = request.makeReply(HTTP.NOT_FOUND) ;
		    error.setContent(msg) ;
		    lr.setTarget(null);
		    lr.setReply(error);
		    return true;
		}
	    } else if ( ! ls.isInternal() ) {
		String index = getIndex();
		if ( index != null ) {
		    try {
			HTTPResource rindex = (HTTPResource) lookup(index) ;
			if ( rindex != null ) 
			    return rindex.lookup(ls, lr);
		    } catch (InvalidResourceException ex) {
		    }
		}
	    } 
	    return true;
	}
	// Perform our own lookup on the next component:
	String       name     = ls.getNextComponent() ;
	HTTPResource resource = null;
	try {
	    resource = (HTTPResource) lookup(name) ;
	} catch (InvalidResourceException ex) {
	    resource = null;
	}
	lr.setTarget(resource);
	return (resource != null ) ? resource.lookup(ls, lr) : false;
    }

    /**
     * We are being unloaded.
     * Cleanup-up our attribute values, and make sure our store is closed.
     */

    public synchronized void notifyUnload() {
	// Cleanup up our children store, if needed:
	if ( children != null ) {
	    ResourceStoreManager man = getServer().getResourceStoreManager();
	    man.unholdResourceStore(this, getRepository()) ;
	    children = null ;
	}
	// Super unload:
	super.notifyUnload() ;
    }

    /**
     * Delete that resource store.
     * This also removes the store we are managing.
     */

    public synchronized void delete() {
	// Cleanup up our children store, if needed:
	if ( children != null ) {
	    ResourceStoreManager man = getServer().getResourceStoreManager();
	    man.unholdResourceStore(this, getRepository()) ;
	    children = null ;
	}
	// Erase the resource store, if any
	File repository = getRepository();
	if ( repository != null )
	    repository.delete();
	// More job ?
	super.delete();
    }

    /**
     * Delete a given child of this directory resource.
     * @param child The identifier of the child to delete.
     */

    public void delete(String child) {
	try {
	    HTTPResource resource = (HTTPResource) lookup(child);
	    if ( resource != null )
		resource.delete();
	} catch (InvalidResourceException ex) {
	    // We really cannot remove that resource if we can't unload it.
	}
    }

    /**
     * Enumerate the name (ie identifiers) of our children.
     * @param all Should all resources be listed.
     * @return An enumeration, providing one element per child, which is
     * the name of the child, as a String.
     */

    public synchronized Enumeration enumerateResourceIdentifiers(boolean all) {
	// Check that our children repository is still valid:
	acquireChildren() ;
	return children.enumerateResourceIdentifiers() ;
    }

    /**
     * Initialize ourself.
     * As we are a container resource that really contains something, we make
     * sure our URL ends properly with a slash.
     * @param values Our default attribute values.
     */

    public void initialize(Object values[]) {
        super.initialize(values);
	// If my URL doesn't end with a slah, correct it:
	String url = getURLPath() ;
	if ((url != null) && ! url.endsWith("/") )
	    setValue(ATTR_URL, url+"/") ;
    }

}



