// DirectoryResource.java
// $Id: DirectoryResource.java,v 1.4 1997/06/10 16:11:18 abaird Exp $
// (c) COPYRIGHT MIT and INRIA, 1996.
// Please first read the full copyright statement in file COPYRIGHT.html

package w3c.tools.resources.http;

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

import w3c.tools.resources.FilteredResource;
import w3c.tools.resources.ResourceContext;
import w3c.tools.resources.http.HTTPResource;
import w3c.tools.resources.Resource;
import w3c.tools.resources.ResourceStore;
import w3c.tools.resources.InvalidResourceException;
import w3c.tools.resources.LookupState;
import w3c.tools.resources.indexer.ResourceIndexer;
import w3c.tools.resources.*;
import w3c.tools.resources.impl.*;
import w3c.tools.store.*;
import w3c.tools.sorter.*;
import w3c.jigsaw.http.* ;
import w3c.jigsaw.indexer.* ;
import w3c.jigsaw.html.* ;
import w3c.jigsaw.resources.*;
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 ResourceContainerImpl {

  protected String iconDirectory = null;

  protected long dirStamp = -1;

  protected boolean negotiableFlag = false;

  protected String indexer = null;

  /**
   * Our current (cached) directory listing.
   */
  protected transient 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;
      ResourceReference rr = null;
      rr = resolve( new LookupState(space, noext));
      try {
	if (rr != null) r = (HTTPResource)rr.lock();
	if ((r != null) && ! (r instanceof NegotiatedResource))
	  // I know I will pay for this
	  return ;
	// Okay, continue :-(
	NegotiatedResource negotiated = (NegotiatedResource) r;
	if (negotiated == null ) {
	  negotiated = new NegotiatedResource();
	  String variants[] = new String[1];
	  variants[0] = name;
	  HTTPResourceContextImpl context = 
	    new HTTPResourceContextImpl((HTTPResourceContext)getContext());
	  context.addProperty("name",noext);
	  context.addProperty("urlPath", getUrlPath()+noext);
	  context.addProperty("variants", variants);
	  try {
	    registerChild(negotiated,context);
	  } catch (ChildNotSupportedException ex) {
	    System.out.println(ex.getMessage());
	    ex.printStackTrace();
	  } catch (ResourceInitException ex) {
	    System.out.println(ex.getMessage());
	    ex.printStackTrace();
	  }
	} else {
	  String variants[]  = negotiated.getVariants() ;
	  String nvariants[] = new String[variants.length+1] ;
	  System.arraycopy(variants, 0, nvariants, 0, variants.length);
	  nvariants[variants.length] = name ;
	  negotiated.setVariants(nvariants);
	}
      } finally {
	if (rr != null) rr.unlock();
      }
    }
  }

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

  protected ResourceIndexer getIndexer(ResourceContext c) {
    String          i = getIndexer();
    //IndexerModule   m = (IndexerModule) c.getModule(IndexerModule.NAME);
    //    ResourceIndexer p = m.getIndexer(c);
    //    return p;
    return null;
  }

  public String getIndexer() {
    return (String)holder.protectedGetValue("indexer");
  }

  public void setIndexer(String indexer) {
    holder.protectedSetValue("indexer",indexer);
  }

  public String igetIndexer() {
    return indexer;
  }

  public void isetIndexer(String indexer) {
    this.indexer = 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 ResourceReference 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;
    //    }

    HTTPResourceContext c = (HTTPResourceContext) getContext().getClone();
    c.addProperty("filename",name);
    c.addProperty("name",name);
    // FIXME
    c.addProperty("contentType", w3c.www.mime.MimeType.TEXT_HTML);
    c.setFile(file);
    ResourceIndexer indexer = null;
    if ( indexerName != null )
      indexer = c.getIndexer(indexerName);
    else
      indexer = c.getIndexer();
    c.setContainer(this.self);
    HTTPResource child = null;
    try {
      child = indexer.index(c);
      if ( child != null ) {
	ResourceReference rr = registerChild(child, c);
	if ( getNegotiableFlag() ) 
	  updateNegotiableResource(name) ;
	markModified() ;
	return rr;
      }
      else return null;
    } catch (ResourceInitException ex) {
      System.out.println(ex.getMessage());
      ex.printStackTrace();
      return null;
    } catch (ChildNotSupportedException ex) {
      System.out.println(ex.getMessage());
      ex.printStackTrace();
      return null;
    }
  }

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

  public String getIconDirectory() {
      String icondir = (String) holder.protectedGetValue("iconDirectory");
      if ( icondir != null )
	  return icondir;
      return "/icons";
  }

  public void setIconDirectory(String iconDirectory) {
    holder.protectedSetValue("iconDirectory",iconDirectory);
  }

  public String igetIconDirectory() {
    return iconDirectory;
  }

  public void isetIconDirectory(String iconDirectory) {
    this.iconDirectory = iconDirectory;
  }

  /**
   * 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 ((Long)holder.protectedGetValue("dirStamp")).longValue();
  }

  public void setDirStamp(long dirStamp) {
    holder.protectedSetValue("dirStamp", new Long(dirStamp));
  }

  public long igetDirStamp() {
    return dirStamp;
  }

  public void isetDirStamp(long dirStamp) {
    this.dirStamp = dirStamp;
  }

  /**
   * 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 ((Boolean)
	    holder.protectedGetValue("negotiableFlag")).booleanValue();
  }

  public void setNegotiableFlag(boolean negotiableFlag) {
    holder.protectedSetValue("negotiableFlag", new Boolean(negotiableFlag));
  }

  public boolean igetNegotiableFlag() {
    return negotiableFlag;
  }

  public void isetNegotiableFlag(boolean negotiableFlag) {
    this.negotiableFlag = negotiableFlag;
  }

/**
   * Register and init the given resource as a new child of the container.
   * @param resource The resource to be added as a child.
   * @param ctxt The init context to be used to init the child.
   * @exception ChildNotSupportedException If the container doesn't
   * support that kind of a child.
   * @exception ResourceInitException If the resource didn't init.
   * @exception SecurityException If that access is denied.
   */
  public ResourceReference registerChild(Resource resource, 
					 ResourceContext context) 
    throws ChildNotSupportedException, ResourceInitException
  {
    ResourceContext newContext = myContext.getClone();
    File childDirectory = new File(directory, 
				   (String)context.getProperty("name"));
    newContext.addProperty("directory",childDirectory);
    // merge parent context and given one
    String newnames[]  = context.getPropertyNames();
    Object newvalues[] = context.getPropertyValues();
    int i = 0;
    while (i < newnames.length) {
      newContext.addProperty(newnames[i],newvalues[i]);
      i++;
    }
    newContext.setContainer(self);
    resource.create(newContext); 

    String name = resource.getName();
    PersistentReference pr = store.getPersistentReference(name);
    ResourceCache cache = ((ResourceSpaceImpl)space).getCache();
    ResourceReference rr = cache.getReference(pr,resource);
    
    newContext.setResourceReference(rr);
    resource.init(newContext);

    space.save(resource);
    catalog.put(name,rr);
    return rr;
  }


  public void init(ResourceContext context) 
    throws ResourceInitException
  {
    super.init(context);
    if (directory != null) {
      //      directory = new File(directory,getName());
      if (! directory.exists())
	directory.mkdirs();
      if (! directory.isDirectory())
	throw new ResourceInitException(directory.getAbsolutePath()+
					" exists and is not a directory.");
    }
  }

  /**
   * 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() ) {
      Vector V = new Vector(20);
      File directory = igetDirectory() ;
      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 ;
		ResourceReference rr = null;
		rr = resolve( new LookupState(space,lst[i]));
		}
 	      }
	    }
	    setDirStamp(dirstamp);
	  }
	}
      }
    return listChildNames();
  }

  /**
   * 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) 
 	|| (igetDirectory().lastModified() > listing_stamp)
 	|| (getLastModified() > listing_stamp)) {
      Enumeration   enum      = enumerateResourceIdentifiers(true) ;
      Vector        resources = Sorter.sortStringEnumeration(enum) ;
      HtmlGenerator g         = new HtmlGenerator("Directory of "+
 						  getName());
      g.append("<h1>"+getName()+"</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);
	ResourceReference rr = null;
 	HTTPResource resource = null;
 	// The resource may be null, if some loadResource failed:
	rr = resolve( new LookupState(space, name));
	if (rr == null) {
	  g.append(name+" cannot be loaded (server misconfigured)");
	  g.append("<br>");
	  continue;
	}
 	try {
 	  resource = (HTTPResource)rr.lock();
	  // 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>");
	} finally {
	  rr.unlock();
	}
      }
      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 :-)
    setExtensibleFlag(false);
    Enumeration enum = enumerateResourceIdentifiers(true);
    while (enum.hasMoreElements()) {
       deleteChild((String) enum.nextElement());
    }
  }

  public synchronized boolean verify() {
    File dir = igetDirectory();
    if ( ! dir.exists() ) {
      // Is the parent extensible:
      ResourceReference rr = getParent();
      try {
	ResourceContainerImpl p = (ResourceContainerImpl) rr.lock();
	if ( ! p.getExtensibleFlag() ) 
	  return false;
      } finally {
	rr.unlock();
      }
      // 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;
    }
  }

  public DirectoryResource () { 
  }
  
  public DirectoryResource (ResourceSpace space, ResourceStore store) {
    super(space,store);  
  }

  public ResourceReference[] getFilters() {return null;}
  public ResourceReference[] getFilters(Class cls) {return null; }
  public void registerFilter(ResourceReference filter, Hashtable defs){}
  public void unregisterFilter(ResourceReference filter){};

  static final long serialVersionUID = 2712991455961981039L;

}
