// HTTPFrame.java
// $Id: HTTPFrame.java,v 1.63.4.6 2000/01/20 17:46:51 ylafon Exp $
// (c) COPYRIGHT MIT and INRIA, 1997.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.jigsaw.frames;

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

import org.w3c.tools.codec.*;
import org.w3c.tools.sorter.*;
import org.w3c.tools.resources.*;
import org.w3c.tools.resources.event.*;
import org.w3c.jigsaw.http.* ;
import org.w3c.jigsaw.html.* ;
import org.w3c.www.mime.*;
import org.w3c.www.http.*;
import org.w3c.tools.crypt.Md5;

import org.w3c.tools.resources.ProtocolException;
import org.w3c.tools.resources.ResourceException;

/**
 * Default class to handle the HTTP protocol, manage FileResource and
 * DirectoryResource.
 */
public class HTTPFrame extends ProtocolFrame {

    public static final 
    String STATE_CONTENT_LOCATION = "org.w3c.jigsaw.frames.HTTPFrame.cl";
    
    private static final boolean debug = true;

    /**
     * Condition check return code - Condition existed but failed.
     */
    public static final int COND_FAILED = 1;
    /**
     * Condition check return code - Condition existed and succeeded.
     */
    public static final int COND_OK = 2;
  
    /**
     * Methods allowed by that class in general:
     */
    protected static HttpTokenList _allowed        = null;

    private   static HttpTokenList _put_allowed    = null;
    private   static HttpTokenList _browse_allowed = null;
    private   static HttpTokenList _accept_ranges  = null;

    static {
	// allowed shares _allowed value, that's a feature.
	String str_allowed[] = { "GET", "HEAD", "OPTIONS", "TRACE"};
	_allowed = HttpFactory.makeStringList(str_allowed);
	String str_allow[] = { "HEAD" , "GET" , "PUT" , "OPTIONS", "TRACE" };
	_put_allowed = HttpFactory.makeStringList(str_allow);
	String str_all[] = { "HEAD" , "GET" , "BROWSE" , "OPTIONS","TRACE" };
	_browse_allowed = HttpFactory.makeStringList(str_allow);
	String accept_ranges[] = { "bytes" };
	_accept_ranges = HttpFactory.makeStringList(accept_ranges);
    }
    /**
     * Methods allowed by instances of that class in particular:
     */
    protected        HttpTokenList  allowed = _allowed;


    /**
     * Attributes index - The index for the quality attribute.
     */
    protected static int ATTR_QUALITY = -1 ;
    /**
     * Attribute index - The index for the title attribute.
     */
    protected static int ATTR_TITLE = -1 ;
    /**
     * Attribute index - The index for the content languages attribute.
     */
    protected static int ATTR_CONTENT_LANGUAGE = -1 ;
    /**
     * Attribute index - The index for the content encodings attribute.
     */
    protected static int ATTR_CONTENT_ENCODING = -1 ;
    /**
     * Attribute index - The index for the content type attribute.
     */
    protected static int ATTR_CONTENT_TYPE = -1 ;
    /**
     * Attribute index - The index for the charset attribute.
     */
    protected static int ATTR_CHARSET = -1 ;
    /**
     * Attribute index - The index for the content length attribute.
     */
    protected static int ATTR_CONTENT_LENGTH = -1 ;
    /**
     * Attribute index - The icon (if any) associated to the resource.
     */
    protected static int ATTR_ICON = -1 ;
    /**
     * Attribute index - Max age: the maximum drift allowed from reality.
     */
    protected static int ATTR_MAXAGE = -1 ;
    /**
     * Attribute index - Send MD5 Digest: the md5 digest of the resource sent
     */
    protected static int ATTR_MD5 = -1;
    /**
     * Attribute index - delete allowed for the associated resource ?
     */
    protected static int ATTR_ALLOW_DEL = -1;

    //
    // Attribute relative to FileResource
    //

    /**
     * Attribute index - Do we allow PUT method on this file.
     */
    protected static int ATTR_PUTABLE = -1 ;

    //
    // Attribute relative to DirectoryResource
    //

    /**
     * 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 ;
    /**
     * Attribute index - The icon directory to use in dir listing.
     */
    protected static int ATTR_ICONDIR = -1 ;
    /**
     * Attribute index - Allow the GNN browse method.
     */
    protected static int ATTR_BROWSABLE = -1 ;
    /**
     * Attribute index - Style sheet for directory listing
     */
    protected static int ATTR_STYLE_LINK = -1 ;

    static {
	Attribute a   = null ;
	Class     cls = null ;

	// Get a pointer to our class:
	try {
	    cls = Class.forName("org.w3c.jigsaw.frames.HTTPFrame") ;
	} catch (Exception ex) {
	    ex.printStackTrace() ;
	    System.exit(1) ;
	}
	// The quality attribute:
	a = new DoubleAttribute("quality"
				, new Double(1.0) 
				, Attribute.EDITABLE);
	ATTR_QUALITY = AttributeRegistry.registerAttribute(cls, a) ;
	// The title attribute:
	a = new StringAttribute("title"
				, null
				, Attribute.EDITABLE) ;
	ATTR_TITLE = AttributeRegistry.registerAttribute(cls, a) ;
	// The content language attribute:
	a = new LanguageAttribute("content-language"
				  , null
				  , Attribute.EDITABLE) ;
	ATTR_CONTENT_LANGUAGE = AttributeRegistry.registerAttribute(cls,a);
	// The content encoding attribute:
	a = new EncodingAttribute("content-encoding"
				  , null
				  , Attribute.EDITABLE) ;
	ATTR_CONTENT_ENCODING = AttributeRegistry.registerAttribute(cls,a);
	// The content type attribute:
	a = new MimeTypeAttribute("content-type"
				  , null
				  , Attribute.EDITABLE) ;
	ATTR_CONTENT_TYPE = AttributeRegistry.registerAttribute(cls,a);
	// The Charset attribute:
	a = new StringAttribute("charset"
				, null
				, Attribute.EDITABLE) ;
	ATTR_CHARSET = AttributeRegistry.registerAttribute(cls,a);
        // The content length attribute:
	a = new IntegerAttribute("content-length"
				 , null
				 , Attribute.COMPUTED);
	ATTR_CONTENT_LENGTH = AttributeRegistry.registerAttribute(cls,a);
	// The icon attribute:
	a = new StringAttribute("icon"
				, null
				, Attribute.EDITABLE) ;
	ATTR_ICON = AttributeRegistry.registerAttribute(cls, a) ;
	// The max age attribute (in ms)
	a = new LongAttribute("maxage"
			      , null
			      , Attribute.EDITABLE) ;
	ATTR_MAXAGE = AttributeRegistry.registerAttribute(cls, a) ;
	// Should we send MD5 digest?
	a = new BooleanAttribute("send-md5"
				 , Boolean.FALSE
				 , Attribute.EDITABLE);
	ATTR_MD5 = AttributeRegistry.registerAttribute(cls, a) ;
	// delete allowed for the associated resource ?
	a = new BooleanAttribute("allow-delete"
				 , Boolean.FALSE
				 , Attribute.EDITABLE);
	ATTR_ALLOW_DEL = AttributeRegistry.registerAttribute(cls, a) ;

	//
	// Attribute relative to a FileResource
	//

	// The putable flag:
	a = new BooleanAttribute("putable"
				 , Boolean.FALSE
				 , Attribute.EDITABLE) ;
	ATTR_PUTABLE = AttributeRegistry.registerAttribute(cls, a) ;

	//
	// Attribute relative to a DirectoryResource
	//

	//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 icon directory.
	a = new StringAttribute("icondir"
				, null
				, Attribute.EDITABLE) ;
	ATTR_ICONDIR = AttributeRegistry.registerAttribute(cls,a);
	// The browsable flag:
	a = new BooleanAttribute("browsable"
				 , Boolean.FALSE
				 , Attribute.EDITABLE) ;
	ATTR_BROWSABLE = AttributeRegistry.registerAttribute(cls, a) ;
	// The style sheet attribute:
	a = new StringAttribute("style-sheet-link"
				, null
				, Attribute.EDITABLE) ;
	ATTR_STYLE_LINK = AttributeRegistry.registerAttribute(cls, a) ;
    }

