// EntityCachedResource.java
// $Id: EntityCachedResource.java,v 1.22 2000/10/19 15:54:50 ylafon Exp $
// (c) COPYRIGHT MIT, INRIA and Keio, 1999.
// Please first read the full copyright statement in file COPYRIGHT.html

package org.w3c.www.protocol.http.cache;

import java.net.URL;

import java.util.Enumeration;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;

import org.w3c.util.ArrayDictionary;
import org.w3c.tools.resources.Attribute;
import org.w3c.tools.resources.AttributeRegistry;
import org.w3c.tools.resources.BooleanAttribute;
import org.w3c.tools.resources.IntegerAttribute;
import org.w3c.tools.resources.LongAttribute;
import org.w3c.tools.resources.StringAttribute;
import org.w3c.www.http.HeaderDescription;
import org.w3c.www.http.HTTP;
import org.w3c.www.http.HttpContentRange;
import org.w3c.www.http.HttpRange;
import org.w3c.www.http.HttpEntityTag;
import org.w3c.www.http.HttpFactory;
import org.w3c.www.http.ByteRangeOutputStream;
import org.w3c.www.protocol.http.HttpException;
import org.w3c.www.protocol.http.Reply;
import org.w3c.www.protocol.http.Request;
import org.w3c.www.mime.MimeType;
import org.w3c.jigsaw.frames.MimeTypeAttribute;

/**
 * A cached resource with an entity
 */
public class EntityCachedResource extends CachedResource {

    /**
     * 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;  
    /**
     * Attribute index - The Content-Type of the resource
     */
    protected static int ATTR_CONTENT_TYPE = -1;
    /**
     * Attribute index - The resource's max age.
     */
    protected static int ATTR_FRESHNESS_LIFETIME = -1;
    /**
     * Attribute index - The initial age of this resource.
     */
    protected static int ATTR_INITIAL_AGE = -1;
    /**
     * Attribute index - The response time
     */
    protected static int ATTR_RESPONSE_TIME = -1;    
    /**
     * Attribute index - Revalidate flag
     */
    protected static int ATTR_REVALIDATE = -1;    
    /**
     * Attribute index - The download state
     */
    protected static int ATTR_LOAD_STATE = -1;    

    static {
	Attribute a = null;
	Class     c = null;
	try {
	    c = Class.forName(
		"org.w3c.www.protocol.http.cache.EntityCachedResource");
	} catch (Exception ex) {
	    ex.printStackTrace();
	    System.exit(1);
	}
	// Declare the contenht type attribute:
	a = new MimeTypeAttribute("content-type"
				  , null
				  , Attribute.COMPUTED);
	ATTR_CONTENT_TYPE = AttributeRegistry.registerAttribute(c, a);
	// Declare the max-age (freshness lifetime) value.
	a = new IntegerAttribute("freshness-lifetime"
				 , null
				 , Attribute.COMPUTED);
	ATTR_FRESHNESS_LIFETIME = AttributeRegistry.registerAttribute(c, a);
	// Declare the initial age value.
	a = new IntegerAttribute("initial-age"
				 , null
				 , Attribute.COMPUTED);
	ATTR_INITIAL_AGE = AttributeRegistry.registerAttribute(c, a);
	// Declare the response time value.
	a = new LongAttribute("response-time"
			      , null
			      , Attribute.COMPUTED);
	ATTR_RESPONSE_TIME = AttributeRegistry.registerAttribute(c, a);
	// Declare the response time value.
	a = new BooleanAttribute("revalidate"
			      , Boolean.FALSE
			      , Attribute.COMPUTED);
	ATTR_REVALIDATE = AttributeRegistry.registerAttribute(c, a);	
    }

    // some download specific variables
    protected boolean revalidating = false;
    protected boolean regetting    = false;
    protected boolean hasEntity    = false;
    protected int       oldsize = -1;
    protected int    wantedsize = -1;
    // our cache filter, if we need to notify it
    protected CacheFilter filter;

    /**
     * Get the Content-Type of the cached resource of <code>null</code> if
     * there is no mime type (it should NEVER happen!)
     * @return a MimeType
     */
    public MimeType getContentType() {	
	return (MimeType) getValue(ATTR_CONTENT_TYPE, null);
    }

    /**
     * Set the Content-Type of this cached resource
     * @param a MimeType, the mime type of this resource
     */
    public void setContentType(MimeType type) {
	setValue(ATTR_CONTENT_TYPE, type);
    }

