// FileResource.java
// $Id: FileResource.java,v 1.1 1997/05/29 11:58:27 ylafon Exp $
// (c) COPYRIGHT MIT and INRIA, 1997.
// Please first read the full copyright statement in file COPYRIGHT.html

package w3c.tools.resources.jigsaw ;

import java.io.*;
import java.util.*;
import java.lang.IllegalAccessException;
import w3c.tools.resources.*;
import w3c.jigsaw.http.* ;
import w3c.www.mime.*;
import w3c.www.http.*;

public class FileResource extends HTTPResource implements FilteredResource {

    private static final boolean debug = true;

    // The max list of methods we could support here, selected at init time:
    private static HttpTokenList _put_allowed   = null;
    private static HttpTokenList _accept_ranges = null;
    static {
	String str_allow[] = { "HEAD" , "GET" ,/* "PUT" ,*/ "OPTIONS" };
	_put_allowed = HttpFactory.makeStringList(str_allow);
	String accept_ranges[] = { "bytes" };
	_accept_ranges = HttpFactory.makeStringList(accept_ranges);
    }

    /**
     * Attributes index - The filename attribute.
     */
    protected static String PROP_FILENAME = "filename" ;
    /**
     * Attribute index - Do we allow PUT method on this file.
     */
    protected static String PROP_PUTABLE = "putable" ;
    /**
     * Attribute index - The date at which we last checked the file content.
     */
    protected static String PROP_FILESTAMP = "filestamp" ;

    /**
     * The file we refer to.
     * This is a cached version of some attributes, so we need to override
     * the setValue method in order to be able to catch any changes to it.
     */
    protected File file = null ;

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

    /**
     * Get this resource filename attribute.
     */

    public String getFilename() 
	throws IllegalAccessException
    {
	return (String) getValue(PROP_FILENAME);
    }

    /**
     * Get the PUT'able flag (are we allow to PUT to the resource ?)
     */

    public boolean getPutableFlag()
	throws IllegalAccessException
    {
	return ((Boolean) getValue(PROP_PUTABLE)).booleanValue() ;
    }
    
    /**
     * Get the date at which we last examined the file.
     */

    public long getFileStamp() 
	throws IllegalAccessException
    {
	return ((Long) getValue(PROP_FILESTAMP)).longValue() ;
    }

       /**
     * Set some of this resource attribute.
     * We just catch here any write access to the filename's, to update 
     * our cache file object.
     */

    public synchronized void setValue(String name, Object value)
	throws IllegalAccessException
    {
	super.setValue(name, value) ;
	if ((name.equals(PROP_FILENAME))/* || (name.equals(PROP_IDENTIFIER))*/)
	    file = null;
	if (name.equals(PROP_FILESTAMP))
	    etag = null;
//	if (name.equals(ATTR_PUTABLE) ) {
//	    if (value == Boolean.TRUE)
//		allowed = _put_allowed;
//	    else
//		allowed = _allowed;
    }
 
	    
    /**
     * Get this file resource file name.
     */

    public synchronized File getFile() {
	// Have we already computed this ?
	if ( file == null ) {
	    try {
		// Get the file name:
		String name = getFilename() ;
		if ( name == null )
		    // name = getIdentifier() ;
		    name = getName();
		// Get the file directory:
		HTTPResource p = getParent() ;
		//  while ((p != null) && ! (p instanceof DirectoryResource) )
		//    p = p.getParent() ;
		if ( p == null )
		    return null;
		file = new File(/*((DirectoryResource) p).getDirectory()
				  ,*/ name);
	    } catch (IllegalAccessException ex) {
		if(debug)
		    ex.printStackTrace();
	    }
	}
	return file ;
    }
    
    public int checkIfMatch(Request request) {
	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;
    }

    public int checkIfNoneMatch(Request request) {
	// 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;
    }

    public int checkIfModifiedSince(Request request) {
	// Check for an If-Modified-Since conditional:
	long ims = request.getIfModifiedSince() ;
	long cmt = -1;
	try {
	    cmt = getLastModified();
	} catch (IllegalAccessException ex) {
	    if(debug)
		ex.printStackTrace();
	}
	if ( ims >= 0 )
	    return ((cmt > 0) && (cmt - 1000 <= ims)) ? COND_FAILED : COND_OK;
	return 0;
    }

    public int checkIfUnmodifiedSince(Request request) {
	// Check for an If-Unmodified-Since conditional:
	long iums = request.getIfUnmodifiedSince();
	long cmt = -1;
	try {
	    cmt = getLastModified();
	} catch (IllegalAccessException ex) {
	    if (debug)
		ex.printStackTrace();
	}
	if ( iums >= 0 ) 
	    return ((cmt > 0) && (cmt - 1000) >= iums) ? COND_FAILED : COND_OK;
	return 0;
    }