    /**
     * The associated DirectoryResource (if any)
     */
    protected DirectoryResource dresource  = null;

    /**
     * The associated FileResource (if any)
     */
    protected FileResource      fresource  = null;

    /**
     * Register this frame to the given resource.
     * @param resource The resource associated with this frame.
     */
    public void registerResource(FramedResource resource) {
	super.registerResource(resource);
	if (resource instanceof FileResource)
	    fresource = (FileResource) resource;
	else if (resource instanceof DirectoryResource)
	    dresource = (DirectoryResource) resource;
    }

    /**
     * Get the associated FileResource (if any)
     * @return a FileResource instance or <strong>null</strong>
     * if no FileResource is associated with this frame.
     */
    public FileResource getFileResource() {
	return fresource;
    }

    /**
     * Get the associated DirectoryResource (if any)
     * @return a DirectoryResource instance or <strong>null</strong>
     * if no DirectoryResource is associated with this frame.
     */
    public DirectoryResource getDirectoryResource() {
	return dresource;
    }

    /**
     * use this one instead of registerResource if the resource type 
     * doesn't matter or if this is not a file or a directory resource.
     * In subclasses you should have to do that:
     * <pre>
     *  public void registerResource(FramedResource resource) {
     *   super.registerOtherResource(resource);
     *  }
     * </pre>
     * @param the resource to register.
     */
    public void registerOtherResource(FramedResource resource) {
	super.registerResource(resource);
	dresource = null;
	fresource = null;
    }

    // The HTTPResource keeps a cache of ready to use Http values. This 
    // allows to save converting to/from wire rep these objects. Not 
    // much CPU time, but also memory is spared.
    HttpMimeType  contenttype     = null;
    HttpInteger   contentlength   = null;
    HttpDate      lastmodified    = null;
    HttpTokenList contentencoding = null;
    HttpTokenList contentlanguage = null;

    // The Http entity tag for this resource (for FileResource only)
    HttpEntityTag etag   = null;
    // the MD5 digest for this resource (for FileResource only)
    HttpString md5Digest = null;

    /**
     * Get this resource's help url.
     * @return An URL, encoded as a String, or <strong>null</strong> if not
     * available.
     */

    public String getHelpURL() {
	httpd server = (httpd) getServer();
	if ( server == null ) 
	    return null;
	String docurl = server.getDocumentationURL();
	if ( docurl == null )
	    return null;
	return docurl + "/" + getClass().getName() + ".html";
    }

    /**
     * Get the help URL for that resource's attribute.
     * @param topic The topic (can be an attribute name, or a property, etc).
     * @return A String encoded URL, or <strong>null</strong>.
     */

    public String getHelpURL(String topic) {
	httpd server = (httpd) getServer();
	if ( server == null ) 
	    return null;
	String docurl = server.getDocumentationURL();
	if ( docurl == null )
	    return null;
	Class defines = AttributeRegistry.getAttributeClass(getClass(), topic);
	if ( defines != null ) 
	    return docurl + "/" + defines.getName() + ".html";
	return null;
    }

    /** 
     * give the md5 digest from cache or calculate it
     * @return the HttpString version of the digest
     */

    private HttpString getMd5Digest() {
	if (md5Digest != null)
	    return md5Digest;
	// not found, compute it if necessary!
	Resource r = getResource();
	if (r instanceof FileResource) {
	    try {
		Md5 md5 = new Md5 (
		    new FileInputStream(((FileResource)r).getFile()));
		String s = null;
		try {
		    byte b[] = md5.getDigest();
		    Base64Encoder b64;
		    ByteArrayOutputStream bos = new ByteArrayOutputStream();
		    b64 = new Base64Encoder(new ByteArrayInputStream(b), bos);
		    b64.process();
		    s = bos.toString();
		    md5Digest = HttpFactory.makeString(s);
		} catch (Exception mdex) {
		    // error, set it to null
		    md5Digest = null;
		}
		return md5Digest;
	    } catch (FileNotFoundException ex) {
		// silent fail
		md5Digest = null;
	    }
	}
	return null;
    }
    
    /**
     * Listen its resource.
     */
    public void attributeChanged(AttributeChangedEvent evt) {
	super.attributeChanged(evt);
	String name = evt.getAttribute().getName();
	if (name.equals("file-stamp")) {
	    etag = null;
	    lastmodified = null;
	    md5Digest = null;
	} else if (name.equals("file-length")) {
	    setValue(ATTR_CONTENT_LENGTH, evt.getNewValue());
	} else if (name.equals("last-modified")) {
	    setValue(ATTR_LAST_MODIFIED, evt.getNewValue());
	} else {
	    lastmodified = null;
	}
    }

    /**
     * Catch setValue, to maintain cached header values correctness.
     * @param idx The index of the attribute to be set.
     * @param value The new value for the attribute.
     */

    public synchronized void setValue(int idx, Object value) {
	super.setValue(idx, value);
	if (idx == ATTR_CONTENT_TYPE)
	    contenttype = null;
	if (idx == ATTR_CHARSET)
	    contenttype = null;
	if (idx == ATTR_CONTENT_LENGTH)
	    contentlength = null;
	if ( idx == ATTR_CONTENT_ENCODING )
	    contentencoding = null;
	if ( idx == ATTR_CONTENT_LANGUAGE )
	    contentlanguage = null;
	if ( idx == ATTR_PUTABLE) {
	    if (fresource != null) {
		if (value == Boolean.TRUE)
		    allowed = _put_allowed;
		else
		    allowed = _allowed;
	    }
	}
	if (idx == ATTR_MD5) {
	    md5Digest = null; // reset the digest state
	}
	// Any attribute setting modifies the last modified time:
	lastmodified = null;
    }

    /**
     * Get the full URL for that resource.
     * @return An URL instance.
     */
    public URL getURL(Request request) {
	try {
	    return new URL(request.getURL(), resource.getURLPath());
	} catch (MalformedURLException ex) {
	    throw new RuntimeException("unable to build "+
				       getURLPath()+
				       " full URL, from server "+
				       getServer().getURL());
	}
    }

    /**
     * Get this resource quality.
     * @return The resource quality, or some negative value if not defined.
     */

    public double getQuality() {
	return getDouble(ATTR_QUALITY, -1.0) ;
    }

    /**
     * Get this resource title.
     * @return This resource's title, or <strong>null</strong> if not 
     *    defined.
     */

    public String getTitle() {
	return getString(ATTR_TITLE, null) ;
    }

    /**
     * Get this resource content language.
     * Language are stored as a comma separated String of tokens.
     * @return A comma separated string of language tokens, or
     *    <strong>null</strong> if undefined.
     */

    public String getContentLanguage() {
	return (String) getValue(ATTR_CONTENT_LANGUAGE, null) ;
    } 

    /**
     * Get this resource content encoding.
     * The content encoding of a resource is stored as a comma separated
     * list of tokens (as decribed in the Content_encoding header of the
     * HTTP specification, and in the order they should appear in the header).
     * @return A string of comma separated encoding tokens, or
     *    <strong>null</strong> if not defined.
     */

    public String getContentEncoding() {
	String def = (String) attributes[ATTR_CONTENT_ENCODING].getDefault();
	String s =  (String) getString (ATTR_CONTENT_ENCODING, def) ;
	return (String) getString (ATTR_CONTENT_ENCODING, def) ;
    }

    /**
     * Get this resource charset.
     * @return A String, or <strong>null</strong> if not
     *    defined.
     */
    public String getCharset() {
	return (String) getValue(ATTR_CHARSET, null);
    }

