Hello,

as discussed two weeks ago, some browsers (e.g. Internet Explorer 7.0 and Firefox 2.0) sends as accepted media type XML with a higher quality than HTML. The consequence is, that a HTTP server sends XML instead of HTML, if it could produce XML. For this problem I've created a Filter (named HtmlPreferer now), that will increase the qualities for HTML media types (text/html and application/xhtml+xml) higher than both XML types (text/xml and application/xml), if at least one of both is available in a request.

Now it is available in the JAX-RS extension. What do you think about make it available for all developers in the main project? I put the actual source and a test case as attachment.

Jerome or Thierry: If you want to integrate the class in the main package or where ever, use the file of the JAX-RS; perhaps I continue developing or something like this.

best regards
  Stephan

Rob Heittman schrieb:
This legacy browser behavior is frustrating in the extreme. Why in heaven's name would a tool meant primarily for viewing HTML, request XML as a higher quality representation? Just goes to show how uber-excited everybody was about XML once upon a time. You know, because in the future, all web pages will someday be XML with a reference to an XSL stylesheet, not HTML.

Choosing a different MediaType for your XML, that the browser doesn't ask for, is the usual solution.

Another workable solution I have found -- if you are using XML and will get criticized for making up MIME types -- is to expose the browser-friendly HTML variant by itself on a distinct URI (e.g. person.html). That's sloppy too, just in a different way.

Now, packaging your data with JSON instead of XML will avoid the issue altogether, without making up MIME types =)

- R

On 2/18/08, *Stephan Koops* <[EMAIL PROTECTED] <mailto:[EMAIL PROTECTED]>> wrote:

    if a browser requests to a REST server, some browsers (Firefox and IE
    for example, Opera not) requests text/xml and application/xml with a
    higher quality than text/html.

/*
 * Copyright 2005-2008 Noelios Consulting.
 * 
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the "License"). You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the license at
 * http://www.opensource.org/licenses/cddl1.txt See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL HEADER in each file and
 * include the License file at http://www.opensource.org/licenses/cddl1.txt If
 * applicable, add the following below this CDDL HEADER, with the fields
 * enclosed by brackets "[]" replaced with your own identifying information:
 * Portions Copyright [yyyy] [name of copyright owner]
 */
package org.restlet.ext.jaxrs;

import java.util.List;
import java.util.Map;

import org.restlet.Context;
import org.restlet.Filter;
import org.restlet.Restlet;
import org.restlet.data.MediaType;
import org.restlet.data.Preference;
import org.restlet.data.Request;
import org.restlet.data.Response;

/**
 * <p>
 * Some browsers (e.g. Internet Explorer 7.0 and Firefox 2.0) sends as accepted
 * media type XML with a higher quality than html. The consequence is, that a
 * HTTP server sends XML instead of HTML, if it could produce XML. To avoid
 * this, you can use this filter.
 * </p>
 * <p>
 * This Filter will increase the qualities for HTML media types (text/html and
 * application/xhtml+xml) higher than both XML types (text/xml and
 * application/xml), if at least one of both is available in a request. The
 * check is implemented in method [EMAIL PROTECTED] 
#shouldChangeToPrefereHtml(Request)}.
 * <br>
 * Requests that not are not effected.
 * </p>
 * <p>
 * You may alter the test if the filter should change the request by subclass
 * this Filter and overrider method [EMAIL PROTECTED] 
#shouldChangeToPrefereHtml(Request)}.
 * </p>
 * 
 * @author Stephan Koops
 */
public class HtmlPreferer extends Filter {

    private static final int MT_PREF_APP_XHTML = 1;

    private static final int MT_PREF_APP_XML = 3;

    private static final int MT_PREF_TEXT_HTML = 0;

    private static final int MT_PREF_TEXT_XML = 2;

    private static final String MT_QUALITY_ARRAY = 
"org.restlet.HtmlPreferer.qualities";

    /**
     * Creates a new [EMAIL PROTECTED] HtmlPreferer}. You should use constructor
     * [EMAIL PROTECTED] #HtmlPreferer(Context)} or [EMAIL PROTECTED] 
#HtmlPreferer(Context, Restlet)}.
     */
    @Deprecated
    public HtmlPreferer() {
        super();
    }

    /**
     * Creates a new [EMAIL PROTECTED] HtmlPreferer}. You can give also the 
next restlet
     * by using constructor [EMAIL PROTECTED] #HtmlPreferer(Context, Restlet)}.
     * 
     * @param context
     *                the context from the parent
     */
    public HtmlPreferer(Context context) {
        super(context);
    }