    public Reply handleRangeRequest(Request request, HttpRange r) 
	throws HTTPException
    {
	// 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 = -1;
	try {
	    cl = getContentLength();
	} catch (IllegalAccessException ex) {
	    if (debug)
		ex.printStackTrace();
	}
	int fb = r.getFirstPosition();
	int lb = r.getLastPosition();
	if ((fb < 0) && (lb >= 0)) {
	    fb = cl - 1 - lb;
	    lb = cl;
	} else if (lb < 0) {
	    lb = cl;
	}
	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);
	    try {
		rr.setContentLength(lb-fb);
		rr.setHeaderValue(rr.H_CONTENT_RANGE, cr);
		rr.setStream(new ByteRangeOutputStream(file, fb, lb+1));
		return rr;
	    } catch (IOException ex) {
	    }
	} 
	return null;
    }

    /**
     * Check this file content, and update attributes if needed.
     * This method is normally called before any perform request is done, so
     * that we make sure that all meta-informations is up to date before
     * handling a request.
     * @return The time of the last update to the resource.
     */

    protected long checkContent() {
	try {
	    File file = getFile() ;
	    // Has this resource changed since last queried ? 
	    long lmt = file.lastModified() ;
	    long cmt = getFileStamp() ;
	    if ((cmt < 0) || (cmt < lmt)) {
//		updateFileAttributes() ;
		return getLastModified() ;
	    } else {
		return cmt;
	    }
	} catch (IllegalAccessException ex) {
	    if(debug)
		ex.printStackTrace();
	    long lm = -1;
	    try {
		lm = getLastModified();
	    } catch (IllegalAccessException lex) {
		ex.printStackTrace();
	    }
	    return lm;
	}
    }

     /**
     * The HEAD method on files, and their sub-classes.
     * @return A Reply instance.
     */

    public Reply head(Request request)
	throws HTTPException
    {
	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;
	}
	// Make sure the file still exists:
	if ( ! file.exists() ) {
	    // Delete the resource if parent is extensible:
	    HTTPResource p = null;
	    try {
		p = getParent() ;
	    } catch (IllegalAccessException ex) {
		// if this happens, it is more than strange!
		if (debug)
		    ex.printStackTrace();
	    }
//	    while ((p != null) && ! (p instanceof DirectoryResource) )
//		p = p.getParent() ;
	    if ( p == null )
		return null;
//	    DirectoryResource d = (DirectoryResource) p;
//	    if ( d.getExtensibleFlag() ) {
//		// The resource is indexed but has no file, emit an error
//		String msg = file+": deleted, removing the FileResource.";
//		getServer().errlog(this, msg);
//		delete();
//	    }
	    // 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) ;
	}
	return createDefaultReply(request, HTTP.OK);
    }

    /**
     * The GET method on files.
     * Check for the last modified time against the IMS if any. If OK, emit
     * a not modified reply, otherwise, emit the whole file.
     * @param request The request to handle.
     * @exception HTTPException If some error occured.
     */

    public Reply get(Request request)
	throws HTTPException
    {
	File file = getFile() ;
	checkContent();
	updateCachedHeaders();
	// Check validators:
	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;
	}
	// Does this file really exists, if so send it back
	if ( file.exists() ) {
	    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) {
		// I hate to have to loose time in tries
	    }
	    return reply ;
	} else {
	    // Delete the resource if parent is extensible:
	    HTTPResource p = null;
	    try {
		p = getParent() ;
	    } catch (IllegalAccessException ex) {
		if (debug)
		    ex.printStackTrace();
	    }
//	    while ((p != null) && ! (p instanceof DirectoryResource) )
//		p = p.getParent() ;
	    if ( p == null )
		return null;
//	    DirectoryResource d = (DirectoryResource) p;
//	    if ( d.getExtensibleFlag() ) {
//		// The resource is indexed but has no file, emit an error
//		String msg = file+": deleted, removing the FileResource.";
//		getServer().errlog(this, msg);
//		delete();
//	    }
	    // 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) ;
	}
	// not reached
    }

    /**
     * Is that resource still wrapping an existing file ?
     * If the underlying file has disappeared <string> and if</strong> the
     * container directory is extensible, remove the resource.
     */

    public synchronized boolean verify() {
	File file = getFile();
	if ( ! file.exists() ) {
	    // Is the parent extensible:
	    HTTPResource p = null;
	    try {
		p = getParent() ;
	    } catch (IllegalAccessException ex) {
		if (debug)
		    ex.printStackTrace();
	    }
//	    while ((p != null) && ! (p instanceof DirectoryResource) )
//		p = p.getParent() ;
	    if ( p == null )
		return false;
//	    DirectoryResource d = (DirectoryResource) p;
//	    if ( ! d.getExtensibleFlag() ) 
//		return false;
	    // Emit an error message, and delete the resource:
//	    String msg = file+": deleted, removing the FileResource.";
//	    getServer().errlog(this, msg);
//	    delete();
	    return false;
	} else {
	    return true;
	}
    }

    /**
     * Update the file related attributes.
     * The file we serve has changed since the last time we checked it, if
     * any of the attribute values depend on the file content, this is the
     * appropriate place to recompute them.
     */

    public void updateFileAttributes() {
	File file = getFile() ;
	try {
	    setValue(PROP_FILESTAMP, new Long(file.lastModified()));
	    setValue(PROP_CONTENT_LENGTH, new Integer((int)file.length()));
	} catch (IllegalAccessException ex) {
	    if (debug)
		ex.printStackTrace();
	}
	return ;
    }

    /**
     * Update our computed attributes.
     */

    public void updateAttributes() {
	long fstamp = getFile().lastModified() ;
	long stamp  = -1;

	try {
	    stamp = ((Long)getValue(PROP_FILESTAMP)).longValue() ;
	} catch (IllegalAccessException ex) {
	    if (debug)
		ex.printStackTrace();
	}
	if ((stamp < 0) || (stamp < fstamp)) 
	    updateFileAttributes() ;
    }

   // The Http entity tag for this resource
    HttpEntityTag etag   = null;
    public ResourceReference[] getFilters() {return null;}
    public ResourceReference[] getFilters(Class cls) {return null; }
    public void registerFilter(ResourceReference filter, Hashtable defs){}
    public void unregisterFilter(ResourceReference filter){};
}