    /**
     * Get this resource content type.
     * @return An instance of MIMEType, or <strong>null</strong> if not
     *    defined.
     */
    public MimeType getContentType() {
	return (MimeType) getValue(ATTR_CONTENT_TYPE, null);
    }

    /**
     * Get this resource Etag
     * @return an instance of HttpEntityTag, or <strong>null</strong> if not
     *    defined.
     */

    public HttpEntityTag getETag() {
	if (etag == null) {
	    String etag_s = null;
	    if (fresource != null) {
		long lstamp = fresource.getFileStamp();
		if ( lstamp >= 0L ) {
		    String soid  = Integer.toString(getOid(), 32);
		    String stamp = Long.toString(lstamp, 32);
		    etag_s = Integer.toString(getOid(), 32)+":"
			+ Long.toString(lstamp, 32);
		}
	    }
	    etag = HttpFactory.makeETag(false, etag_s);
	}
	return etag;
    }

    /**
     * Get this resource content length.
     * @return The resource content length, or <strong>-1</strong> if not
     *    defined.
     */

    public int getContentLength() {
	return getInt(ATTR_CONTENT_LENGTH, -1) ;
    }

    /**
     * Get this resource's icon.
     */

    public String getIcon() {
	return getString(ATTR_ICON, null) ;
    }

    /**
     * Get this resource's max age.
     * The max age of a resource indicates how much drift is allowed between
     * the physicall version of the resource, and any in-memory cached version
     * of it.
     * <p>The max age attribute is a long number giving the number of 
     * milliseconds of allowed drift.
     */

    public long getMaxAge() {
	return getLong(ATTR_MAXAGE, (long) -1) ;
    }

    //
    // Relative to FileResource ...
    //

    /**
     * Does this resource support byte ranges.
     */
    protected boolean acceptRanges = false;

    /**
     * Get the PUT'able flag (are we allow to PUT to the resource ?)
     */
    public boolean getPutableFlag() {
	return getBoolean(ATTR_PUTABLE, false) ;
    }

    /**
     * Do we send the MD5 digest?
     */
    public boolean getMD5Flag() {
	return getBoolean(ATTR_MD5, false) ;
    }

    /**
     * delete allowed for the associated resource ?
     */
    public boolean getAllowDeleteFlag() {
	return getBoolean(ATTR_ALLOW_DEL, false);
    }

    /**
     * handles a Range Request
     * @param request, the request
     * @param r, the HttpRange
     * @return a Reply if range is valid, or null if there is a change in the
     * resource, or if the HttpRange is not valid ( 4-2, for example).
     * @exception ProtocolException If processsing the request failed.
     */

    public Reply handleRangeRequest(Request request, HttpRange r) 
	throws ProtocolException
    {
	// Should we check against a IfRange header ?
	HttpEntityTag t = request.getIfRange();

	if ( t != null ) {
	    if (t.isWeak() || ! t.getTag().equals(etag.getTag()))
		return null;
	}
	// Check the range:
	int cl = getContentLength();
	int fb = r.getFirstPosition();
	int lb = r.getLastPosition();
	int sz;

	if (fb > cl-1) { // first byte already out of range
	    HttpContentRange cr = HttpFactory.makeContentRange("bytes", 0,
							       cl - 1, cl);
	    Reply rr;
	    rr = createDefaultReply(request, 
				    HTTP.REQUESTED_RANGE_NOT_SATISFIABLE);
	    rr.setContentLength(-1);
	    rr.setHeaderValue(rr.H_CONTENT_RANGE, cr);
	    if (getMD5Flag()) 
		rr.setContentMD5(null);
	    return rr;
	}

	if ((fb < 0) && (lb >= 0)) { // ex: bytes=-20 final 20 bytes
	    if (lb >= cl)   // cut the end
		lb = cl;
	    sz = lb;
	    fb = cl - lb;
	    lb = cl - 1;
	} else if (lb < 0) {  // ex: bytes=10- the last size - 10
	    lb = cl-1;
	    sz = lb-fb+1;
	} else {              // ex: bytes=10-20
	    if (lb >= cl)  // cut the end
		lb = cl-1;
	    sz = lb-fb+1;
	}
	if ((fb < 0) || (lb < 0) || (fb <= lb)) {
	    HttpContentRange cr = null;
	    fb = (fb < 0) ? 0 : fb;
	    lb = ((lb > cl) || (lb < 0)) ? cl : lb;
	    cr = HttpFactory.makeContentRange("bytes", fb, lb, cl);
	    // Emit reply:
	    Reply rr = createDefaultReply(request, HTTP.PARTIAL_CONTENT);
	    // FIXME check for MD5 of only the subpart
	    try { // create the MD5 for the subpart
		if (getMD5Flag()) {
		    String s = null;
		    try {
			ByteRangeOutputStream br;
			br = new ByteRangeOutputStream(fresource.getFile(),
						       fb, lb+1);
			Md5 md5 = new Md5 (br);
			byte b[] = md5.getDigest();
			Base64Encoder b64;
			ByteArrayOutputStream bs = new ByteArrayOutputStream();
			b64 = new Base64Encoder(new ByteArrayInputStream(b),
						bs);
			b64.process();
			s = bs.toString();
		    } catch (Exception md_ex) {
			// default to null, no action here then
		    }
		    if (s == null)
			rr.setContentMD5(null); 
		    else 
			rr.setContentMD5(s);
		}
      		rr.setContentLength(sz);
		rr.setHeaderValue(rr.H_CONTENT_RANGE, cr);
		rr.setStream(new ByteRangeOutputStream(fresource.getFile(),
						       fb,
						       lb+1));
		return rr;
	    } catch (IOException ex) {
	    }
	} 
	return null;
    }

    //
    // Relative to DirectoryResource ...
    //

    /**
     * Get this class browsable flag.
     */
    public boolean getBrowsableFlag() {
	return getBoolean (ATTR_BROWSABLE, false) ;
    }

    /**
     * Get this frame style sheet link
     */
    public String getStyleSheetURL() {
	return getString (ATTR_STYLE_LINK, null);
    }

    /**
     * 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 ;
    }

    /**
     * Get the optional icon directory.
     */
    public String getIconDirectory() { 
	return getString(ATTR_ICONDIR, "/icons") ;
    }



    /**
     * 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) ;
    }

    /**
     * Add our own Style Sheet to the HtmlGenerator.
     * @param g The HtmlGenerator.
     */
    public void addStyleSheet(HtmlGenerator g) {
	// Add style link
	String css_url = getStyleSheetURL();
	if (css_url != null) {
	    g.addLink( new HtmlLink("STYLESHEET", css_url));
	}
    }

    /**
     * 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 ProtocolException If processsing the request failed.
     * @exception ResourceException If the resource got a fatal error.
     */