    /**
     * Creates a new [EMAIL PROTECTED] HtmlPreferer}.
     * 
     * @param context
     *                the context from the parent
     * @param next
     *                the [EMAIL PROTECTED] Restlet} to call after filtering.
     */
    public HtmlPreferer(Context context, Restlet next) {
        super(context, next);
    }

    /**
     * Allows filtering before processing by the next Restlet.
     * 
     * @param request
     *                The request to filter.
     * @param response
     *                The response to update.
     * @return The continuation status, see
     *         [EMAIL PROTECTED] Filter#beforeHandle(Request, Response)}
     * @see Filter#beforeHandle(Request, Response)
     */
    @Override
    protected int beforeHandle(Request request, Response response) {
        if (shouldChangeToPrefereHtml(request))
            prefereHtml(request);
        return super.beforeHandle(request, response);
    }

    /**
     * Returns the quality of accepted media type application/xhtml+xml, or
     * null, if not present in the given request.
     * 
     * @param request
     * @return the quality of accepted media type application/xhtml+xml, or
     *         null, if not present in the given request.
     * @see #getHtmlMinQuality(Request)
     */
    protected Float getAppXhtmlQuality(Request request) {
        return getHtmlXmlMtQualities(request)[MT_PREF_APP_XHTML];
    }

    /**
     * Returns the quality of accepted media type app/xml, or null, if not
     * present in the given request.
     * 
     * @param request
     * @return the quality of accepted media type app/xml, or null, if not
     *         present in the given request.
     * @see #getXmlMaxQuality(Request)
     */
    protected Float getAppXmlQuality(Request request) {
        return getHtmlXmlMtQualities(request)[MT_PREF_APP_XML];
    }

    /**
     * Returns the lowest quality of the HTML types (text/html and
     * application/xhtml+xml), or null, if not available.
     * 
     * @param request
     * @return the lowest quality of the HTML types (text/html and
     *         application/xhtml+xml), or null, if not available.
     * @see #getTextHtmlQuality(Request)
     * @see #getAppXhtmlQuality(Request)
     */
    protected Float getHtmlMinQuality(Request request) {
        Float xhtmlQuality = getAppXhtmlQuality(request);
        Float htmlQuality = getTextHtmlQuality(request);
        if (xhtmlQuality == null)
            return htmlQuality;
        if (htmlQuality == null)
            return xhtmlQuality;
        return Math.min(xhtmlQuality, htmlQuality);
    }

    @SuppressWarnings("unchecked")
    private Float[] getHtmlXmlMtQualities(Request request) {
        Float[] htmlXmlQualities;
        Map<String, Object> attributes = request.getAttributes();
        htmlXmlQualities = (Float[]) attributes.get(MT_QUALITY_ARRAY);
        if (htmlXmlQualities == null) {
            htmlXmlQualities = new Float[4];
            List<Preference<MediaType>> acceptedMediaTypes = request
                    .getClientInfo().getAcceptedMediaTypes();
            for (Preference<MediaType> accPref : acceptedMediaTypes) {
                MediaType accMediaType = accPref.getMetadata();
                if (accMediaType.equals(MediaType.TEXT_HTML, true))
                    htmlXmlQualities[MT_PREF_TEXT_HTML] = accPref.getQuality();
                if (accMediaType.equals(MediaType.TEXT_XML, true))
                    htmlXmlQualities[MT_PREF_TEXT_XML] = accPref.getQuality();
                if (accMediaType.equals(MediaType.APPLICATION_XHTML_XML, true))
                    htmlXmlQualities[MT_PREF_APP_XHTML] = accPref.getQuality();
                if (accMediaType.equals(MediaType.APPLICATION_XML, true))
                    htmlXmlQualities[MT_PREF_APP_XML] = accPref.getQuality();
            }
            attributes.put(MT_QUALITY_ARRAY, htmlXmlQualities);
        }
        return htmlXmlQualities;
    }

    /**
     * Returns the quality of accepted media type text/html, or null, if not
     * present in the given request.
     * 
     * @param request
     * @return the quality of accepted media type text/html, or null, if not
     *         present in the given request.
     * @see #getHtmlMinQuality(Request)
     */
    protected Float getTextHtmlQuality(Request request) {
        return getHtmlXmlMtQualities(request)[MT_PREF_TEXT_HTML];
    }

    /**
     * Returns the quality of accepted media type text/xml, or null, if not
     * present in the given request.
     * 
     * @param request
     * @return the quality of accepted media type text/xml, or null, if not
     *         present in the given request.
     * @see #getXmlMaxQuality(Request)
     */
    protected Float getTextXmlQuality(Request request) {
        return getHtmlXmlMtQualities(request)[MT_PREF_TEXT_XML];
    }

