// NegotiatedResource.java
// $Id: NegotiatedResource.java,v 1.14 1997/03/12 19:03:12 abaird 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.util.*;

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

class VariantState {
    HTTPResource variant = null ;
    double qs      = 0.0 ;
    double qe      = 0.0 ;
    double qc      = 0.0 ;
    double ql      = 0.0 ;
    double q       = 0.0 ;	// quality (mime type one)
    double Q       = 0.0 ;	// the big Q

    public String toString() {
	String name = (String) variant.getIdentifier() ;
	if ( name == null )
	    name = "<noname>" ;
	return "[" + name 
	    + " qs=" + qs 
	    + " qe=" + qe
	    + " ql=" + ql
	    + " q =" + q
	    + " Q =" + getQ() 
	    +"]" ;
    }

    void setContentEncodingQuality (double qe) {
	this.qe = qe ;
    }

    void setQuality (double q) {
	this.q = q ;
    }

    void setQuality (HttpAccept a) {
	q = a.getQuality() ;
    }

    void setLanguageQuality (double ql) {
	this.ql = ql ;
    }

    void setLanguageQuality (HttpAcceptLanguage l) {
	this.ql = l.getQuality() ;
    }

    double getLanguageQuality () {
	return ql ;
    }

    HTTPResource getResource () {
	return variant ;
    }

    double getQ() {
	return qe * q * qs * ql ;
    }

    VariantState (HTTPResource variant, double qs) {
	this.qs      = qs ;
	this.variant = variant ;
    }
}

/**
 * This class provides content negociation among any other resources.
 * This resource tries to conform to the HTTP/1.0 specification of
 * content negociation.
 * <p>A bit of terminology here: in HTTP terms, a resource should negotiate
 * among a set of entities, not among a set of variants. Jigsaw uses the
 * term <em>resource</em> as an object able to respond to HTTP requests, and
 * so is able to negotiate among resources (eg it can negotiate among 
 * a CGIResource, a FileResource of whatever other resources).
 */
 
public class NegotiatedResource extends FilteredResource {
    /**
     * Turn debugging on/off.
     */
    private static final boolean debug = false;
    /**
     * Minimum quality for a resource to be considered further.
     */
    private static final double REQUIRED_QUALITY = 0.0001 ;
    /**
     * The Vary header field for this resource is always the same.
     */
    protected static HttpTokenList VARY = null;

    /**
     * Attribute index - The set of names of variants.
     */
    protected static int ATTR_VARIANTS = -1 ;

    static {
	// Compute and initialize the Vary header once and for all
	String vary[] = { "Accept",
			  "Accept-Charset",
			  "Accept-Language",
			  "Accept-Encoding" };
	VARY = HttpFactory.makeStringList(vary);
    }

    static {
	Attribute   a = null ;
	Class     cls = null ;
	try {
	    cls = Class.forName("w3c.jigsaw.resources.NegotiatedResource") ;
	} catch (Exception ex) {
	    ex.printStackTrace() ;
	    System.exit(1) ;
	}
	// The names of the varint we negotiate
	a = new StringArrayAttribute("variants"
				     , null
				     , Attribute.EDITABLE) ;
	ATTR_VARIANTS = AttributeRegistry.registerAttribute(cls, a) ;
    }
	    
    /**
     * Get the variant names.
     */

    public String[] getVariantNames() {
	return (String[]) getValue(ATTR_VARIANTS, null) ;
    }

    /**
     * Get the variant resources.
     * This is somehow broken, it shouldn't allocate the array of variants
     * on each call. However, don't forget that the list of variants can be
     * dynamically edited, this means that if we are to keep a cache of it 
     * (as would be the case if we kept the array of variants as instance var)
     * we should also take care of editing of attributes (possible, but I
     * just don't have enough lifes).
     * @return An array of HTTPResource, or <strong>null</strong>.
     * @exception HTTPException If one of the variants doesn't exist.
     */

    public HTTPResource[] getVariantResources() 
	throws HTTPException
    {
	// Get the variant names:
	String names[] = getVariantNames() ;
	if ( names == null )
	    return null ;
	// Look them up in our parent directory:
	HTTPResource      variants[] = new HTTPResource[names.length] ;
	DirectoryResource parent     = (DirectoryResource) getParent() ;
	for (int i = 0 ; i < names.length ; i++) {
	    try {
		variants[i] = (HTTPResource) parent.lookup(names[i]) ;
	    } catch (InvalidResourceException ex) {
		throw new HTTPException(names[i]+": couldn't be restored.");
	    }
	}
	return variants ;
    }
	
     
    /**
     * Print the current negotiation state.
     * @param header The header to print first.
     * @param states The current negotiation states.
     */

    protected void printNegotiationState (String header, Vector states) {
	if ( debug ) {
	    System.out.println ("------" + header) ;
	    for (int i = 0 ; i < states.size() ; i++) {
		VariantState state = (VariantState) states.elementAt(i) ;
		System.out.println (state) ;
	    }
	    System.out.println ("-----") ;
	}
    }

