// HTTPResource.java
// $Id: HTTPResource.java,v 1.6 1997/07/08 15:58:43 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.net.*;
import java.util.*;

import w3c.tools.store.*;
import w3c.jigsaw.http.* ;
import w3c.tools.resources.LookupState;

import w3c.www.mime.*;
import w3c.www.http.*;
import w3c.tools.resources.*;
import w3c.tools.resources.Resource;
import w3c.tools.resources.impl.*;

/**
 * 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 ResourceImpl implements FilteredResource {
  /**
   * 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;
    
  // 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;
  
  protected double quality = -1;

  protected String title = null;

  protected String[] contentLanguage = null;

  protected String[] contentEncoding = null;

  protected MimeType contentType = null;

  protected int contentLength = -1;

  protected long lastModified = -1;

  protected long maxAge = -1;

  protected String icon = null;

  protected int oid = -1;

  protected String urlPath = 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;
  }

  /**
   * 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() {
    modified = true;
    setLastModified(System.currentTimeMillis());
  }

  /**
   * 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 ResourceReference getParent() {
    return container;
  }

  /**
   * 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 (String)holder.protectedGetValue("urlPath");
    }
    
    public void setUrlPath(String urlPath) {
	holder.protectedSetValue("urlPath",urlPath);
    }

    protected String igetUrlPath() {
	return urlPath;
    }

    protected void isetUrlPath(String urlPath) {
	this.urlPath = urlPath;
    }

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

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

  public httpd getServer() {
    return ((HTTPResourceContext)getContext()).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 ((Double)holder.protectedGetValue("quality")).doubleValue();
  }

  public void setQuality(double quality){
    holder.protectedSetValue("quality", new Double(quality));
  }

  protected double igetQuality() {
      return quality;
  }

  protected void isetQuality(double quality){
    this.quality = quality;
  }

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

  public String getTitle() {
    return (String)holder.protectedGetValue("title");
  }

  public void setTitle(String title) {
    holder.protectedSetValue("title",title);
  }

    protected  String igetTitle() {
	return title;
    }

  protected void isetTitle(String title) {
    this.title = title;
  }

  /**
   * 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[])holder.protectedGetValue("contentLanguage");
  } 

  public void setContentLanguage(String languages[]){
    holder.protectedSetValue("contentLanguage",languages);
  }

  protected String[] igetContentLanguage() {
    return contentLanguage;
  } 

  protected void isetContentLanguage(String languages[]){
    this.contentLanguage = languages;
  }

  /**
   * 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() {
    return (String[])holder.protectedGetValue("contentEncoding");
  }

  public void setContentEncoding(String encodings[]){
    holder.protectedSetValue("contentEncoding",encodings);
  }

  protected String[] igetContentEncoding() {
    return contentEncoding;
  }

  protected void isetContentEncoding(String encodings[]){
    this.contentEncoding = encodings;
  }

  /**
   * Get this resource content type.
   * @return An instance of MIMEType, or <strong>null</strong> if not
   *    defined.
   */

  public MimeType getContentType() {
    return (MimeType)holder.protectedGetValue("contentType");
  }

  public void setContentType(MimeType contentType) {
    holder.protectedSetValue("contentType",contentType);
  }

  protected MimeType igetContentType() {
    return contentType;
  }

  protected void isetContentType(MimeType contentType) {
    this.contentType = contentType;
  }

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

  public int getContentLength() {
    return ((Integer)holder.protectedGetValue("contentLength")).intValue();
  }

  public void setContentLength(int length) {
    holder.protectedSetValue("contentLength", new Integer(length));
  }

    protected int igetContentLength() {
	return contentLength;
    }

    protected void isetContentLength(int length) {
	this.contentLength = length;
    }

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

  public void setLastModified(long lastModified) {
    holder.protectedSetValue("lastModified", new Long(lastModified));
  }
  
  protected long igetLastModified() {
    return lastModified;
  }

  protected void isetLastModified(long lastModified) {
    this.lastModified = lastModified;
  }

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

  public String getIcon() {
    return (String)holder.protectedGetValue("icon");
  }

  public void setIcon(String icon) {
    holder.protectedSetValue("icon",icon);
  }

  protected String igetIcon() {
    return icon;
  }

  protected void isetIcon(String icon) {
    this.icon = icon;
  }

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

  public void setMaxAge(long maxAge) {
    holder.protectedSetValue("maxAge", new Long(maxAge));
  }

  protected long igetMaxAge() {
    return maxAge;
  }

  protected void isetMaxAge(long maxAge) {
    this.maxAge = maxAge;
  }

  /**
   * 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 && getContentType() != 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());
    }
    // (definesAttribute(ATTR_CONTENT_ENCODING) &&
    if (getContentEncoding() != null && _contentencoding == null )
      _contentencoding = HttpFactory.makeStringList(getContentEncoding());
    // (definesAttribute(ATTR_CONTENT_LANGUAGE) &&
    if (getContentLanguage() != null && _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.setContentLength(-1);
    reply.setContentType(null);
    // Add the allow header:
    if ( allowed != null )
      reply.setHeaderValue(Reply.H_ALLOW, allowed);
    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() {
    super.destroy();
  }

  /**
   * 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 {
      reply = extended(request) ;
    }
    return reply;
  }

  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 = 8831811339780240926L;

}