    /**
     * Get this resource's freshness lifetime (RFC2616: 13.2.4).
     * @return A long number of seconds for which that entry will remain
     * valid, or <strong>-1</strong> if undefined.
     */
    public int getFreshnessLifetime() {
	return getInt(ATTR_FRESHNESS_LIFETIME, -1);
    }

    /**
     * Set this cached entry . freshness lifetime (RFC2616: 13.2.4).
     * @param maxage A number of seconds during which the entry will 
     * remain valid, or <strong>-1</strong> to undefine previous setting.
     */
    public void setFreshnessLifetime(int freshnessLifetime) {
	setInt(ATTR_FRESHNESS_LIFETIME, freshnessLifetime);
    }

    /**
     * Get this cached entry initial age.
     * @return A long number of seconds giving the initial age
     * or <strong>-1</strong> if undefined.
     */
    public int getInitialAge() {
	return getInt(ATTR_INITIAL_AGE, -1);
    }

    /**
     * Set this resource's initial age.
     * @param initage The initial age as a number of seconds
     * or <strong>-1</strong> to undefine previous setting.
     */
    public void setInitialAge(int initage) {
	setInt(ATTR_INITIAL_AGE, initage);
    }

    /**
     * Get the time of the response used to cached that entry.
     * @return A long number of milliseconds since Java epoch, or <strong>
     * -1</strong> if undefined.
     */
    public long getResponseTime() {
	return getLong(ATTR_RESPONSE_TIME, -1);
    }

    /**
     * Set this cached entry response time.
     * @param responsetime A long number of milliseconds indicating the 
     * response time relative to Java epoch, or <strong>-1</strong> to 
     * undefined previous setting.
     */
    public void setResponseTime(long responsetime) {
	setLong(ATTR_RESPONSE_TIME, responsetime);
    }

    /**
     * Get the revalidate flag
     * @return a boolean, <code>true</code> if the proxy must revalidate
     * stale entries
     * -1</strong> if undefined.
     */
    public boolean getRevalidate() {
	return getBoolean(ATTR_REVALIDATE, false);
    }

    /**
     * Set this cached entry revalidate flag.
     * @param validate, a boolean, <code>true</code> if this entry needs
     * to be revalidated while stale.
     */
    public void setRevalidate(boolean validate) {
	setBoolean(ATTR_REVALIDATE, validate);
    }

    /**
     * Get the entity tag associated with that cached entry
     * @return the entity tag or <strong>null</strong> if undefined
     */
    public HttpEntityTag getHETag() {
	if (definesAttribute(ATTR_ETAG)) {
	    if (etags == null) {
		etags    = new HttpEntityTag[1];
		etags[0] = HttpFactory.parseETag(getETag());
	    }
	    return etags[0];
	}
	return null;
    }
    // FIXME add entity tag here

    // end of the basic accessors

    /**
     * Get the cached data for that cached entry.
     * @return A <em>non-buffered</em> output stream.
     */
    public synchronized InputStream getInputStream() 
	throws IOException
    {
	return new BufferedInputStream(new FileInputStream(getFile()));
    }

    /**
     * Get the current age of this resource
     * @return a long the current age of this resource
     */
    public int getCurrentAge() {
	long now = System.currentTimeMillis();
	// RFC2616: 13.2.3 Age Calculation
	 return (int) (getInitialAge() + ((now - getResponseTime()) / 1000));
    }

    /**
     * Try to validate an <code>If-Modified-Since</code> request.
     * @param request The request to validate.
     * @return An integer, <code>COND_FAILED</code>, if the condition  was
     * checked, but failed; <code>COND_OK</code> of condition was checked
     * and succeeded, <strong>0</strong> otherwise.
     */

    public int checkIfModifiedSince(Request request) {
	long ims = request.getIfModifiedSince();
	long cmt = getLastModified();
	if ( ims >= 0 )
	    return ((cmt > 0) && (cmt - 1000 <= ims)) ? COND_FAILED : COND_OK;
	return 0;
    }

    /**
     * Try to validate an <code>If-Unmodified-Since</code> request.
     * @param request The request to validate.
     * @return An integer, <code>COND_FAILED</code>, if the condition  was
     * checked, but failed; <code>COND_OK</code> of condition was checked
     * and succeeded, <strong>0</strong> otherwise.
     */

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

    /**
     * Try to validate an <code>If-Match</code> request.
     * @param request The request to validate.
     * @return An integer, <code>COND_FAILED</code>, if the condition  was
     * checked, but failed; <code>COND_OK</code> of condition was checked
     * and succeeded, <strong>0</strong> otherwise.
     */

