NegotiatedFrame.java
Author: Benoit Mahe

    // NegotiatedFrame.java
    // $Id: NegotiatedFrame.html,v 1.4 1999/10/27 22:10:35 ylafon Exp $
    // (c) COPYRIGHT MIT and INRIA, 1996.
    // Please first read the full copyright statement in file COPYRIGHT.html

    package org.w3c.jigsaw.frames;

    import java.io.*;
    import java.util.*;

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

    import org.w3c.tools.resources.ProtocolException;
    import org.w3c.tools.resources.NotAProtocolException;

    /**
     * Content negotiation.
     */
    public class NegotiatedFrame extends HTTPFrame {

        class VariantState {
    	ResourceReference 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() {
    	    try {
    		Resource res = variant.lock();
    		String name = (String) res.getIdentifier() ;
    		if ( name == null )
    		    name = "<noname>" ;
    		return "[" + name
    		    + " qs=" + qs
    		    + " qe=" + qe
    		    + " ql=" + ql
    		    + " q =" + q
    		    + " Q =" + getQ()
    		    +"]" ;
    	    } catch (InvalidResourceException ex) {
    		return "invalid";
    	    } finally {
    		variant.unlock();
    	    }
    	}

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

    	ResourceReference getResource () {
    	    return variant ;
    	}

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

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

        private static Class httpFrameClass = null;

        static {
    	try {
    	    httpFrameClass = Class.forName("org.w3c.jigsaw.frames.HTTPFrame") ;
    	} catch (Exception ex) {
    	    throw new RuntimeException("No HTTPFrame class found.");
    	}
        }

        /**
         * 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("org.w3c.jigsaw.frames.NegotiatedFrame") ;
    	} 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) ;
        }

        public void setVariants(String variants[]) {
    	setValue(ATTR_VARIANTS, variants);
        }

        /**
         * 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 ResourceReference, or <strong>null</strong>.
         * @exception ProtocolException If one of the variants doesn't exist.
         */

        public ResourceReference[] getVariantResources()
    	throws ProtocolException
        {
    	// Get the variant names:
    	String names[] = getVariantNames() ;
    	if ( names == null )
    	    return null ;
    	// Look them up in our parent directory:
    	ResourceReference variants[] = new ResourceReference[names.length] ;
    	ResourceReference r_parent     = resource.getParent() ;
    	try {
    	    DirectoryResource parent = (DirectoryResource) r_parent.lock();
    	    for (int i = 0 ; i < names.length ; i++) {
    		variants[i] = parent.lookup(names[i]) ;
    		if (variants[i] == null)
    		    throw new HTTPException(names[i]+
    					    ": couldn't be restored.");
    	    }
    	} catch (InvalidResourceException ex) {
    	    throw new HTTPException("invalid parent for negotiation");
    	} finally {
    	    r_parent.unlock();
    	}
    	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 ProtocolException
        {
    	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) ;
    		ResourceReference rr  = state.getResource();
    		try {
    		    FramedResource resource = (FramedResource)rr.lock() ;
    		    HTTPFrame itsframe =
    			(HTTPFrame) resource.getFrame(httpFrameClass);
    		    if (itsframe != null) {
    			if ( !itsframe.definesAttribute("content-encoding") ) {
    			    state.setContentEncodingQuality (1.0) ;
    			} else {
    			    String ve = itsframe.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 ;
    				}
    			    }
    			}
    		    }
    		} catch (InvalidResourceException ex) {

    		} finally {
    		    rr.unlock();
    		}
    	    }
    	    // 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 ProtocolException
        {
    	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) ;
    		ResourceReference rr  = state.getResource();
    		try {
    		    FramedResource resource = (FramedResource)rr.lock() ;
    		    HTTPFrame itsframe =
    			(HTTPFrame) resource.getFrame(httpFrameClass);
    		    if (itsframe != null) {
    			if ( !itsframe.definesAttribute("content-language") ) {
    			    state.setLanguageQuality (-1.0) ;
    			} else {
    			    varyLang = true ;
    			    String lang = itsframe.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]) ;
    			}
    		    }
    		} catch (InvalidResourceException ex) {
    		    //FIXME
    		} finally {
    		    rr.unlock();
    		}
    	    }
    	    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 ProtocolException
        {
    	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:
    		ResourceReference rr = state.getResource();
    		try {
    		    FramedResource resource = (FramedResource)rr.lock() ;
    		    HTTPFrame itsframe =
    			(HTTPFrame) resource.getFrame(httpFrameClass);
    		    if (itsframe != null) {
    			MimeType vt = itsframe.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]) ;
    		    }
    		} catch (InvalidResourceException ex) {
    		    //FIXME
    		} finally {
    		    rr.unlock();
    		}
    	    }
    	}
    	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 ResourceReference negotiate (Request request)
    	throws ProtocolException
        {
    	// Check for zero or one variant:
    	ResourceReference variants[] = getVariantResources() ;
    	if ( variants.length < 2 ) {
    	    if ( variants.length == 0 ) {
    		Reply reply = request.makeReply(HTTP.NOT_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 ;
    	    try {
    		FramedResource resource = (FramedResource)variants[i].lock() ;
    		HTTPFrame itsframe =
    		    (HTTPFrame) resource.getFrame(httpFrameClass);
    		if (itsframe != null) {
    		    if ( itsframe.definesAttribute ("quality") )
    			qs = itsframe.getQuality() ;
    		    if ( qs > REQUIRED_QUALITY )
    			states.addElement(new VariantState (variants[i], qs)) ;
    		}
    	    } catch (InvalidResourceException ex) {
    		//FIXME
    	    } finally {
    		variants[i].unlock();
    	    }
    	}
    	// 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.NOT_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 "+
    						 resource.getIdentifier()) ;
    	    g.append ("<ul>") ;
    	    for (int i = 0 ; i < states.size() ; i++) {
    		VariantState state = (VariantState) states.elementAt(i) ;
    		String name = null;
    		ResourceReference rr = state.getResource();
    		try {
    		    name = rr.lock().getIdentifier();
    		    g.append ("<li>"
    			      + "<a href=\"" + name + "\">" + name + "</a>"
    			      + " Q= " + state.getQ()) ;
    		} catch (InvalidResourceException ex) {
    		    //FIXME
    		} finally {
    		    rr.unlock();
    		}
    	    }
    	    reply.setStream (g) ;
    	    throw new HTTPException (reply) ;
    	}
        }

        public void registerResource(FramedResource resource) {
    	super.registerOtherResource(resource);
        }

        /**
         * 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 ProtocolException If negotiating among the resource variants
         *    failed.
         */

        public ReplyInterface perform(RequestInterface req)
    	throws ProtocolException, NotAProtocolException
        {
    	ReplyInterface repi = performFrames(req);
    	if (repi != null)
    	    return repi;

    	if (! checkRequest(req))
    	    return null;

    	Request request = (Request) req;
    	// Run content negotiation now:
    	ResourceReference 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:
    	try {
    	    FramedResource resource = (FramedResource) selected.lock();
    	    Reply reply = (Reply)resource.perform(request) ;
    	    reply.setHeaderValue(reply.H_VARY, VARY);
    	    HTTPFrame itsframe =
    		(HTTPFrame) resource.getFrame(httpFrameClass);
    	    if (itsframe != null) {
    		reply.setContentLocation(
    				  itsframe.getURL(request).toExternalForm()) ;
    		return reply;
    	    }
    	    Reply error = request.makeReply(HTTP.INTERNAL_SERVER_ERROR) ;
    	    error.setContent("Error negotiating : "+
    			     "selected resource has no HTTPFrame");
    	    throw new HTTPException(error) ;
    	} catch (InvalidResourceException ex) {
    	    Reply error = request.makeReply(HTTP.INTERNAL_SERVER_ERROR) ;
    	    error.setContent("Error negotiating : Invalid selected resource");
    	    throw new HTTPException(error) ;
    	} finally {
    	    selected.unlock();
    	}
        }

    }