// HTTPResource.java
// $Id: HTTPResource.java,v 1.34 1997/12/12 10:35:11 ylafon 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.net.*;

import w3c.tools.store.*;
import w3c.jigsaw.http.* ;
import w3c.www.mime.*;
import w3c.www.http.*;

/**
 * The basic HTTP resource.
 * Defines a bunch of attributes for all HTTP resources, and all the HTTP
 * method that provides access to them.
 */

public class HTTPResource extends Resource {

    /**
     * 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;
    static {
	// allowed shares _allowed value, that's a feature.
	String str_allowed[] = { "GET", "HEAD", "OPTIONS" };
	_allowed = HttpFactory.makeStringList(str_allowed);
    }
    // Methods allowed by instances of that class in particular:
    protected        HttpTokenList  allowed = _allowed;
    
    /**
     * Attribute index - The index for our parent attribute.
     */
    protected static int ATTR_PARENT = -1 ;
    /**
     * Attribute index - The index for our URL attribute.
     */
    protected static int ATTR_URL = -1;
    /**
     * 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 content length attribute.
     */
    protected static int ATTR_CONTENT_LENGTH = -1 ;
    /**
     * Attribute index - The index for the last-modified attribute.
     */
    protected static int ATTR_LAST_MODIFIED = -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 - The object identifier.
     */
    protected static int ATTR_OID = -1;
    /**
     * Attribute index - The hierarchical context of the resource.
     */
    protected static int ATTR_CONTEXT = -1;