    public synchronized Reply getDirectoryListing(Request request)
	throws ProtocolException, ResourceException
    {
	if (dresource == null) 
	    throw new ResourceException("this frame is not attached to a "+
					"DirectoryResource. ("+
					resource.getIdentifier()+")");
	// delete us if the directory was deleted
	if (! dresource.getDirectory().exists()) {
	    //delete us and emit an error
	    String msg = dresource.getIdentifier()+
		": deleted, removing the DirectoryResource";
	    getServer().errlog(dresource, msg);
	    try {
		dresource.delete();
	    } catch (MultipleLockException ex) {
	    }
	    // Emit an error back:
	    Reply error = request.makeReply(HTTP.NOT_FOUND) ;
	    error.setContent ("<h1>Document not found</h1>"+
			      "<p>The document "+
			      request.getURL()+
			      " is indexed but not available."+
			      "<p>The server is misconfigured.") ;
	    throw new HTTPException (error) ;
	}
	// Have we already an up-to-date computed a listing ?
	if ((listing == null) 
	    || (dresource.getDirectory().lastModified() > listing_stamp)
	    || (dresource.getLastModified() > listing_stamp)
	    || (getLastModified() > listing_stamp)) {
      
	    Class http_class = null;
	    try {
		http_class = Class.forName("org.w3c.jigsaw.frames.HTTPFrame");
	    } catch (ClassNotFoundException ex) {
		http_class = null;
	    }

	    Enumeration   enum      = 
		dresource.enumerateResourceIdentifiers() ;
	    Vector        resources = Sorter.sortStringEnumeration(enum) ;
	    HtmlGenerator g         = 
		new HtmlGenerator("Directory of "+
				  dresource.getIdentifier());
	    // Add style link
	    addStyleSheet(g);
	    g.append("<h1>"+dresource.getIdentifier()+"</h1>");
	    // Link to the parent, when possible:
	    if ( dresource.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;
		long              size = -1;
		rr = dresource.lookup(name);
		FramedResource resource = null;
		if (rr != null) {
		    try {
			resource = (FramedResource) rr.lock();
			// remove manually deleted FileResources
			if( resource instanceof FileResource ) {
			    FileResource fr = (FileResource)resource;
			    if( !fr.getFile().exists() ) {
				try {
				    fr.delete();
				} catch (MultipleLockException ex) {};
				continue;
			    } else {
				size = fr.getFile().length();
			    }
			}
			HTTPFrame itsframe = null;
			if (http_class != null)
			    itsframe = 
				(HTTPFrame) resource.getFrame(http_class);
			if (itsframe != null) {
			    // Icon first, if available
			    String icon = itsframe.getIcon() ;
			    if ( icon != null ) 
				g.append("<img src=\""+
					 getIconDirectory() +"/" + icon+
					 "\">");
			    // Resource's name with link:
			    if (resource instanceof ContainerInterface)
				g.append("<a href=\"" 
					 , URLEncoder.encode(name)
					 , "/\">"+name+"</a>");
			    else
				g.append("<a href=\"" 
					 , URLEncoder.encode(name)
					 , "\">"+name+"</a>");
			    // resource's title, if any:
			    String title = itsframe.getTitle();
			    if ( title != null )
				g.append(" "+title);
			    //size (if any)
			    if (size != -1) {
				String s = null;
				if (size > 1023) {
				    s = " ["+(size/1024)+" Kb]";
				} else {
				    s = " ["+size+" bytes]";
				}
				g.append(s);
			    }
			    g.append("<br>\n");
			} else {
			    // Resource's name with link:
			    g.append(name+" (<i>Not available via HTTP.</i>)");
			    g.append("<br>\n");
			}
		    } catch (InvalidResourceException ex) {
			g.append(name+
				 " cannot be loaded (server misconfigured)");
			g.append("<br>\n");
			continue;
		    } finally { 
			rr.unlock();
		    }
		}
	    }
	    g.close() ;
	    listing_stamp = getLastModified() ;
	    listing       = g ;
	} else if ( checkIfModifiedSince(request) == COND_FAILED ) {
	    // Is it an IMS request ?
	    Reply reply = createDefaultReply(request, HTTP.NOT_MODIFIED) ;
	    reply.setContentMD5(null);
	    return reply;
	}
	// New content or need update:
	Reply reply = createDefaultReply(request, HTTP.OK) ;
	reply.setLastModified(listing_stamp) ;
	reply.setStream(listing) ;
	// check MD5
	return reply ;
    }

    //
    // Commom part.
    //



    /**
     * Update the cached headers value.
     * Each resource maintains a set of cached values for headers, this
     * allows for a nice sped-up in headers marshalling, which - as the 
     * complexity of the protocol increases - becomes a bottleneck.
     */

    protected void updateCachedHeaders() {
	// Precompute a set of header values to keep by:
	if ( contenttype == null ) {
	    String charset = getCharset();
	    if (charset == null)
		contenttype = HttpFactory.makeMimeType(getContentType());
	    else {
		MimeType ctype = getContentType().getClone();
		ctype.addParameter("charset", charset);
		contenttype = HttpFactory.makeMimeType(ctype);
	    }
	}
	if (contentlength == null) {
	    int cl = -1;
	    if (fresource != null) 
		cl = fresource.getFileLength();
	    if ( cl >= 0 ) {
		setValue(ATTR_CONTENT_LENGTH, new Integer(cl));
		contentlength = HttpFactory.makeInteger(cl);
	    }
	}
	if ( lastmodified == null ) {
	    long lm = getLastModified();
	    if ( lm > 0 )
		lastmodified = HttpFactory.makeDate(getLastModified());
	}
	if (definesAttribute(ATTR_CONTENT_ENCODING) &&(contentencoding==null))
	    contentencoding = HttpFactory.makeStringList(getContentEncoding());
	if (definesAttribute(ATTR_CONTENT_LANGUAGE) &&(contentlanguage==null))
	    contentlanguage = HttpFactory.makeStringList(getContentLanguage());

	if (fresource != null) {
	    // We only take care of etag here:
	    if ( etag == null ) {
		getETag();
	    }
	    if (getMD5Flag() && (md5Digest == null)) {
		getMd5Digest();
	    }
	}
    }

    /**
     * Create a reply to answer to request on this file.
     * This method will create a suitable reply (matching the given request)
     * and will set all its default header values to the appropriate 
     * values.
     * @param request The request to make a reply for.
     * @return An instance of Reply, suited to answer this request.
     */

    public Reply createDefaultReply(Request request, int status) {
	Reply reply = request.makeReply(status);
	updateCachedHeaders();
	if ( status != HTTP.NOT_MODIFIED ) {
	    if ( contentlength != null )
		reply.setHeaderValue(Reply.H_CONTENT_LENGTH, contentlength);
	    if ( contenttype != null )
		reply.setHeaderValue(Reply.H_CONTENT_TYPE, contenttype);
	    if ( lastmodified != null )
		reply.setHeaderValue(Reply.H_LAST_MODIFIED, lastmodified);
	    if ( contentencoding != null ) {
		reply.setHeaderValue(Reply.H_CONTENT_ENCODING,contentencoding);
	    }
	    if ( contentlanguage != null )
		reply.setHeaderValue(Reply.H_CONTENT_LANGUAGE,contentlanguage);
      
	}
	long maxage = getMaxAge();
	if ( maxage >= 0 ) {
	    if (reply.getMajorVersion() >= 1 ) {
		if (reply.getMinorVersion() >= 1) {
		    reply.setMaxAge((int) (maxage / 1000));
		} 
		// If max-age is zero, say what you mean:
		long expires = (System.currentTimeMillis()
				+ ((maxage == 0) ? -1000 : maxage));
		reply.setExpires(expires);
	    }
	}
	// Set the date of the reply (round it to secs):
	reply.setDate((System.currentTimeMillis() / 1000L) * 1000L);
    
	if (fresource != null) {
	    // Set the entity tag:
	    if ( getETag() != null )
		reply.setETag(etag);
	    if ( acceptRanges )
		reply.setHeaderValue(reply.H_ACCEPT_RANGES, _accept_ranges);
	    if ( getMD5Flag()) {
		reply.setHeaderValue(reply.H_CONTENT_MD5, getMd5Digest());
	    }
	}

	return reply;
    }