    /**
     * Negotiate among content encodings.
     * <p>BUG: This will work only for single encoded variants.
     * @param states The current negotiation states.
     * @param request The request to handle.
     */
     
    protected boolean negotiateContentEncoding (Vector states
						, Request request) 
	throws HTTPException
    {
	if ( ! request.hasAcceptEncoding() ) {
	    // All encodings accepted:
	    for (int i = 0 ; i < states.size() ; i++) {
		VariantState state = (VariantState) states.elementAt(i) ;
		state.setContentEncodingQuality(1.0) ;
	    }
	} else {
	    String encodings[] = request.getAcceptEncoding() ;
	    for (int i = 0 ; i < states.size() ; i++) {
		VariantState state    = (VariantState) states.elementAt(i) ;
		HTTPResource resource = state.getResource() ;
		if ( ! resource.definesAttribute ("content-encoding") ) {
		    state.setContentEncodingQuality (1.0) ;
		} else {
		    String ve = resource.getContentEncoding() ;
		    state.setContentEncodingQuality (0.001) ;
		  encoding_loop:
		    for (int j = 0 ; j < encodings.length ; j++) {
			if ( ve.equals (encodings[j]) ) {
			    state.setContentEncodingQuality(1.0) ;
			    break encoding_loop ;
			}
		    }
		}
	    }
	    // FIXME We should check here against unlegible variants as now
	}
	return false ;
    }

    /**
     * Negotiate on charsets.
     * <p>BUG: Not implemented yet.
     * @param states The current states of negotiation.
     * @param request The request to handle.
     */

    protected boolean negotiateCharsetQuality (Vector states
					       , Request request) {
	return false ;
    }

    /**
     * Negotiate among language qualities.
     * <p>BUG: This will only work for variants that have one language tag.
     * @param states The current states of negotiation.
     * @param request The request to handle.
     */

    protected boolean negotiateLanguageQuality (Vector states
						, Request request) 
	throws HTTPException
    {
	if ( ! request.hasAcceptLanguage() ) {
	    for (int i = 0 ; i < states.size() ; i++) {
		VariantState state = (VariantState) states.elementAt(i) ;
		state.setLanguageQuality (1.0) ;
	    }
	} else {
	    HttpAcceptLanguage languages[] = request.getAcceptLanguage() ;
	    boolean  varyLang    = false ;
	    for (int i = 0 ; i < states.size() ; i++) {
		VariantState state    = (VariantState) states.elementAt(i) ;
		HTTPResource resource = state.getResource() ;
		if ( ! resource.definesAttribute ("content-language") ) {
		    state.setLanguageQuality (-1.0) ;
		} else {
		    varyLang = true ;
		    String lang = resource.getContentLanguage() ;
		    int jidx    = -1 ;
		    for (int j = 0 ; j < languages.length ; j++) {
			if ( languages[j].getLanguage().equals(lang) )
			    jidx = j ;
		    }
		    if ( jidx < 0 ) 
			state.setLanguageQuality(0.001) ;
		    else 
			state.setLanguageQuality (languages[jidx]) ;
		}
	    }
	    if ( varyLang ) {
		for (int i = 0 ; i < states.size() ; i++) {
		    VariantState s = (VariantState) states.elementAt(i);
		    if ( s.getLanguageQuality() < 0 )
			s.setLanguageQuality (0.5) ;
		}
	    } else {
		for (int i = 0 ; i < states.size() ; i++) {
		    VariantState s = (VariantState) states.elementAt(i) ;
		    s.setLanguageQuality (1.0) ;
		}
	    }
	}
	return false ;
    }
    
    /**
     * Negotiate among content types.
     * @param states The current states of negotiation.
     * @param request The request to handle.
     */

    protected boolean negotiateContentType (Vector states
					 , Request request) 
	throws HTTPException
    {
	if ( ! request.hasAccept() ) {
	    // All variants get a quality of 1.0
	    for (int i = 0 ; i < states.size() ; i++) {
		VariantState state = (VariantState) states.elementAt(i) ;
		state.setQuality (1.0) ;
	    }
	} else {
	    // The browser has given some preferences:
	    HttpAccept accepts[] = request.getAccept() ;
	    for (int i = 0 ; i < states.size() ; i++ ) {
		VariantState state = (VariantState) states.elementAt(i) ;
		// Get the most specific match for this variant:
		MimeType vt = state.getResource().getContentType();
		int jmatch = -1 ;
		int jidx   = -1 ;
		for (int j = 0 ; j < accepts.length ; j++) {
		    int match = vt.match (accepts[j].getMimeType()) ;
		    if ( match > jmatch ) {
			jmatch = match ;
			jidx   = j ;
		    }
		}
		if ( jidx < 0 )
		    state.setQuality (0.0) ;
		else 
		    state.setQuality(accepts[jidx]) ;
	    }
	}
	return false ;
    }

    /**
     * Negotiate among the various variants for the Resource.
     * We made our best efforts to be as compliant as possible to the HTTP/1.0
     * content negotiation algorithm.
     */