    public int checkIfMatch(Request request) {
	HttpEntityTag tags[] = request.getIfMatch();
	if ( tags != null ) {
	    HttpEntityTag etag = getHETag();
	    // Good, real validators in use:
	    if ( etag != null ) {
		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;
    }

    /**
     * Try to validate an <code>If-None-Match</code> request.
     * @param request The request to validate.
     * @return An integer, <code>COND_FAILED</code>, if the condition  was
     * checked, but failed; <code>COND_OK</code> of condition was checked
     * and succeeded, <strong>0</strong> otherwise.
     */

    public int checkIfNoneMatch(Request request) {
	// Check for an If-None-Match conditional:
	HttpEntityTag tags[] = request.getIfNoneMatch();
	if ( tags != null ) {
	    HttpEntityTag etag = getHETag();
	    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;
    }

    /**
     * Called when the tee succeed, it allows you to notify a listener of the 
     * Tee that the download completed succesfully with a specific size
     * @parameter the size received, an integer
     */
    public synchronized void notifyTeeSuccess(int size) {
	int state = getLoadState();
        try {
	    if ( wantedsize > 0 ) {
		if (!regetting) {
		    // sanity check
		    if (state == STATE_NOT_LOADED) {
			if (size == wantedsize) {
			    // cool! the right size and it was the first 
			    // download!
			    setCurrentLength(size);
			    setLoadState(STATE_LOAD_COMPLETE);
			} else {
			    // argh! wrong size and a success hum...
			    setCurrentLength(size);
			    setLoadState(STATE_LOAD_ERROR);
			    System.out.println(getIdentifier()
					       +": tee stream mismatch, "
					       + "bytes(adv/got)="+
					       wantedsize+"/"+size);
			}
		    } else {
			// how can we end up here, I frankly don't know
			setCurrentLength(size);
			setLoadState(STATE_LOAD_ERROR);
			System.out.println(getIdentifier()
					   +": UNKNOWN STATE for "
					   +"tee stream!, bytes(adv/got)="+
					   wantedsize+"/"+size);
		    }
		} else {
		    // we asked for the diff, and we have it!!!
		    if (size == wantedsize) {
			setCurrentLength(oldsize+wantedsize);
			setLoadState(STATE_LOAD_COMPLETE);
		    } else {
			// argh! wrong size and a success hum...
			setCurrentLength(size);
			setLoadState(STATE_LOAD_ERROR);
			System.out.println(getIdentifier()
					   +": tee stream mismatch in reget, "
					   + "bytes(adv/got)="+
					   wantedsize+"/"+size);
		    }
		}
	    } else {
		// we didn't knew the size, we should trust what we got
		// (unless it is HTTP/1.0)
		setCurrentLength(size);
		// FIXME (a trust flag to select btw COMPLETE and UNKNOWN
		setLoadState(STATE_LOAD_COMPLETE);
	    }
	    // Update cache filter space usage:
//	    filter.markUsed(this, oldsize, wantedsize);
	  
	} finally {
	    cleanUpload();
	}
    }

    public void notifyTeeFailure(int size) {
	System.out.println(getIdentifier()+": tee streaming failed !");
	int state = getLoadState();
	
	setCurrentLength(size);
	setLoadState(STATE_LOAD_ERROR);
	System.out.println(getIdentifier()
			   +": tee stream mismatch, "
			   + "bytes(adv/got)="+
			   wantedsize+"/"+size);
	// and finish the thing!
	cleanUpload();
    }

    // FIXME should be called after every upload
    protected synchronized void cleanUpload() {
	// FIXME reset a whole bunch of stuff
	uploading = false;
	filter.cleanUpload(this);
	notifyAll();
    }

    /**
     * FIXME Will be replaced soon, so that multiple people may share 
     * the same temporary resource.
     * Wait for the upload to finish, if needed.
     */
    protected final synchronized void waitUpload() {
	while ( uploading ) {
	    try {
		wait();
	    } catch (InterruptedException ex) {
	    }
	}
    }

    /**
     * handle a range request, according to the first range or the
     * request FIXME we should handle all the ranges at some point...
     */
    protected Reply handleRangeRequest(Request request, HttpRange r) {
	// Should we check against a IfRange header ?
	HttpEntityTag t = request.getIfRange();
	if ( t != null ) {
	    if (t.isWeak() || ! t.getTag().equals(getHETag().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 = request.makeReply(HTTP.REQUESTED_RANGE_NOT_SATISFIABLE);
	    rr.setContentLength(-1);
	    rr.setHeaderValue(rr.H_CONTENT_RANGE, cr);
	    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 = request.makeReply(HTTP.PARTIAL_CONTENT);
	    try {
		rr.setContentMD5(null); // just in case :)
		rr.setContentLength(sz);
		rr.setHeaderValue(rr.H_CONTENT_RANGE, cr);
		rr.setStream(new ByteRangeOutputStream(getFile(), fb, lb+1));
		return rr;
	    } catch (IOException ex) {
	    }
	} 
	return null;
    }
	
    /**
     * decorate the reply header with some meta information taken
     * from the cached resource
     * @return a reply, the one we just updated
     */
    protected Reply setReplyHeaders(Reply reply) {
	int status = reply.getStatus();
	if (status != HTTP.NOT_MODIFIED) {
	    // FIXME check for byte range replies
	    reply.setContentLength(getContentLength());
	    // dump the headers we know.
	    reply.setContentMD5(getContentMD5());
	    reply.setContentLanguage(getContentLanguage());
	    reply.setContentEncoding(getContentEncoding());
	    reply.setContentType(getContentType());
	    reply.setLastModified(getLastModified());
	}
	reply.setETag(getHETag());
	long date = getDate();
	if ( date > 0 )
	    reply.setDate(getDate());
	reply.setAge(getCurrentAge());
	ArrayDictionary a = getExtraHeaders();
	if ( a != null ) {
	    // This is the slowest operation of the whole cache :-(
	    Enumeration e = a.keys();
	    while (e.hasMoreElements() ) {
		String hname  = (String) e.nextElement();
		String hvalue = (String) a.get(hname);
		reply.setValue(hname, hvalue);
	    }
	}
	return reply;
    }

    /**
     * This cached entry has been checked valid, perform given request.
     * @param request The request to perform.
     * @return An Reply instance.
     * @exception HttpException If something went wrong.
     */
    public Reply perform(Request request)
	throws HttpException
    {
	// If the resource is currently being uploaded, wait:
	waitUpload();
	// Now perform the request:
	try {
	    Reply   reply       = null;
	    boolean needsEntity = true;
	    // Handle range requests:
	    HttpRange ranges[] = request.getRange();
	    if ((ranges != null) && (ranges.length == 1)) 
		reply = handleRangeRequest(request, ranges[0]);
	    // Handle full retreivals:
	    if ( reply == null ) {
		int status = getStatus();
		// Try validating first 
		// NOTE: We know we are only dealing with GETs and HEADs here
		// otherwise the cache wouldn't be used...
		if ( checkIfMatch(request) == COND_FAILED ) {
		    status      = HTTP.PRECONDITION_FAILED;
		    needsEntity = false;
		} else if ( checkIfNoneMatch(request) == COND_FAILED ) {
		    status      = HTTP.NOT_MODIFIED;
		    needsEntity = false;
		} else if ( checkIfModifiedSince(request) == COND_FAILED ) {
		    status      = HTTP.NOT_MODIFIED;
		    needsEntity = false;
		} else if ( checkIfUnmodifiedSince(request) == COND_FAILED ) {
		    status      = HTTP.PRECONDITION_FAILED;
		    needsEntity = false;
		}
		// Emit reply:
		reply = request.makeReply(status);
		if ( needsEntity ) {
		    reply.setStream(getInputStream());
		}
	    }
	    setReplyHeaders(reply);
	    // Check if entity is needed:
	    String mth = request.getMethod();
	    if ( mth.equals("HEAD") || mth.equals("OPTIONS") )
		reply.setStream(null);
//	    filter.markUsed(this);
	    return reply;
	} catch (IOException ex) {
//	    if (debug)
//		ex.printStackTrace();
	    // Some exception occured, delete that resource (no longer usefull)
//	    delete();
	}
	return null;
    }

    /**
     * Try using an active stream to cache the content.
     * Byte size usage is taken care of only at the end of the download
     * to make sure we get the right sizes (might different from the
     * advertized ones).
     * @return An InputStream instance if active caching was possible,
     * <strong>null</strong> otherwise.
     */
    public synchronized InputStream tryActiveCacheContent(InputStream in)
	throws IOException
    {
	// If we don't return null, we *are* responsible for cleaning up
	// the upload *whatever* happens ...
	InputStream  tee = null;
	OutputStream out = null;
	uploading = true;
	// Open the output stream:
	try {
	    out = new FileOutputStream(getFile());
	} catch (IOException ex) {
//	    if (debug)
		ex.printStackTrace();
	    // We'll let cacheContent take care of that situation:
	    return null;
	}
	// We might be able to use active streams:
//	if (upnewsize > ACTIVE_STREAM_THRESOLD ) {
	    tee = ActiveStream.createTee(this, in, out);
	    if ( tee != null )
		return tee;
//	} 
	// We were not able to active stream:
	try {
	    out.close();
	} catch (IOException ex) {
	}
	return null;
    }

    /**
     * The basic initialization
     */
    public void initialize(Object values[]) {
	super.initialize(values);
    }    

    /**
     * sets some useful information about the entity
     * @param the request that requested this entity
     * @param the reply triggered by this request
     */
    protected void updateInfo(Request request, Reply reply) {
	String   mth       = request.getMethod();
	boolean  hasEntity = !(mth.equals("HEAD") || mth.equals("OPTIONS"));
	// is it a revalidation?
	if (!request.hasState(CacheState.STATE_REVALIDATION)) {
	    // no, go for it!
	    setStatus(reply.getStatus());
	    setContentType(reply.getContentType());
	    setContentLength(reply.getContentLength());
	    setLastModified(reply.getLastModified());
	    if (reply.hasHeader(reply.H_ETAG)) {
		setETag(reply.getETag().toString());
	    } else {
		// be safe here!
		setETag(null);
	    }
	    ArrayDictionary a = new ArrayDictionary(5, 5);
	    Enumeration     e = reply.enumerateHeaderDescriptions();
	    while ( e.hasMoreElements() ) {
		HeaderDescription d = (HeaderDescription) e.nextElement();
		// Skip all well-known headers:
		if ( d.isHeader(Reply.H_CONTENT_TYPE)
		     || d.isHeader(Reply.H_CONTENT_LENGTH)
		     || d.isHeader(Reply.H_LAST_MODIFIED)
		     || d.isHeader(Reply.H_ETAG)
		     || d.isHeader(Reply.H_AGE)
		     || d.isHeader(Reply.H_DATE)
		     || d.isHeader(Reply.H_CONNECTION)
		     || d.isHeader(Reply.H_PROXY_CONNECTION)
		     || d.isHeader(Reply.H_TRANSFER_ENCODING)
		     || d.getName().equalsIgnoreCase("keep-alive"))
		    continue;
		// This is an extra header:
		a.put(d.getName(), reply.getValue(d));
	    }
	    setExtraHeaders(a);
	    // FIXME add the headers ;)
	}
    }

    /**
     * This cached entry needs revalidation, it will modify the 
     * request to do that.
     */
    public Request setRequestRevalidation(Request request) {
	Request origreq = (Request) request.getClone();
	request.setState(CacheState.STATE_RESOURCE, this);
	request.setState(CacheState.STATE_ORIGREQ, origreq);
	// At this point, we use the suggested way of using date as etag:
	request.setIfModifiedSince(getLastModified());
	// But if we do have an etag, we also uses it, as recommended:
	if ((etags == null) && (getETag() != null)) {
	    etags    = new HttpEntityTag[1];
	    etags[0] = HttpFactory.parseETag(getETag());
	}
	request.setIfNoneMatch(etags);
	// We have to remove all other conditionals here:
	request.setIfRange(null);
	request.setRange(null);
	request.setIfUnmodifiedSince(-1);
	request.setIfMatch(null);
	return request;
    }	

    /**
     * A constructor for new resources that will get some data
     * directly
     * FIXME params
     */
    public EntityCachedResource(CacheFilter filter, Request req, Reply rep) {
	invalidated = false;
	setValue(ATTR_IDENTIFIER, req.getURL().toExternalForm());
	// Keep fast track of the filter:
	this.filter = filter;
	// update the headers
	updateInfo(req, rep);
	// and do some calculation according to the validator
	filter.getValidator().updateExpirationInfo(this, req, rep);
	// Save the content of resource into the content cache:
	setFile(filter.getStore().getNewEntryFile());
	wantedsize = rep.getContentLength();
	InputStream in;
	try {
	    in = tryActiveCacheContent(rep.getInputStream());
	    if (in == null) {
		// something bad happened
		// in = cacheContent(reply.getInputStream());
	    }
	    rep.setStream(in);
	} catch (IOException ex) {
	    // FIXME
	}	
    }

    public EntityCachedResource() {
	super();
    }
}