    /**
     * Check the <code>If-Match</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 checkIfMatch(Request request) {
	if (fresource != null) {
	    HttpEntityTag tags[] = request.getIfMatch();
	    if ( tags != null ) {
		// Good, real validators in use:
		if ( etag != null ) {
		    // Note: if etag is null this means that the resource has 
		    // changed and has not been even emited since then...
		    for (int i = 0 ; i < tags.length ; i++) {
			HttpEntityTag t = tags[i];
			if ((!t.isWeak()) && t.getTag().equals(etag.getTag()))
			    return COND_OK;
		    }
		}
		return COND_FAILED;
	    }
	}
	return 0;
    }

    /**
     * Check the <code>If-None-Match</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 checkIfNoneMatch(Request request) {
	if (fresource != null) {
	    // Check for an If-None-Match conditional:
	    HttpEntityTag tags[] = request.getIfNoneMatch();
	    if ( tags != null ) {
		if ( etag == null )
		    return COND_OK;
		for (int i = 0 ; i < tags.length ; i++) {
		    HttpEntityTag t = tags[i];
		    if (( ! t.isWeak()) && t.getTag().equals(etag.getTag()))
			return COND_FAILED;
		}
		return COND_OK;
	    }
	}
	return 0;
    }

    /**
     * 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) {
	// Check for an If-Modified-Since conditional:
	long ims = request.getIfModifiedSince();
	if (dresource != null) {
	    if (ims >= 0) 
		return (((listing_stamp > 0) && (listing_stamp - 1000 <= ims))
			? COND_FAILED
			: COND_OK);
	} else if (fresource != null) {
	    long cmt = getLastModified();
	    if ( ims >= 0 )
		return ((cmt > 0) && (cmt - 1000 <= ims)) 
		    ? COND_FAILED : COND_OK;
	}
	return 0;
    }

    /**
     * Check the <code>If-Unmodified-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 checkIfUnmodifiedSince(Request request) {
	if (fresource != null) {
	    // Check for an If-Unmodified-Since conditional:
	    long iums = request.getIfUnmodifiedSince();
	    long cmt = getLastModified();
	    if ( iums >= 0 ) 
		return ((cmt > 0) && (cmt - 1000) >= iums) 
		    ? COND_FAILED : COND_OK;
	}
	return 0;
    }

     /**
     * Lookup the target resource. Lookup filters and then resource.
     * @param ls The current lookup state
     * @param lr The result
     * @return true if lookup is done.
     * @exception ProtocolException If an error relative to the protocol occurs
     * @see org.w3c.tools.resources.ResourceFrame#lookupFilters
     * @see #lookupResource
     */
    public boolean lookup(LookupState ls, LookupResult lr) 
	throws ProtocolException
    {
	RequestInterface req = ls.getRequest(); 
	if (! checkRequest(req)) 
	    return false;
	if (lookupFilters(ls,lr))
	    return true;
	return lookupResource(ls,lr);
    }

     /**
     * Lookup the target resource (dispath to more specific lookup methods).
     * @param ls The current lookup state
     * @param lr The result
     * @return true if lookup is done.
     * @exception ProtocolException If an error relative to the protocol occurs
     * @see #lookupDirectory
     * @see #lookupFile
     * @see #lookupOther
     */
    protected boolean lookupResource(LookupState ls, LookupResult lr) 
	throws ProtocolException
    {
	if (fresource != null) {
	    return lookupFile(ls,lr);
	} else if (dresource != null) {
	    return lookupDirectory(ls,lr);
	} else {
	    return lookupOther(ls,lr);
	}
    }