    /**
     * Returns the highest quality of the XML types (text/xml and
     * application/xml), or null, if not available.
     * 
     * @param request
     * @return the highest quality of the XML types (text/xml and
     *         application/xml), or null, if not available.
     * @see #getAppXmlQuality(Request)
     * @see #getTextXmlQuality(Request)
     */
    protected Float getXmlMaxQuality(Request request) {
        Float appXmlQuality = getAppXmlQuality(request);
        Float textXmlQuality = getTextXmlQuality(request);
        if (appXmlQuality == null)
            return textXmlQuality;
        if (textXmlQuality == null)
            return appXmlQuality;
        return Math.max(appXmlQuality, textXmlQuality);
    }

    /**
     * Alters the request, that HTML is prefered before XML.
     * 
     * @param request
     *                the request to alter.
     */
    protected void prefereHtml(Request request) {
        Float xmlQualityO = getXmlMaxQuality(request);
        if (xmlQualityO == null)
            return;
        float xmlQuality = xmlQualityO;
        float htmlMinQuality;
        if (xmlQuality < 1) {
            htmlMinQuality = xmlQuality + 0.001f;
        } else {
            lowerWithQuality(request, xmlQuality);
            htmlMinQuality = 1;
        }
        htmlPrefsMin(request, htmlMinQuality);
    }

    /**
     * Lowers all accepted media type Preferences with the given quality to
     * 0.001 (littlest accuracy for HTML qualities). Also lowers recursive the
     * preferences with the goal quality.
     * 
     * @param request
     * @param quality
     */
    private void lowerWithQuality(Request request, float quality) {
        List<Preference<MediaType>> acceptedMediaTypes = request
                .getClientInfo().getAcceptedMediaTypes();
        float goalQuality = quality - 0.001f;
        boolean alreadyAvailable = false;
        for (Preference<MediaType> accPref : acceptedMediaTypes) {
            if (accPref.getQuality() == goalQuality) {
                alreadyAvailable = true;
                break;
            }
        }
        if (alreadyAvailable)
            lowerWithQuality(request, goalQuality);
        for (Preference<MediaType> accPref : acceptedMediaTypes) {
            if (accPref.getQuality() == quality)
                accPref.setQuality(goalQuality);
        }
    }

    /**
     * sets the quality of the preferences of the HTML media types (text/html
     * and app/xhtml) at least to the given quality.
     * 
     * @param request
     * @param htmlQuality
     */
    private void htmlPrefsMin(Request request, float htmlQuality) {
        for (Preference<MediaType> accPref : request.getClientInfo()
                .getAcceptedMediaTypes()) {
            MediaType accMediaType = accPref.getMetadata();
            if (accMediaType.equals(MediaType.APPLICATION_XHTML_XML, true)
                    || accMediaType.equals(MediaType.TEXT_HTML, true)) {
                if (accPref.getQuality() < htmlQuality)
                    accPref.setQuality(htmlQuality);
            }
        }
    }

    /**
     * This method checks, if HTML should be prefered for the given request. The
     * check may be overridden or complemented by override that method.
     * 
     * @param request
     *                the request to check.
     * @return true, if the [EMAIL PROTECTED] Request} should be altered, or 
false if not.
     */
    protected boolean shouldChangeToPrefereHtml(Request request) {
        Float htmlQuality = getHtmlMinQuality(request);
        Float xmlQuality = getXmlMaxQuality(request);
        if (htmlQuality == null || xmlQuality == null)
            return false;
        return xmlQuality >= htmlQuality;
    }
}
/*
 * Copyright 2005-2008 Noelios Consulting.
 * 
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the "License"). You may not use this file except in
 * compliance with the License.
 * 
 * You can obtain a copy of the license at
 * http://www.opensource.org/licenses/cddl1.txt See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL HEADER in each file and
 * include the License file at http://www.opensource.org/licenses/cddl1.txt If
 * applicable, add the following below this CDDL HEADER, with the fields
 * enclosed by brackets "[]" replaced with your own identifying information:
 * Portions Copyright [yyyy] [name of copyright owner]
 */
package org.restlet.test.jaxrs;

import java.util.List;

import org.restlet.Restlet;
import org.restlet.data.MediaType;
import org.restlet.data.Preference;
import org.restlet.data.Request;
import org.restlet.ext.jaxrs.HtmlPreferer;

import junit.framework.TestCase;

/**
 * This TextCase checks the [EMAIL PROTECTED] HtmlPreferer}.
 * 
 * @author Stephan Koops
 */
public class HtmlPrefererTest extends TestCase {