    protected HTTPResource negotiate (Request request) 
	throws HTTPException
    {
	// Check for zero or one variant:
	HTTPResource variants[] = getVariantResources() ;
	if ( variants.length < 2 ) {
	    if ( variants.length == 0 ) {
		Reply reply = request.makeReply(HTTP.NONE_ACCEPTABLE) ;
		reply.setContent ("<p>No acceptable variants.") ;
		throw new HTTPException (reply) ;
	    } else {
		return variants[0] ;
	    }
	}
	// Build a vector of variant negociation states, one per variants:
	Vector states = new Vector (variants.length) ;
	for (int i = 0 ; i < variants.length ; i++) {
	    double qs = 1.0 ;
	    if ( variants[i].definesAttribute ("quality") )
		qs = variants[i].getQuality() ;
	    if ( qs > REQUIRED_QUALITY )
		states.addElement (new VariantState (variants[i], qs)) ;
	}
	// Content-encoding negociation:
	if ( debug )
	    printNegotiationState ("init:", states) ;
	if ( negotiateContentEncoding (states, request) ) 
	    // Remains a single acceptable variant:
	    return ((VariantState) states.elementAt(0)).getResource() ;
	if ( debug )
	    printNegotiationState ("encoding:", states) ;
	// Charset quality negociation:
	if ( negotiateCharsetQuality (states, request) ) 
	    // Remains a single acceptable variant:
	    return ((VariantState) states.elementAt(0)).getResource() ;
	if ( debug )
	    printNegotiationState ("charset:", states) ;
	// Language quality negociation:
	if ( negotiateLanguageQuality (states, request) ) 
	    // Remains a single acceptable variant:
	    return ((VariantState) states.elementAt(0)).getResource() ;
	if ( debug )
	    printNegotiationState ("language:", states) ;
	// Content-type negociation:
	if ( negotiateContentType (states, request) )
	    // Remains a single acceptable variant:
	    return ((VariantState) states.elementAt(0)).getResource() ;
	if ( debug )
	    printNegotiationState ("type:", states) ;
	// If we reached this point, this means that multiple variants are 
	// acceptable at this point. Keep the one that have the best quality.
	if ( debug )
	    printNegotiationState ("before Q selection:", states) ;
	double qmax = REQUIRED_QUALITY ;
	for (int i = 0 ; i < states.size() ; ) {
	    VariantState state = (VariantState) states.elementAt(i) ;
	    if ( state.getQ() > qmax ) {
		for (int j = i ; j > 0 ; j--)
		    states.removeElementAt(0) ;
		qmax = state.getQ() ;
		i = 1 ;
	    } else {
		states.removeElementAt(i) ;
	    }
	}
	if ( debug )
	    printNegotiationState ("After Q selection:", states) ;
	if ( qmax == REQUIRED_QUALITY ) {
	    Reply reply = request.makeReply(HTTP.NONE_ACCEPTABLE) ;
	    reply.setContent ("<p>No acceptable variant.") ;
	    throw new HTTPException (reply) ;
	} else if ( states.size() == 1 ) {
	    return ((VariantState) states.elementAt(0)).getResource() ;
	} else {
	    // Respond with multiple choice (for the time being, there should
	    // be a parameter to decide what to do.
	    Reply reply = request.makeReply(HTTP.MULTIPLE_CHOICE) ;
	    HtmlGenerator g = new HtmlGenerator ("Multiple choice for "
						 + getIdentifier()) ;
	    g.append ("<ul>") ;
	    for (int i = 0 ; i < states.size() ; i++) {
		VariantState state = (VariantState) states.elementAt(i) ;
		String name = (String) state.getResource().getIdentifier();
		g.append ("<li>" 
			  + "<a href=\"" + name + "\">" + name + "</a>"
			  + " Q= " + state.getQ()) ;
	    }
	    reply.setStream (g) ;
	    throw new HTTPException (reply) ;
	}
    }

    /**
     * Perform an HTTP request.
     * Negotiate among the variants, the best variant according to the request
     * fields, and make this elect3d variant serve the request.
     * @param request The request to handle.
     * @exception HTTPException If negotiating among the resource variants 
     *    failed.
     */

    public Reply perform(Request request)
	throws HTTPException, ClientException
    {
	// Run content negotiation now:
	HTTPResource selected = negotiate(request) ;
	// This should never happen: either the negotiation succeed, or the
	// negotiate method should return an error.
	if ( selected == null ) {
	    Reply error = request.makeReply(HTTP.INTERNAL_SERVER_ERROR) ;
	    error.setContent("Error negotiating among resource's variants.");
	    throw new HTTPException(error) ;
	}
	// FIXME content neg should be done at lookup time
	// FIXME enhencing the reply should be done at outgoingfilter
	// Get the original variant reply, and add its location as a header:
	Reply reply = selected.perform(request) ;
	reply.setHeaderValue(reply.H_VARY, VARY);
	reply.setContentLocation(selected.getURL(request).toExternalForm()) ;
	return reply;
    }

}