    /**
     * Lookup the target resource when associated with a DirectoryResource.
     * @param ls The current lookup state
     * @param lr The result
     * @return true if lookup is done.
     * @exception ProtocolException If an error relative to the protocol
     * occurs
     */
    protected boolean lookupDirectory(LookupState ls, LookupResult lr) 
	throws ProtocolException
    {
	// Give a chance to our super-class to run its own lookup scheme:
	// do we have to create a resource (PUT) ?
	if ((ls.hasMoreComponents()) && getPutableFlag()) {
	    Request request = (Request) ls.getRequest() ;
	    if ((request == null) || request.getMethod().equals("PUT")) {
		// We might well want to create a resource:
		String            name = ls.peekNextComponent() ;
		ResourceReference rr   = dresource.lookup(name);
		if ((rr == null) && (dresource.getExtensibleFlag())) {
		    if (ls.countRemainingComponents() == 1)
			rr = dresource.createResource(name, request);
		    else
			rr = dresource.createDirectoryResource(name);
		    if (rr == null) {
			Reply error = 
			    request.makeReply(HTTP.UNSUPPORTED_MEDIA_TYPE);
			error.setContent(
			    "Failed to create resource "+
			    name +" : "+
			    "Unable to create the appropriate file:"+
			    request.getURLPath()+
			    " this media type is not supported");
			throw new HTTPException (error);
		    }
		} else if (rr == null) {
		    Reply error = request.makeReply(HTTP.FORBIDDEN) ;
		    error.setContent("You are not allowed to create resource "+
				     name +" : "+
				     dresource.getIdentifier()+
				     " is not extensible.");
		    throw new HTTPException (error);
		}
	    }
	}
	if ( super.lookup(ls, lr) ) {
	    if ( ! ls.isDirectory() && ! ls.isInternal() ) {
		// The directory lookup URL doesn't end with a slash:
		Request request = (Request)ls.getRequest() ;
		if ( request == null ) {
		    lr.setTarget(null);
		    return true;
		}
		URL url = null;
		try {
		    url = (ls.hasRequest() 
			   ? getURL(request)
			   : new URL(getServer().getURL(), 
				     resource.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.FOUND);
		    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() ) {
		Request request = (Request)ls.getRequest() ;
		request.setState(STATE_CONTENT_LOCATION, "true");
		// return the index file.
		String index = getIndex();
		if ( index != null && index.length() > 0) {
		    DirectoryResource dir = (DirectoryResource) resource;
		    ResourceReference rr = dir.lookup(index);
		    if (rr != null) {
			try {
			    FramedResource rindex = (FramedResource) rr.lock();
			    return rindex.lookup(ls,lr);
			} catch (InvalidResourceException ex) {
			} finally {
			    rr.unlock();
			}
		    }
		}
	    }
	    return true;
	}
	return false;
    }

    /**
     * Lookup the target resource when associated with a FileResource.
     * @param ls The current lookup state
     * @param lr The result
     * @return true if lookup is done.
     * @exception ProtocolException If an error relative to the protocol occurs
     */
    protected boolean lookupFile(LookupState ls, LookupResult lr) 
	throws ProtocolException
    {
	return super.lookup(ls,lr);
    }

    /**
     * Lookup the target resource when associated with an unknown resource.
     * @param ls The current lookup state
     * @param lr The result
     * @return true if lookup is done.
     * @exception ProtocolException If an error relative to the protocol occurs
     */
    protected boolean lookupOther(LookupState ls, LookupResult lr) 
	throws ProtocolException
    {
	return super.lookup(ls,lr);
    }

    /**
     * Check the request.
     * @param request the incomming request.
     * @return true if the request is an HTTP Request. 
     */
    public boolean checkRequest(RequestInterface request) {
	return ((request == null) 
		? true 
		:(request instanceof org.w3c.jigsaw.http.Request));
    }

    /**
     * Perform the request on all the frames of that resource. The
     * Reply returned is the first non-null reply.
     * @param request A RequestInterface instance.
     * @return A ReplyInterface instance.
     * @exception ProtocolException If an error relative to the protocol occurs
     * @exception ResourceException If an error not relative to the 
     * protocol occurs
     */
    protected ReplyInterface performFrames(RequestInterface request) 
	throws ProtocolException, ResourceException
    {
	return super.performFrames(request);
    }

    /**
     * Perform the request
     * @param req The request to handle.
     * @exception ProtocolException If processsing the request failed.
     * @exception ResourceException If the resource got a fatal error.
     */

    public ReplyInterface perform(RequestInterface req) 
	throws ProtocolException, ResourceException
    {
	ReplyInterface repi = super.perform(req);
	if (repi != null)
	    return repi;

	if (! checkRequest(req))
	    return null;

	Reply  reply  = null;
	Request request = (Request) req;
	String method = request.getMethod () ;
	// Perform the request:
	if ( method.equals("GET") ) {
	    reply = get(request) ;
	} else if ( method.equals("HEAD") ) {
	    reply = head(request) ;
	} else if ( method.equals("POST") ) {
	    reply = post(request) ;
	} else if ( method.equals("PUT") ) {
	    reply = put(request) ;
	} else if ( method.equals("OPTIONS") ) {
	    reply = options(request);
	} else if ( method.equals("DELETE") ) {
	    reply = delete(request) ;
	} else if ( method.equals("LINK") ) {
	    reply = link(request) ;
	} else if ( method.equals("UNLINK") ) {
	    reply = unlink(request) ;
	} else if ( method.equals("TRACE") ) {
	    reply = trace(request) ;
	} else {
	    reply = extended(request) ;
	}
	return reply;
    }

    /**
     * The default GET method.
     * @param request The request to handle.
     * @exception ProtocolException If processsing the request failed.
     * @exception ResourceException If the resource got a fatal error.
     */

    public Reply get(Request request)
	throws ProtocolException, ResourceException
    {
	if (dresource != null) {
	    // we manage a DirectoryResource
	    return getDirectoryResource(request) ;
	} else if (fresource != null) {
	    // we manage a FileResource
	    return getFileResource(request);
	} else {
	    return getOtherResource(request);
	}
    }

    /**
     * The default GET method for other king of associated resource
     * @param request The request to handle.
     * @exception ProtocolException If processsing the request failed.
     * @exception ResourceException If the resource got a fatal error.
     */
    protected Reply getOtherResource(Request request) 
	throws ProtocolException, ResourceException
    {
	// we don't manage this kind of resource
	Reply error = request.makeReply(HTTP.NOT_IMPLEMENTED) ;
	error.setContent("Method GET not implemented.") ;
	throw new HTTPException (error) ;
    }

    /**
     * Create the reply relative to the given file.
     * @param request the incomming request.
     * @return A Reply instance
     * @exception ProtocolException If processsing the request failed.
     * @exception ResourceException If the resource got a fatal error.
     */
    protected Reply createFileReply(Request request) 
	throws ProtocolException, ResourceException
    {
	File file = fresource.getFile() ;
	Reply reply = null;
	// Check for a range request:
	HttpRange ranges[] = request.getRange();
	if ((ranges != null) && (ranges.length == 1)) {
	    Reply rangereply = handleRangeRequest(request, ranges[0]);
	    if ( rangereply != null )
		return rangereply;
	}
	// Default to full reply:
	reply = createDefaultReply(request, HTTP.OK) ;
	try { 
	    reply.setStream(new FileInputStream(file));
	} catch (IOException ex) {
	    Reply error = request.makeReply(HTTP.SERVICE_UNAVAILABLE);
	    error.setContent(ex.getMessage());
	    return error;
	}
	return reply ;
    }

    /**
     * Alway throws an HTTPException
     */
    protected Reply deleteMe(Request request) 
	throws HTTPException
    {
	// Delete the resource if parent is extensible:
	boolean extensible = false;
	ResourceReference rr = fresource.getParent();
	ResourceReference rrtemp = null;
	Resource p = null;
	while ( true ) {
	    try {
		if (rr == null)
		    break;
		p = rr.lock();
		if (p instanceof DirectoryResource) {
		    extensible = 
			((DirectoryResource)p).getExtensibleFlag();
		    break;
		}
		rrtemp = p.getParent();
	    } catch (InvalidResourceException ex) {
		break;
	    } finally {
		if (rr != null)
		    rr.unlock();
	    }
	    rr = rrtemp;
	}
	if (extensible) {
	    // The resource is indexed but has no file, emit an error
	    String msg = fresource.getFile()+
		": deleted, removing the FileResource.";
	    getServer().errlog(fresource, msg);
	    try {
		fresource.delete();
	    } catch (MultipleLockException ex) {
		Reply error = request.makeReply(HTTP.NOT_FOUND) ;
		error.setContentMD5(null); // FIXME must compute it!
		error.setContent ("<h1>Document not found</h1>"+
				  "<p>The document "+
				  request.getURL()+
				  " is indexed but not available."+
				  "<p>"+ex.getMessage()+
				  "<p>The server is misconfigured.") ;
		throw new HTTPException (error) ;
	    }
	}
	// Emit an error back:
	Reply error = request.makeReply(HTTP.NOT_FOUND) ;
	error.setContentMD5(null);
	error.setContent ("<h1>Document not found</h1>"+
			  "<p>The document "+
			  request.getURL()+
			  " is indexed but not available."+
			  "<p>The server is misconfigured.") ;
	return error;
    }

    /**
     * Get for FileResource
     * @param request the incomming request.
     * @return A Reply instance
     * @exception ProtocolException If processsing the request failed.
     * @exception ResourceException If the resource got a fatal error.
     */
    protected Reply getFileResource(Request request) 
	throws ProtocolException, ResourceException
    {
	if (fresource == null) 
	    throw new ResourceException("this frame is not attached to a "+
					"FileResource. ("+
					resource.getIdentifier()+")");
	Reply reply = null;
	File file = fresource.getFile() ;
	fresource.checkContent();
	updateCachedHeaders();
	// Check validators:
	if ( checkIfMatch(request) == COND_FAILED ) {
	    reply = request.makeReply(HTTP.PRECONDITION_FAILED);
	    reply.setContent("Pre-conditions failed.");
	    reply.setContentMD5(null);
	    return reply;
	}
	if ( checkIfNoneMatch(request) == COND_FAILED ) {
	    reply = createDefaultReply(request, HTTP.NOT_MODIFIED);
	    reply.setContentMD5(null);
	    return reply;
	}
	if ( checkIfModifiedSince(request) == COND_FAILED ) {
	    reply = createDefaultReply(request, HTTP.NOT_MODIFIED);
	    reply.setContentMD5(null);
	    return reply;
	}
	if ( checkIfUnmodifiedSince(request) == COND_FAILED ) {
	    reply = request.makeReply(HTTP.PRECONDITION_FAILED);
	    reply.setContent("Pre-conditions failed.");
	    reply.setContentMD5(null);
	    return reply;
	}
	// Does this file really exists, if so send it back
	if ( file.exists() ) {
	    reply = createFileReply(request);
	    if (request.hasState(STATE_CONTENT_LOCATION))
		reply.setContentLocation(getURL(request).toExternalForm());
	    return reply;
	} else {
	    return deleteMe(request);
	}
    }

    /**
     * Perform a GET for the associated DirectoryResource.
     * @param request the incomming request.
     * @return A Reply instance.
     * @exception ProtocolException if request processing failed.
     * @exception ResourceException If the resource got a fatal error.
     */
    protected Reply getDirectoryResource(Request request) 
	throws ProtocolException, ResourceException
    {
	String index = getIndex();
	if ( index != null && index.length() > 0 ) {
	    if (index.equals("*forbid*")) {
		Reply rep = request.makeReply(HTTP.FORBIDDEN);
		rep.setContent("<h1>Forbidden</h1>"+
			       "The directory resource "+ request.getURL() +
			       " cannot be browsed");
		return rep;
	   } 
	}
	return getDirectoryListing(request) ;
    }
    /**
     * The default HEAD method replies does a GET and removes entity.
     * @param request The request to handle.
     * @exception ProtocolException Always thrown, to return a NOT_IMPLEMENTED
     * error.
     * @exception ResourceException If the resource got a fatal error.
     */

    public Reply head(Request request)
	throws ProtocolException, ResourceException
    {
	if (dresource != null) {
	    return headDirectoryResource(request);
	} else if (fresource != null) {
	    return headFileResource(request);
	} else {
	    return headOtherResource(request);
	}
    }

    /**
     * Perform a HEAD request for the associated resource.
     * @param request the incomming request.
     * @return A Reply instance
     * @exception ProtocolException If processsing the request failed.
     * @exception ResourceException If the resource got a fatal error.
     */
    protected Reply headOtherResource(Request request) 
	throws ProtocolException, ResourceException
    {
	Reply reply = null;
	reply = getOtherResource(request) ;
	reply.setStream((InputStream) null);
	return reply;
    }

    /**
     * Perform a HEAD request for the associated DirectoryResource.
     * @param request the incomming request.
     * @return A Reply instance
     * @exception ProtocolException If processsing the request failed.
     * @exception ResourceException If the resource got a fatal error.
     */
    protected Reply headDirectoryResource(Request request) 
	throws ProtocolException, ResourceException
    {
	Reply reply = null;
	reply = getDirectoryResource(request) ;
	reply.setStream((InputStream) null);
	return reply;
    }

    /**
     * Perform a HEAD request for the associated FileResource.
     * @param request the incomming request.
     * @return A Reply instance
     * @exception ProtocolException If processsing the request failed.
     * @exception ResourceException If the resource got a fatal error.
     */
    protected Reply headFileResource(Request request) 
	throws ProtocolException, ResourceException
    {
	if (fresource == null) 
	    throw new ResourceException("this frame is not attached to a "+
					    "FileResource. ("+
					    resource.getIdentifier()+")");
	Reply reply = null;
	fresource.checkContent();
	updateCachedHeaders();
	// Conditional check:
	if ( checkIfMatch(request) == COND_FAILED ) {
	    Reply r = request.makeReply(HTTP.PRECONDITION_FAILED);
	    r.setContent("Pre-conditions failed.");
	    return r;
	}
	if ( checkIfNoneMatch(request) == COND_FAILED )
	    return createDefaultReply(request, HTTP.NOT_MODIFIED);
	if ( checkIfModifiedSince(request) == COND_FAILED )
	    return createDefaultReply(request, HTTP.NOT_MODIFIED);
	if ( checkIfUnmodifiedSince(request) == COND_FAILED ) {
	    Reply r = request.makeReply(HTTP.PRECONDITION_FAILED);
	    r.setContent("Pre-conditions failed.");
	    return r;
	}
	if (! fresource.getFile().exists()) {
	    return deleteMe(request);
	}
	reply = createDefaultReply(request, HTTP.OK);
	if (request.hasState(STATE_CONTENT_LOCATION))
	    reply.setContentLocation(getURL(request).toExternalForm());
	return reply;
	
    }

    /**
     * The default POST method replies with a not implemented.
     * @param request The request to handle.
     * @exception ProtocolException Always thrown, to return a NOT_IMPLEMENTED
     *    error.
     * @exception ResourceException If the resource got a fatal error.
     */

    public Reply post(Request request)
	throws ProtocolException, ResourceException
    {
	Reply error = request.makeReply(HTTP.NOT_ALLOWED) ;
	if ( allowed != null )
	    error.setHeaderValue(Reply.H_ALLOW, allowed);
	error.setContent("Method POST not allowed on this resource.") ;
	throw new HTTPException (error) ;
    }

    /**
     * The default PUT method.
     * @param request The request to handle.
     * @exception ProtocolException If processsing the request failed.
     * @exception ResourceException If the resource got a fatal error.
     */

    public Reply put(Request request)
	throws ProtocolException, ResourceException
    {
	if (fresource != null) {
	    return putFileResource(request);
	} else {
	    return putOtherResource(request);
	}
    }

    /**
     * Always throw a ProtocolException.
     * @param request The incmming request.
     * @return a Reply instance.
     * @exception ProtocolException (Always thrown).
     */
    protected Reply putOtherResource(Request request) 
	throws ProtocolException
    {
	Reply error = request.makeReply(HTTP.NOT_IMPLEMENTED) ;
	error.setContent("Method PUT not implemented.") ;
	throw new HTTPException (error) ;
    }

    /**
     * Change the content of the associated FileResource.
     * @param request The incomming request.
     * @exception org.w3c.tools.resources.ProtocolException if a protocol 
     * error occurs
     * @exception ResourceException If the resource got a fatal error.
     */
    protected Reply putFileResource(Request request)
	throws ProtocolException, ResourceException
    {
	Reply reply = null;
	int status = HTTP.OK;
	fresource.checkContent();
	updateCachedHeaders();
	// Is this resource writable ?
	if ( ! getPutableFlag() ) {
	    Reply error = request.makeReply(HTTP.NOT_ALLOWED) ;
	    error.setContent("Method PUT not allowed.") ;
	    throw new HTTPException (error) ;
	}
	// Check validators:
	if ((checkIfMatch(request) == COND_FAILED)
	    || (checkIfNoneMatch(request) == COND_FAILED)
	    || (checkIfModifiedSince(request) == COND_FAILED)
	    || (checkIfUnmodifiedSince(request) == COND_FAILED)) {
	    Reply r = request.makeReply(HTTP.PRECONDITION_FAILED);
	    r.setContent("Pre-condition failed.");
	    return r;
	}
	// Check the request:
	InputStream in = null;
	try {
	    in = request.getInputStream();
	    if ( in == null ) {
		Reply error = request.makeReply(HTTP.BAD_REQUEST) ;
		error.setContent ("<p>Request doesn't have a valid content.");
		throw new HTTPException (error) ;
	    }
	} catch (IOException ex) {
	    throw new ClientException(request.getClient(), ex);
	}
	// We do not support (for the time being) put with ranges:
	if ( request.hasContentRange() ) {
	    Reply error = request.makeReply(HTTP.BAD_REQUEST);
	    error.setContent("partial PUT not supported.");
	    throw new HTTPException(error);
	}
	// Check that if some type is provided it doesn't conflict:
	if ( request.hasContentType() ) {
	    MimeType rtype = request.getContentType() ;
	    MimeType type  = getContentType() ;
	    if ( type == null ) {
		setValue (ATTR_CONTENT_TYPE, rtype) ;
	    } else if ( rtype.match (type) < 0 ) {
		if (debug) {
		    System.out.println("No match between: ["+
				       rtype.toString()+"] and ["+
				       type.toString()+"]");
		}
		Reply error = request.makeReply(HTTP.UNSUPPORTED_MEDIA_TYPE) ;
		error.setContent ("<p>Invalid content type: "+type.toString());
		throw new HTTPException (error) ;
	    }
	}
	// Write the body back to the file:
	try {
	    // We are about to accept the put, notify client before continuing
	    Client client = request.getClient();
	    if ( client != null  && request.getExpect() != null ) {
		client.sendContinue();
	    }
	    if ( fresource.newContent(request.getInputStream()) )
		status = HTTP.CREATED;
	    else
		status = HTTP.NO_CONTENT;
	} catch (IOException ex) {
	    throw new ClientException(request.getClient(), ex);
	}
	if ( status == HTTP.CREATED ) {
	    reply = request.makeReply(status);
	    reply.setContent("<P>Resource succesfully created");
	    if (request.hasState(STATE_CONTENT_LOCATION))
		reply.setContentLocation(getURL(request).toExternalForm());
            // Henrik's fix, create the Etag on 201
	    if (fresource != null) {
		// We only take car eof etag here:
		if ( etag == null ) {
		    reply.setETag(getETag());
		}
	    }
	    reply.setLocation(getURL(request));
	    reply.setContent ("<p>Entity body saved succesfully !") ;
	} else {
	    reply = createDefaultReply(request, status);
	}
	return reply ;
    }

    /**
     * The default OPTIONS method replies with a not implemented.
     * @param request The request to handle.
     * @exception ProtocolException In case of errors.
     * @exception ResourceException If the resource got a fatal error.
     */

    public Reply options(Request request)
	throws ProtocolException, ResourceException
    {
	Reply reply = createDefaultReply(request, HTTP.OK);
	// Removed unused headers:
	reply.setContentLength(-1);
	reply.setContentType(null);
	// Add the allow header:
	if ( allowed != null )
	    reply.setHeaderValue(Reply.H_ALLOW, allowed);
	return reply;
    }

    /**
     * The default DELETE method, actually the resource (file, directory)
     * is moved into the trash directory which is not accessible via HTTP.
     * @param request The request to handle.
     * @exception ProtocolException If processsing the request failed.
     * @exception ResourceException If the resource got a fatal error.
     */

    public Reply delete(Request request)
	throws ProtocolException, ResourceException
    {
	if (getAllowDeleteFlag()) {
	    if (dresource != null) {
		// we manage a DirectoryResource
		return deleteDirectoryResource(request) ;
	    } else if (fresource != null) {
		// we manage a FileResource
		return deleteFileResource(request);
	    } else {
		return deleteOtherResource(request);
	    }
	} else {
	    Reply error = request.makeReply(HTTP.NOT_ALLOWED) ;
	    error.setContent("Method DELETE not allowed.") ;
	    throw new HTTPException (error) ;
	}
    }

    protected File computeTrashFile(File file) {
	String trashdir = getServer().getTrashDirectory();
	String filename = file.getName();
	File trashfile = new File(trashdir, filename);
	int nb = 1;
	while(trashfile.exists()) {
	    trashfile = new File(trashdir, filename+"."+nb);
	    nb++;
	}
	return trashfile;
    }

    protected File computeTrashDir(File dir) {
	return computeTrashFile(dir);
    }

    /**
     * Perform a DELETE for the associated DirectoryResource.
     * @param request the incomming request.
     * @return A Reply instance.
     * @exception ProtocolException if request processing failed.
     * @exception ResourceException If the resource got a fatal error.
     */
    protected Reply deleteDirectoryResource(Request request) 
	throws ProtocolException, ResourceException
    {
	File olddir = dresource.getDirectory();
	try {
	    //delete the FileResource
	    dresource.delete();
	} catch (MultipleLockException ex) {
	    Reply error = request.makeReply(HTTP.FORBIDDEN);
	    error.setContent("Can't delete resource: "+
			     resource.getIdentifier()+
			     " is locked. Try again later.");
	    throw new HTTPException(error);
	}
	File newdir = computeTrashDir(olddir);
	olddir.renameTo(newdir);
	return request.makeReply(HTTP.NO_CONTENT);
    }

    /**
     * Perform a DELETE for the associated FileResource.
     * @param request the incomming request.
     * @return A Reply instance.
     * @exception ProtocolException if request processing failed.
     * @exception ResourceException If the resource got a fatal error.
     */
    protected Reply deleteFileResource(Request request) 
	throws ProtocolException, ResourceException
    {
	File oldfile = fresource.getFile();
	try {
	    //delete the FileResource
	    fresource.delete();
	} catch (MultipleLockException ex) {
	    Reply error = request.makeReply(HTTP.FORBIDDEN);
	    error.setContent("Can't delete resource: "+
			     resource.getIdentifier()+
			     " is locked. Try again later.");
	    throw new HTTPException(error);
	}
	File newfile = computeTrashFile(oldfile);
	oldfile.renameTo(newfile);
	return request.makeReply(HTTP.NO_CONTENT);
    }

    /**
     * Perform a DELETE for the associated resource.
     * @param request the incomming request.
     * @return A Reply instance.
     * @exception ProtocolException if request processing failed.
     * @exception ResourceException If the resource got a fatal error.
     */
    protected Reply deleteOtherResource(Request request) 
	throws ProtocolException, ResourceException
    {
	// we don't manage this kind of resource
	Reply error = request.makeReply(HTTP.NOT_IMPLEMENTED) ;
	error.setContent("Method DELETE not implemented.") ;
	throw new HTTPException (error) ;
    }

    /**
     * The default LINK method replies with a not implemented.
     * @param request The request to handle.
     * @exception ProtocolException Always thrown, to return a NOT_IMPLEMENTED
     * error.
     * @exception ResourceException If the resource got a fatal error.
     */

    public Reply link(Request request) 
	throws ProtocolException, ResourceException
    {
	Reply error = request.makeReply(HTTP.NOT_IMPLEMENTED) ;
	error.setContent("Method LINK not implemented.") ;
	throw new HTTPException (error) ;
    }

    /**
     * The default UNLINK method replies with a not implemented.
     * @param request The request to handle.
     * @exception ProtocolException Always thrown, to return a NOT_IMPLEMENTED
     *    error.
     * @exception ResourceException If the resource got a fatal error.
     */

    public Reply unlink(Request request)
	throws ProtocolException, ResourceException
    {
	Reply error = request.makeReply(HTTP.NOT_IMPLEMENTED) ;
	error.setContent("Method UNLINK not implemented.") ;
	throw new HTTPException (error) ;
    }

    /**
     * The default TRACE method replies with a not implemented
     * @param request The request to handle.
     * @exception HTTPException In case of errors.
     * @exception ClientException If the client instance controling the
     * request processing got a fatal error.
     */

    public Reply trace(Request request)
        throws HTTPException, ClientException
    {
        Reply reply = createDefaultReply(request, HTTP.OK);
	reply.setNoCache(); // don't cache this
        reply.setMaxAge(-1); // 
	reply.setContentMD5(null);
        // Dump the request as the body
        // Removed unused headers:
	// FIXME should be something else for chuncked stream
	ByteArrayOutputStream ba = new ByteArrayOutputStream();
	try {
	    reply.setContentType(new MimeType("message/http"));
	    request.dump(ba);
	    reply.setContentLength(ba.size());
	} catch (Exception ex) {
	    ex.printStackTrace();
	}
	reply.setStream(new ByteArrayInputStream(ba.toByteArray()));
	return reply;
    }
    
    public Reply extended(Request request) 
    	throws ProtocolException, ResourceException
    {
	String method = request.getMethod() ;
	Reply error   = request.makeReply(HTTP.NOT_IMPLEMENTED) ;
	error.setContent("Method "+method+" not implemented.") ;
	throw new HTTPException (error) ;
    }

    /**
     * The Browse Mime type.
     */
    protected static MimeType  browsetype = null;

    /**
     * Get the Browse Mime type.
     */
    protected synchronized MimeType getBrowseType() {
	if ( browsetype == null ) {
	    try {
		browsetype = new MimeType("application/x-navibrowse");
	    } catch (Exception ex) {
		ex.printStackTrace();
	    }
	}
	return browsetype;
    }

    /**
     * A present to GNNPress users !
     * This method implements the <code>BROWSE</code> method that
     * AOL press (or GNN press, or whatever its last name is) expects.
     * @param request The request to process.
     * @exception ProtocolException If some error occurs.
     * @return A Reply instance.
     */
    public Reply browse (Request request) 
	throws ProtocolException
    {
	if (dresource == null) {
	    Reply error = request.makeReply(HTTP.NOT_IMPLEMENTED) ;
	    error.setContent("Method "+request.getMethod()+
			     " not implemented.") ;
	    throw new HTTPException (error) ;
	}

	Enumeration  enum      = dresource.enumerateResourceIdentifiers() ;
	Vector       resources = Sorter.sortStringEnumeration(enum) ;
	int          rsize     = ((resources == null) ? 0 : resources.size()) ;
	StringBuffer sb        = new StringBuffer() ;

	// As we have enumerated all resources, just looking the store is ok
	for (int i = 0 ; i < rsize ; i++) {
	    String            rname = (String) resources.elementAt(i) ;
	    ResourceReference rr    = null;
	    Resource          r     = null;
	    try {
		rr = dresource.lookup(rname) ;
		r = rr.lock();
		// may throw InvalidResourceException

		if ( r instanceof DirectoryResource ) {
		    sb.append("application/x-navidir "+
			      rname+
			      "\r\n") ;
		} else {
		    HTTPFrame itsframe = 
			(HTTPFrame) resource.getFrame(getClass());
		    if (itsframe != null) {
			sb.append(itsframe.getContentType().toString()+
				  " "+
				  rname+
				  "\r\n") ;
		    }
		}
	    } catch (InvalidResourceException ex) {
		continue;
	    } finally {
		rr.unlock();
	    }
	}
	Reply reply = request.makeReply(HTTP.OK) ;
	reply.addPragma("no-cache");
	reply.setNoCache();
	reply.setContent(sb.toString()) ;
	reply.setContentType(getBrowseType());
	return reply ;
    }

}