    private static final HtmlPreferer HTML_PREFERER = new HtmlPreferer(null,
            new Restlet());

    /**
     * @param accMediaTypes
     * @param mediaType
     * @param quality
     */
    private void addMediaTypePref(List<Preference<MediaType>> accMediaTypes,
            MediaType mediaType, float quality) {
        accMediaTypes.add(new Preference<MediaType>(mediaType, quality));
    }

    /**
     * @param accMediaTypes
     * @param q0
     * @param q1
     * @param q2
     * @param q3
     * @param q4
     */
    private void check(List<Preference<MediaType>> accMediaTypes, float q0,
            float q1, float q2, float q3, float q4) {
        assertEquals(5, accMediaTypes.size());
        Preference<MediaType> amt0 = accMediaTypes.get(0);
        Preference<MediaType> amt1 = accMediaTypes.get(1);
        Preference<MediaType> amt2 = accMediaTypes.get(2);
        Preference<MediaType> amt3 = accMediaTypes.get(3);
        Preference<MediaType> amt4 = accMediaTypes.get(4);
        assertEquals(q0, amt0.getQuality());
        assertEquals(q1, amt1.getQuality());
        assertEquals(q2, amt2.getQuality());
        assertEquals(q3, amt3.getQuality());
        assertEquals(q4, amt4.getQuality());
    }

    public void test1() {
        Request request = new Request();
        List<Preference<MediaType>> accMediaTypes = request.getClientInfo()
                .getAcceptedMediaTypes();
        addMediaTypePref(accMediaTypes, MediaType.APPLICATION_ALL, 0.2f);
        addMediaTypePref(accMediaTypes, MediaType.TEXT_HTML, 1f);
        addMediaTypePref(accMediaTypes, MediaType.APPLICATION_XHTML_XML, 0.9f);
        addMediaTypePref(accMediaTypes, MediaType.APPLICATION_XML, 0.8f);
        addMediaTypePref(accMediaTypes, MediaType.TEXT_XML, 0.7f);
        HTML_PREFERER.handle(request);
        check(accMediaTypes, 0.2f, 1f, 0.9f, 0.8f, 0.7f);
    }

    public void test2() {
        Request request = new Request();
        List<Preference<MediaType>> accMediaTypes = request.getClientInfo()
                .getAcceptedMediaTypes();
        addMediaTypePref(accMediaTypes, MediaType.APPLICATION_ALL, 0.2f);
        addMediaTypePref(accMediaTypes, MediaType.TEXT_HTML, 0.6f);
        addMediaTypePref(accMediaTypes, MediaType.APPLICATION_XHTML_XML, 0.9f);
        addMediaTypePref(accMediaTypes, MediaType.APPLICATION_XML, 0.8f);
        addMediaTypePref(accMediaTypes, MediaType.TEXT_XML, 0.7f);
        HTML_PREFERER.handle(request);
        check(accMediaTypes, 0.2f, 0.801f, 0.9f, 0.8f, 0.7f);
    }

    public void test3() {
        Request request = new Request();
        List<Preference<MediaType>> accMediaTypes = request.getClientInfo()
                .getAcceptedMediaTypes();
        addMediaTypePref(accMediaTypes, MediaType.APPLICATION_ALL, 0.2f);
        addMediaTypePref(accMediaTypes, MediaType.TEXT_HTML, 0.6f);
        addMediaTypePref(accMediaTypes, MediaType.APPLICATION_XHTML_XML, 1f);
        addMediaTypePref(accMediaTypes, MediaType.IMAGE_BMP, 0.8f);
        addMediaTypePref(accMediaTypes, MediaType.TEXT_PLAIN, 0.7f);
        HTML_PREFERER.handle(request);
        check(accMediaTypes, 0.2f, 0.6f, 1f, 0.8f, 0.7f);
    }

    public void test4() {
        Request request = new Request();
        List<Preference<MediaType>> accMediaTypes = request.getClientInfo()
                .getAcceptedMediaTypes();
        addMediaTypePref(accMediaTypes, MediaType.APPLICATION_ALL, 0.2f);
        addMediaTypePref(accMediaTypes, MediaType.TEXT_HTML, 0.6f);
        addMediaTypePref(accMediaTypes, MediaType.APPLICATION_XML, 1f);
        addMediaTypePref(accMediaTypes, MediaType.IMAGE_BMP, 0.999f);
        addMediaTypePref(accMediaTypes, MediaType.TEXT_PLAIN, 0.7f);
        HTML_PREFERER.handle(request);
        check(accMediaTypes, 0.2f, 1f, 0.999f, 0.998f, 0.7f);
    }
}

Reply via email to