    static {
	Attribute a   = null ;
	Class     cls = null ;
	// Get a pointer to our class:
	try {
	    cls = Class.forName("w3c.jigsaw.resources.HTTPResource") ;
	} catch (Exception ex) {
	    ex.printStackTrace() ;
	    System.exit(1) ;
	}
	// Our parent resource (the one that created us)
	a = new ObjectAttribute("parent"
				, "w3c.tools.store.Resource"
				, null
				, Attribute.COMPUTED|Attribute.DONTSAVE);
	ATTR_PARENT = AttributeRegistry.registerAttribute(cls, a) ;
	// Our URL
	a = new StringAttribute("url"
				, null
				, Attribute.COMPUTED|Attribute.DONTSAVE);
	ATTR_URL = AttributeRegistry.registerAttribute(cls, a) ;
	// Our runtime context
	a = new ObjectAttribute("context"
				, "w3c.jigsaw.resources.ResourceContext"
				, null
				, Attribute.COMPUTED|Attribute.DONTSAVE);
	ATTR_CONTEXT = AttributeRegistry.registerAttribute(cls, a) ;
	// 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"
				  , MimeType.TEXT_PLAIN
				  , Attribute.EDITABLE) ;
	ATTR_CONTENT_TYPE = AttributeRegistry.registerAttribute(cls,a);
	// The content length attribute:
	a = new IntegerAttribute("content-length"
				 , null
				 , Attribute.COMPUTED);
	ATTR_CONTENT_LENGTH = AttributeRegistry.registerAttribute(cls,a);
	// The last modified attribute:
	a = new DateAttribute("last-modified"
			      , null
			      , Attribute.COMPUTED|Attribute.EDITABLE) ;
	ATTR_LAST_MODIFIED = 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) ;
	// The object identifier, *should* be uniq (see below)
	a = new IntegerAttribute("oid"
				 , null
				 , Attribute.COMPUTED);
	ATTR_OID = AttributeRegistry.registerAttribute(cls, a);
    }

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

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

    /**
     * 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_CONTENT_LENGTH)
	    contentlength = null;
	if ( idx == ATTR_CONTENT_ENCODING )
	    contentencoding = null;
	if ( idx == ATTR_CONTENT_LANGUAGE )
	    contentlanguage = null;
	// Any attribute setting modifies the last modified time:
	lastmodified = null;
    }

    /**
     * Set an HTTPResource attribute.
     * Mark modified should also update our last-modification time.
     * @param idx The index of the value to be set.
     * @param value Its new value.
     */

    public void markModified() {
	super.markModified() ;
	values[ATTR_LAST_MODIFIED] = new Long(System.currentTimeMillis());
    }

    // FIXME doc
    
    public boolean lookup(LookupState ls, LookupResult lr) 
	throws HTTPException
    {
	if ( ls.hasMoreComponents() ) {
	    // We are not a container resource, and we don't have children:
	    lr.setTarget(null);
	    return false;
	} else {
	    // We are done !
	    lr.setTarget(this);
	    return true;
	}
    }

    /**
     * Get this resource parent resource.
     * The parent of a resource can be either <strong>null</strong> if it is
     * the server root resource, or any HTTPResource.
     * @return An instance of HTTPResource, or <strong>null</strong>
     */

    public HTTPResource getParent() {
	return (HTTPResource) getValue(ATTR_PARENT, null) ;
    }

    /**
     * Get the file part of the URL this resource is attached to.
     * @return An URL object specifying the location in the information 
     *    space of this resource.
     */

    public String getURLPath() {
	return getString(ATTR_URL, null) ;
    }

    /**
     * Get the full URL for that resource.
     * @return An URL instance.
     */

    public URL getURL(Request request) {
	try {
	    return new URL(request.getURL(), getURLPath());
	} catch (MalformedURLException ex) {
	    throw new RuntimeException("unable to build "
				       + getURLPath()
				       + " full URL, from server "
				       + getServer().getURL());
	}
    }

    /**
     * Get the hierarchical context for that resource.
     * @return A ResourceContext instance, guaranteed not to be <strong>null
     * </strong>
     */

    public ResourceContext getContext() {
	return (ResourceContext) getValue(ATTR_CONTEXT, null);
    }

    /**
     * Get the server this resource is served by.
     * @return The first instance of Jigsaw this resource was attached to.
     */

    public httpd getServer() {
	return ((ResourceContext) getValue(ATTR_CONTEXT, null)).getServer();
    }

    /**
     * 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();
	return (String) getString (ATTR_CONTENT_ENCODING, def) ;
    }

    /**
     * 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 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 last modification time.
     * @return A long giving the date of the last modification time, or
     *    <strong>-1</strong> if undefined.
     */

    public long getLastModified() {
	return getLong(ATTR_LAST_MODIFIED, (long) -1) ;
    }

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

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

    /**
     * Get this resource's object identifier.
     * An object identifier is to be used specifically in etags. It's purpose
     * is to uniquify the etag of a resource. It's computed as a random number
     *, on demand only.
     * @return A uniq object identifier for that resource, as an inteeger.
     */

    public int getOid() {
	int oid = getInt(ATTR_OID, -1);
	if ( oid == -1 ) {
	    double d = Math.random() * ((double) Integer.MAX_VALUE);
	    setInt(ATTR_OID, oid = (int) d);
	}
	return oid;
    }
     

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


    /**
     * 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 )
	    contenttype = HttpFactory.makeMimeType(getContentType());
	if (contentlength == null) {
	    int cl = getContentLength();
	    if ( cl >= 0 )
		contentlength = HttpFactory.makeInteger(getContentLength());
	}
	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());
    }

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

    /**
     * The default GET method replies with a not implemented.
     * @param request The request to handle.
     * @exception HTTPException Always thrown, to return a NOT_IMPLEMENTED
     *    error.
     * @exception ClientException If the client instance controling the
     * request processing got a fatal error.
     */

    public Reply get (Request request) 
	throws HTTPException, ClientException
    {
	Reply error = request.makeReply(HTTP.NOT_IMPLEMENTED) ;
	error.setContent("Method GET not implemented.") ;
	throw new HTTPException (error) ;
    }

    /**
     * The default HEAD method replies does a GET and removes entity.
     * @param request The request to handle.
     * @exception HTTPException Always thrown, to return a NOT_IMPLEMENTED
     *    error.
     * @exception ClientException If the client instance controling the
     * request processing got a fatal error.
     */

    public Reply head(Request request)
	throws HTTPException, ClientException
    {
	Reply reply = get(request) ;
	reply.setStream((InputStream) null);
	return reply;
    }

    /**
     * The default POST method replies with a not implemented.
     * @param request The request to handle.
     * @exception HTTPException Always thrown, to return a NOT_IMPLEMENTED
     *    error.
     * @exception ClientException If the client instance controling the
     * request processing got a fatal error.
     */

    public Reply post(Request request)
	throws HTTPException, ClientException
    {
	Reply error = request.makeReply(HTTP.NOT_IMPLEMENTED) ;
	error.setContent("Method POST not implemented.") ;
	throw new HTTPException (error) ;
    }

    /**
     * The default PUT method replies with a not implemented.
     * @param request The request to handle.
     * @exception HTTPException Always thrown, to return a NOT_IMPLEMENTED
     *    error.
     * @exception ClientException If the client instance controling the
     * request processing got a fatal error.
     */

    public Reply put(Request request)
	throws HTTPException, ClientException
    {
	Reply error = request.makeReply(HTTP.NOT_IMPLEMENTED) ;
	error.setContent("Method PUT not implemented.") ;
	throw new HTTPException (error) ;
    }

    /**
     * The default OPTIONS 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 options(Request request)
	throws HTTPException, ClientException
    {
	Reply reply = createDefaultReply(request, HTTP.OK);
	// Removed unused headers:
	reply.setContentType(null);
	// Add the allow header:
	reply.setContentLength(0);
	if ( allowed != null )
	    reply.setHeaderValue(Reply.H_ALLOW, allowed);
	return reply;
    }

    /**
     * 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); // 
        // Dump the request as the body
        // Removed unused headers:
	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;
    }
    
    /**
     * The default DELETE method replies with a not implemented.
     * @param request The request to handle.
     * @exception HTTPException Always thrown, to return a NOT_IMPLEMENTED
     *    error.
     * @exception ClientException If the client instance controling the
     * request processing got a fatal error.
     */

    public Reply delete(Request request)
	throws HTTPException, ClientException
    {
	Reply error = request.makeReply(HTTP.NOT_IMPLEMENTED) ;
	error.setContent("Method DELETE not implemented.") ;
	throw new HTTPException (error) ;
    }

    /**
     * Delete this resource and notifies its container.
     */

    public synchronized void delete() {
	ContainerResource parent = (ContainerResource) getParent();
	if ( parent != null )
	    parent.markModified();
	super.delete();
    }

    /**
     * Verify that resource.
     * This method is triggered through some administration interface
     * and should check if the resource is still valid or not. It may 
     * delete the resource if needed.
     */

    public boolean verify() {
	return true;
    }

    /**
     * The default LINK method replies with a not implemented.
     * @param request The request to handle.
     * @exception HTTPException Always thrown, to return a NOT_IMPLEMENTED
     *    error.
     * @exception ClientException If the client instance controling the
     * request processing got a fatal error.
     */

    public Reply link(Request request) 
	throws HTTPException, ClientException
    {
	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 HTTPException Always thrown, to return a NOT_IMPLEMENTED
     *    error.
     * @exception ClientException If the client instance controling the
     * request processing got a fatal error.
     */

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

    /**
     * The handler for unknown method replies with a not implemented.
     * @param request The request to handle.
     * @exception HTTPException Always thrown, to return a NOT_IMPLEMENTED
     *    error.
     * @exception ClientException If the client instance controling the
     * request processing got a fatal error.
     */

    public Reply extended(Request request)
	throws HTTPException, ClientException
    {
	Reply error = request.makeReply(HTTP.NOT_IMPLEMENTED) ;
	error.setContent("Method "+request.getMethod()+" not implemented.") ;
	throw new HTTPException (error) ;
    }

    /**
     * Dispatch the given request to the appropriate method.
     * @param request The request to dispatch and process.
     * @return A Repky instance.
     * @exception HTTPException If processing failed.
     * @exception ClientException If the client that is responsible for this
     * request should be terminated.
     */

    public Reply perform(Request request)
        throws HTTPException, ClientException
    {
        Reply  reply  = null;
	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;
    }


}
