// FileResource.java
// $Id: FileResource.java,v 1.38 1997/09/26 15:05:34 bmahe 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 w3c.tools.store.*;
import w3c.jigsaw.http.* ;
import w3c.www.mime.*;
import w3c.www.http.*;

public class FileResource extends FilteredResource {
    // 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 int ATTR_FILENAME = -1 ;
    /**
     * Attribute index - Do we allow PUT method on this file.
     */
    protected static int ATTR_PUTABLE = -1 ;
    /**
     * Attribute index - The date at which we last checked the file content.
     */
    protected static int ATTR_FILESTAMP = -1 ;

    // The Http entity tag for this resource
    HttpEntityTag etag   = null;
    static {
	Attribute a   = null ;
	Class     cls = null ;
	try {
	    cls = Class.forName("w3c.jigsaw.resources.FileResource") ;
	} catch (Exception ex) {
	    ex.printStackTrace();
	    System.exit(0);
	}
	// The filename attribute.
	a = new FilenameAttribute("filename"
				  , null
				  , Attribute.EDITABLE) ;
	ATTR_FILENAME = AttributeRegistry.registerAttribute(cls, a) ;
	// The putable flag:
	a = new BooleanAttribute("putable"
				 , Boolean.FALSE
				 , Attribute.EDITABLE) ;
	ATTR_PUTABLE = AttributeRegistry.registerAttribute(cls, a) ;
	// The file stamp attribute
	a = new DateAttribute("filestamp"
			      , new Long(-1) 
			      , Attribute.COMPUTED) ;
	ATTR_FILESTAMP = AttributeRegistry.registerAttribute(cls, a) ;
    }
    
    /**
     * 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() {
      return (String) getValue(ATTR_FILENAME, null);
    }

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

    public boolean getPutableFlag() {
	return getBoolean(ATTR_PUTABLE, false) ;
    }

    /**
     * Get the date at which we last examined the file.
     */

    public long getFileStamp() {
	return getLong(ATTR_FILESTAMP, (long) -1) ;
    }

    /**
     * Get the name of the backup file for this resource.
     * @return A File object suitable to receive the backup version of this
     *    file.
     */

    public File getBackupFile() {
	File   file = getFile() ;
	String name = file.getName() ;
	return new File(file.getParent(), name+"~") ;
    }

    /**
     * Save the given stream as the underlying file content.
     * This method preserve the old file version in a <code>~</code> file.
     * @param in The input stream to use as the resource entity.
     * @return A boolean, <strong>true</strong> if the resource was just
     * created, <strong>false</strong> otherwise.
     * @exception IOException If dumping the content failed.
     */

    protected synchronized boolean newContent(InputStream in) 
	throws IOException
    {
	File   file     = getFile() ;
	boolean created = (! file.exists() | (file.length() == 0));
	String name     = file.getName() ;
	File   temp     = new File(file.getParent(), "#"+name+"#") ;
	String iomsg    = null ;

	// We are not catching IO exceptions here, except to remove temp:
	try {
	    FileOutputStream fout  = new FileOutputStream(temp) ;
	    byte             buf[] = new byte[4096] ;
	    for (int got = 0 ; (got = in.read(buf)) > 0 ; )
		fout.write(buf, 0, got) ;
	    fout.close() ;
	} catch (IOException ex) {
	    iomsg = ex.getMessage() ;
	} finally {
	    if ( iomsg != null ) {
		temp.delete() ;
		throw new IOException(iomsg) ;
	    } else {
		File backup = getBackupFile();
		if ( backup.exists() )
		    backup.delete();
		file.renameTo(getBackupFile()) ;
		temp.renameTo(file) ;
		// update our attributes for this new content:
		updateFileAttributes() ;
	    }
	}
	return created;
    }

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

    /**
     * Uupdate the cached headers for this resource.
     */

    protected void updateCachedHeaders() {
	super.updateCachedHeaders();
	// We only take car eof etag here:
	if ( etag == null ) {
	    long lstamp = getFileStamp();
	    if ( lstamp >= 0L ) {
		String soid  = Integer.toString(getOid(), 32);
		String stamp = Long.toString(lstamp, 32);
		etag = HttpFactory.makeETag(false, soid+":"+stamp);
	    }
	}
    }

    /**
     * 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 = super.createDefaultReply(request, status);
	// Set the entity tag:
	if ( etag != null )
	    reply.setHeaderValue(reply.H_ETAG, etag);
	if ( acceptRanges )
	    reply.setHeaderValue(reply.H_ACCEPT_RANGES, _accept_ranges);
	return reply;
    }

    /**
     * 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(int idx, Object value) {
	super.setValue(idx, value) ;
	if ((idx == ATTR_FILENAME) || (idx == ATTR_IDENTIFIER))
	      file = null;
	if (idx == ATTR_FILESTAMP)
	    etag = null;
	if ( idx == 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 ) {
	    // Get the file name:
	    String name = getFilename() ;
	    if ( name == null )
		name = getIdentifier() ;
	    // 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);
	}
	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 = getLastModified();
	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 = getLastModified();
	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 = getContentLength();
	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;
    }

    /**
     * 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 = getParent() ;
	    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 = getParent() ;
	    while ((p != null) && ! (p instanceof DirectoryResource) )
		p = p.getParent() ;
	    if ( p == null )
		return null;
	    if ( p instanceof DirectoryResource ) {
		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 = getParent() ;
	    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;
	}
    }

    /**
     * Put a new entity in this resource.
     * @param request The request to handle.
     */

    public synchronized Reply put(Request request)
	throws HTTPException, ClientException
    {
	int status = HTTP.OK;
	checkContent();
	updateCachedHeaders();
	// Is this resource writable ?
	if ( ! getPutableFlag() )
	    super.put(request) ;
	// 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 ) {
		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 ) {
		client.sendContinue();
	    }
	    if ( newContent(request.getInputStream()) )
		status = HTTP.CREATED;
	    else
		status = HTTP.NO_CONTENT;
	} catch (IOException ex) {
	    throw new ClientException(request.getClient(), ex);
	}
	// Refresh the client's display
	Reply reply = null;
	if ( status == HTTP.CREATED ) {
	    reply = request.makeReply(status);
	    reply.setLocation(getURL(request));
	    reply.setContent ("<p>Entity body saved succesfully !") ;
	} else {
	    reply = createDefaultReply(request, status);
	}
	return reply ;
    }

    /**
     * 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() ;
	setValue(ATTR_FILESTAMP, new Long(file.lastModified()));
	setValue(ATTR_CONTENT_LENGTH, new Integer((int)file.length()));
	return ;
    }

    /**
     * Update our computed attributes.
     */

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

	if ((stamp < 0) || (stamp < fstamp)) 
	    updateFileAttributes() ;
    }

    /**
     * Initialize the FileResource instance.
     */

    public void initialize(Object values[]) {
	super.initialize(values);
	// If we have a filename attribute, update url:
	String filename = getFilename();
	if ( filename != null )
	    setValue(ATTR_URL, getParent().getURLPath()+filename);
	// Upgrade the list of allowed method, in case put is supported:
	if ( getPutableFlag() )
	    allowed = _put_allowed;
	acceptRanges = true;
    }
}
