diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java index 336b5a96e1e..db1c04f3d0c 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2010 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -864,9 +864,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator if (acceptedMediaTypes.isEmpty()) { acceptedMediaTypes = Collections.singletonList(MediaType.ALL); } - else { - Collections.sort(acceptedMediaTypes); - } + MediaType.sortBySpecificity(acceptedMediaTypes); HttpOutputMessage outputMessage = new ServletServerHttpResponse(webRequest.getResponse()); Class returnValueType = returnValue.getClass(); List allSupportedMediaTypes = new ArrayList(); diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java index e9b6a8a9248..76836575bf2 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2010 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,9 @@ import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; @@ -100,6 +103,8 @@ import org.springframework.web.util.WebUtils; */ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered { + private static final Log logger = LogFactory.getLog(ContentNegotiatingViewResolver.class); + private static final String ACCEPT_HEADER = "Accept"; private static final boolean jafPresent = @@ -267,9 +272,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport if (logger.isDebugEnabled()) { logger.debug("Requested media type is '" + mediaType + "' (based on filename '" + filename + "')"); } - List mediaTypes = new ArrayList(); - mediaTypes.add(mediaType); - return mediaTypes; + return Collections.singletonList(mediaType); } } if (this.favorParameter) { @@ -281,9 +284,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport logger.debug("Requested media type is '" + mediaType + "' (based on parameter '" + this.parameterName + "'='" + parameterValue + "')"); } - List mediaTypes = new ArrayList(); - mediaTypes.add(mediaType); - return mediaTypes; + return Collections.singletonList(mediaType); } } } @@ -291,6 +292,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport String acceptHeader = request.getHeader(ACCEPT_HEADER); if (StringUtils.hasText(acceptHeader)) { List mediaTypes = MediaType.parseMediaTypes(acceptHeader); + MediaType.sortBySpecificity(mediaTypes); if (logger.isDebugEnabled()) { logger.debug("Requested media types are " + mediaTypes + " (based on Accept header)"); } @@ -298,6 +300,9 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport } } if (this.defaultContentType != null) { + if (logger.isDebugEnabled()) { + logger.debug("Requested media types is " + defaultContentType + " (based on defaultContentType property)"); + } return Collections.singletonList(this.defaultContentType); } else { @@ -348,10 +353,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport ServletRequestAttributes servletAttrs = (ServletRequestAttributes) attrs; List requestedMediaTypes = getMediaTypes(servletAttrs.getRequest()); - if (requestedMediaTypes.size() > 1) { - // avoid sorting attempt for empty list and singleton list - Collections.sort(requestedMediaTypes); - } List candidateViews = new ArrayList(); for (ViewResolver viewResolver : this.viewResolvers) { @@ -364,32 +365,44 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport candidateViews.addAll(this.defaultViews); } - SortedMap views = new TreeMap(); - for (View candidateView : candidateViews) { - String contentType = candidateView.getContentType(); - if (StringUtils.hasText(contentType)) { - MediaType viewMediaType = MediaType.parseMediaType(contentType); - for (MediaType requestedMediaType : requestedMediaTypes) { - if (requestedMediaType.includes(viewMediaType)) { - if (!views.containsKey(requestedMediaType)) { - views.put(requestedMediaType, candidateView); - break; - } + MediaType bestRequestedMediaType = null; + View bestView = null; + for (MediaType requestedMediaType : requestedMediaTypes) { + for (View candidateView : candidateViews) { + if (StringUtils.hasText(candidateView.getContentType())) { + MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType()); + if (requestedMediaType.includes(candidateContentType)) { + bestRequestedMediaType = requestedMediaType; + bestView = candidateView; + break; } } } + if (bestView != null) { + break; + } } - if (!views.isEmpty()) { - MediaType mediaType = views.firstKey(); - View view = views.get(mediaType); + if (bestView != null) { if (logger.isDebugEnabled()) { - logger.debug("Returning [" + view + "] based on requested media type '" + mediaType + "'"); + logger.debug("Returning [" + bestView + "] based on requested media type '" + + bestRequestedMediaType + "'"); } - return view; + return bestView; } else { - return useNotAcceptableStatusCode ? new NotAcceptableView() : null; + if (useNotAcceptableStatusCode) { + if (logger.isDebugEnabled()) { + logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code"); + } + return NOT_ACCEPTABLE_VIEW; + } + else { + if (logger.isDebugEnabled()) { + logger.debug("No acceptable view found; returning null"); + } + return null; + } } } @@ -408,6 +421,9 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport // see if we can find the extended mime.types from the context-support module Resource mappingLocation = new ClassPathResource("org/springframework/mail/javamail/mime.types"); if (mappingLocation.exists()) { + if (logger.isTraceEnabled()) { + logger.trace("Loading Java Activation Framework FileTypeMap from " + mappingLocation); + } InputStream inputStream = null; try { inputStream = mappingLocation.getInputStream(); @@ -427,6 +443,9 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport } } } + if (logger.isTraceEnabled()) { + logger.trace("Loading default Java Activation Framework FileTypeMap"); + } return FileTypeMap.getDefaultFileTypeMap(); } @@ -436,7 +455,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport } } - private static class NotAcceptableView implements View { + private static final View NOT_ACCEPTABLE_VIEW = new View() { public String getContentType() { return null; @@ -446,5 +465,5 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport throws Exception { response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); } - } + }; } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java index 69614e25266..44fb24d6a5c 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2010 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -128,8 +128,8 @@ public class ContentNegotiatingViewResolverTests { expect(viewResolverMock1.resolveViewName(viewName, locale)).andReturn(viewMock1); expect(viewResolverMock2.resolveViewName(viewName, locale)).andReturn(viewMock2); - expect(viewMock1.getContentType()).andReturn("application/xml"); - expect(viewMock2.getContentType()).andReturn("text/html;charset=ISO-8859-1"); + expect(viewMock1.getContentType()).andReturn("application/xml").anyTimes(); + expect(viewMock2.getContentType()).andReturn("text/html;charset=ISO-8859-1").anyTimes(); replay(viewResolverMock1, viewResolverMock2, viewMock1, viewMock2); @@ -160,8 +160,8 @@ public class ContentNegotiatingViewResolverTests { expect(viewResolverMock1.resolveViewName(viewName, locale)).andReturn(viewMock1); expect(viewResolverMock2.resolveViewName(viewName, locale)).andReturn(viewMock2); - expect(viewMock1.getContentType()).andReturn("application/xml"); - expect(viewMock2.getContentType()).andReturn("text/html;charset=ISO-8859-1"); + expect(viewMock1.getContentType()).andReturn("application/xml").anyTimes(); + expect(viewMock2.getContentType()).andReturn("text/html;charset=ISO-8859-1").anyTimes(); replay(viewResolverMock1, viewResolverMock2, viewMock1, viewMock2); @@ -197,9 +197,9 @@ public class ContentNegotiatingViewResolverTests { expect(viewResolverMock1.resolveViewName(viewName, locale)).andReturn(viewMock1); expect(viewResolverMock2.resolveViewName(viewName, locale)).andReturn(viewMock2); - expect(viewMock1.getContentType()).andReturn("application/xml"); - expect(viewMock2.getContentType()).andReturn("text/html;charset=ISO-8859-1"); - expect(viewMock3.getContentType()).andReturn("application/json"); + expect(viewMock1.getContentType()).andReturn("application/xml").anyTimes(); + expect(viewMock2.getContentType()).andReturn("text/html;charset=ISO-8859-1").anyTimes(); + expect(viewMock3.getContentType()).andReturn("application/json").anyTimes(); replay(viewResolverMock1, viewResolverMock2, viewMock1, viewMock2, viewMock3); @@ -229,8 +229,8 @@ public class ContentNegotiatingViewResolverTests { expect(viewResolverMock1.resolveViewName(viewName, locale)).andReturn(viewMock1); expect(viewResolverMock2.resolveViewName(viewName, locale)).andReturn(viewMock2); - expect(viewMock1.getContentType()).andReturn("application/xml"); - expect(viewMock2.getContentType()).andReturn("text/html;charset=ISO-8859-1"); + expect(viewMock1.getContentType()).andReturn("application/xml").anyTimes(); + expect(viewMock2.getContentType()).andReturn("text/html;charset=ISO-8859-1").anyTimes(); replay(viewResolverMock1, viewResolverMock2, viewMock1, viewMock2); @@ -269,9 +269,9 @@ public class ContentNegotiatingViewResolverTests { expect(viewResolverMock1.resolveViewName(viewName, locale)).andReturn(viewMock1); expect(viewResolverMock2.resolveViewName(viewName, locale)).andReturn(viewMock2); - expect(viewMock1.getContentType()).andReturn("application/xml"); - expect(viewMock2.getContentType()).andReturn("text/html;charset=ISO-8859-1"); - expect(viewMock3.getContentType()).andReturn("application/json"); + expect(viewMock1.getContentType()).andReturn("application/xml").anyTimes(); + expect(viewMock2.getContentType()).andReturn("text/html;charset=ISO-8859-1").anyTimes(); + expect(viewMock3.getContentType()).andReturn("application/json").anyTimes(); replay(viewResolverMock1, viewResolverMock2, viewMock1, viewMock2, viewMock3); @@ -296,7 +296,7 @@ public class ContentNegotiatingViewResolverTests { Locale locale = Locale.ENGLISH; expect(viewResolverMock.resolveViewName(viewName, locale)).andReturn(viewMock); - expect(viewMock.getContentType()).andReturn(null); + expect(viewMock.getContentType()).andReturn(null).anyTimes(); replay(viewResolverMock, viewMock); @@ -321,7 +321,7 @@ public class ContentNegotiatingViewResolverTests { Locale locale = Locale.ENGLISH; expect(viewResolverMock.resolveViewName(viewName, locale)).andReturn(viewMock); - expect(viewMock.getContentType()).andReturn("application/pdf"); + expect(viewMock.getContentType()).andReturn("application/pdf").anyTimes(); replay(viewResolverMock, viewMock); @@ -347,7 +347,7 @@ public class ContentNegotiatingViewResolverTests { Locale locale = Locale.ENGLISH; expect(viewResolverMock.resolveViewName(viewName, locale)).andReturn(viewMock); - expect(viewMock.getContentType()).andReturn("application/pdf"); + expect(viewMock.getContentType()).andReturn("application/pdf").anyTimes(); replay(viewResolverMock, viewMock); diff --git a/org.springframework.web/src/main/java/org/springframework/http/MediaType.java b/org.springframework.web/src/main/java/org/springframework/http/MediaType.java index eba7faf97b1..e86624a3e2e 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/MediaType.java +++ b/org.springframework.web/src/main/java/org/springframework/http/MediaType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2010 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Comparator; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -40,10 +41,10 @@ import org.springframework.util.StringUtils; * * @author Arjen Poutsma * @author Juergen Hoeller - * @see HTTP 1.1 + * @see HTTP 1.1, section 3.7 * @since 3.0 */ -public class MediaType implements Comparable { +public class MediaType { public static final MediaType ALL = new MediaType("*", "*"); @@ -60,8 +61,9 @@ public class MediaType implements Comparable { private final Map parameters; /** - * Create a new {@link MediaType} for the given primary type.

The {@linkplain #getSubtype() subtype} is set to - * *, parameters empty. + * Create a new {@link MediaType} for the given primary type. + * + *

The {@linkplain #getSubtype() subtype} is set to *, parameters empty. * * @param type the primary type */ @@ -90,6 +92,17 @@ public class MediaType implements Comparable { this(type, subtype, Collections.singletonMap(PARAM_CHARSET, charSet.toString())); } + /** + * Create a new {@link MediaType} for the given type, subtype, and quality value. + * + * @param type the primary type + * @param subtype the subtype + * @param qualityValue the quality value + */ + public MediaType(String type, String subtype, double qualityValue) { + this(type, subtype, Collections.singletonMap(PARAM_QUALITY_FACTORY, Double.toString(qualityValue))); + } + /** * Create a new {@link MediaType} for the given type, subtype, and parameters. * @@ -100,10 +113,12 @@ public class MediaType implements Comparable { public MediaType(String type, String subtype, Map parameters) { Assert.hasText(type, "'type' must not be empty"); Assert.hasText(subtype, "'subtype' must not be empty"); + Assert.doesNotContain(type, "/", "'type' must not contain /"); + Assert.doesNotContain(subtype, "/", "'subtype' must not contain /"); this.type = type.toLowerCase(Locale.ENGLISH); this.subtype = subtype.toLowerCase(Locale.ENGLISH); if (!CollectionUtils.isEmpty(parameters)) { - this.parameters = new LinkedCaseInsensitiveMap(parameters.size()); + this.parameters = new LinkedCaseInsensitiveMap(parameters.size(), Locale.ENGLISH); this.parameters.putAll(parameters); } else { @@ -141,7 +156,7 @@ public class MediaType implements Comparable { * @return the character set; or null if not available */ public Charset getCharSet() { - String charSet = this.parameters.get(PARAM_CHARSET); + String charSet = getParameter(PARAM_CHARSET); return (charSet != null ? Charset.forName(charSet) : null); } @@ -151,7 +166,7 @@ public class MediaType implements Comparable { * @return the quality factory */ public double getQualityValue() { - String qualityFactory = this.parameters.get(PARAM_QUALITY_FACTORY); + String qualityFactory = getParameter(PARAM_QUALITY_FACTORY); return (qualityFactory != null ? Double.parseDouble(qualityFactory) : 1D); } @@ -166,8 +181,10 @@ public class MediaType implements Comparable { } /** - * Indicate whether this {@link MediaType} includes the given media type.

For instance, {@code text/*} includes - * {@code text/plain}, {@code text/html}, and {@code application/*+xml} includes {@code application/soap+xml}, etc. + * Indicate whether this {@link MediaType} includes the given media type. + * + *

For instance, {@code text/*} includes {@code text/plain}, {@code text/html}, and {@code application/*+xml} + * includes {@code application/soap+xml}, etc. * * @param other the reference media type with which to compare * @return true if this media type includes the given media type; false otherwise @@ -196,51 +213,6 @@ public class MediaType implements Comparable { return isWildcardType(); } - /** - * Compare this {@link MediaType} to another. Sorting with this comparator follows the general rule:

- * audio/basic < audio/* < */*
. That is, an explicit media type is sorted before an unspecific - * media type. Quality parameters are also considered, so that
audio/* < audio/*;q=0.7; - * audio/*;q=0.3
. - * - * @param other the media type to compare to - * @return a negative integer, zero, or a positive integer as this media type is less than, equal to, or greater than - * the specified media type - */ - public int compareTo(MediaType other) { - double qVal1 = this.getQualityValue(); - double qVal2 = other.getQualityValue(); - int qComp = Double.compare(qVal2, qVal1); - if (qComp != 0) { - return qComp; - } - else if (this.isWildcardType() && !other.isWildcardType()) { - return 1; - } - else if (other.isWildcardType() && !this.isWildcardType()) { - return -1; - } - else if (!this.getType().equals(other.getType())) { - return this.getType().compareTo(other.getType()); - } - else { // mediaType1.getType().equals(mediaType2.getType()) - if (this.isWildcardSubtype() && !other.isWildcardSubtype()) { - return 1; - } - else if (other.isWildcardSubtype() && !this.isWildcardSubtype()) { - return -1; - } - else if (!this.getSubtype().equals(other.getSubtype())) { - return this.getSubtype().compareTo(other.getSubtype()); - } - else { // mediaType2.getSubtype().equals(mediaType2.getSubtype()) - double quality1 = this.getQualityValue(); - double quality2 = other.getQualityValue(); - return Double.compare(quality2, quality1); - } - } - - } - @Override public boolean equals(Object other) { if (this == other) { @@ -339,8 +311,9 @@ public class MediaType implements Comparable { } /** - * Return a string representation of the given list of {@link MediaType} objects.

This method can be used to for an - * Accept or Content-Type header. + * Return a string representation of the given list of {@link MediaType} objects. + * + *

This method can be used to for an {@code Accept} or {@code Content-Type} header. * * @param mediaTypes the string to parse * @return the list of media types @@ -358,4 +331,78 @@ public class MediaType implements Comparable { return builder.toString(); } + /** + * Sorts the given list of {@link MediaType} objects by specificity. + * + *

Given two media types: + *

    + *
  1. if either media type has a {@linkplain #isWildcardType() wildcard type}, then the media type without the + * wildcard is ordered before the other.
  2. + *
  3. if the two media types have different {@linkplain #getType() types}, then they are considered equal and + * remain their current order.
  4. + *
  5. if either media type has a {@linkplain #isWildcardSubtype() wildcard subtype}, then the media type without + * the wildcard is sorted before the other.
  6. + *
  7. if the two media types have different {@linkplain #getSubtype() subtypes}, then they are considered equal + * and remain their current order.
  8. + *
  9. if the two media types have different {@linkplain #getQualityValue() quality value}, then the media type + * with the highest quality value is ordered before the other.
  10. + *
  11. if the two media types have a different amount of {@linkplain #getParameter(String) parameters}, then the + * media type with the most parameters is ordered before the other.
  12. + *
+ * + *

For example: + *

audio/basic < audio/* < */*
+ *
audio/* < audio/*;q=0.7; audio/*;q=0.3
+ *
audio/basic;level=1 < audio/basic
+ *
audio/basic == text/html
+ *
audio/basic == audio/wave
+ * + * @param mediaTypes the list of media types to be sorted + * @see HTTP 1.1, section 14.1 + */ + public static void sortBySpecificity(List mediaTypes) { + Assert.notNull(mediaTypes, "'mediaTypes' must not be null"); + if (mediaTypes.size() > 1) { + Collections.sort(mediaTypes, SPECIFICITY_COMPARATOR); + } + } + + static final Comparator SPECIFICITY_COMPARATOR = new Comparator() { + + public int compare(MediaType mediaType1, MediaType mediaType2) { + if (mediaType1.isWildcardType() && !mediaType2.isWildcardType()) { // */* < audio/* + return 1; + } + else if (mediaType2.isWildcardType() && !mediaType1.isWildcardType()) { // audio/* > */* + return -1; + } + else if (!mediaType1.getType().equals(mediaType2.getType())) { // audio/basic == text/html + return 0; + } + else { // mediaType1.getType().equals(mediaType2.getType()) + if (mediaType1.isWildcardSubtype() && !mediaType2.isWildcardSubtype()) { // audio/* < audio/basic + return 1; + } + else if (mediaType2.isWildcardSubtype() && !mediaType1.isWildcardSubtype()) { // audio/basic > audio/* + return -1; + } + else if (!mediaType1.getSubtype().equals(mediaType2.getSubtype())) { // audio/basic == audio/wave + return 0; + } + else { // mediaType2.getSubtype().equals(mediaType2.getSubtype()) + double quality1 = mediaType1.getQualityValue(); + double quality2 = mediaType2.getQualityValue(); + int qualityComparison = Double.compare(quality2, quality1); + if (qualityComparison != 0) { + return qualityComparison; // audio/*;q=0.7 < audio/*;q=0.3 + } else { + int paramsSize1 = mediaType1.parameters.size(); + int paramsSize2 = mediaType2.parameters.size(); + return (paramsSize2 < paramsSize1 ? -1 : (paramsSize2 == paramsSize1 ? 0 : 1)); // audio/basic;level=1 < audio/basic + } + } + } + } + }; + } diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java b/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java index a1a18f51852..626772f6e82 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2010 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -412,7 +412,7 @@ public class RestTemplate extends HttpAccessor implements RestOperations { public void doWithRequest(ClientHttpRequest request) throws IOException { if (responseType != null) { List allSupportedMediaTypes = new ArrayList(); - for (HttpMessageConverter messageConverter : getMessageConverters()) { + for (HttpMessageConverter messageConverter : getMessageConverters()) { if (messageConverter.canRead(responseType, null)) { List supportedMediaTypes = messageConverter.getSupportedMediaTypes(); for (MediaType supportedMediaType : supportedMediaTypes) { @@ -425,7 +425,7 @@ public class RestTemplate extends HttpAccessor implements RestOperations { } } if (!allSupportedMediaTypes.isEmpty()) { - Collections.sort(allSupportedMediaTypes); + MediaType.sortBySpecificity(allSupportedMediaTypes); request.getHeaders().setAccept(allSupportedMediaTypes); } } @@ -458,7 +458,7 @@ public class RestTemplate extends HttpAccessor implements RestOperations { public void doWithRequest(ClientHttpRequest httpRequest) throws IOException { super.doWithRequest(httpRequest); if (requestBody != null) { - Class requestType = requestBody.getClass(); + Class requestType = requestBody.getClass(); for (HttpMessageConverter messageConverter : getMessageConverters()) { if (messageConverter.canWrite(requestType, requestContentType)) { messageConverter.write(requestBody, requestContentType, httpRequest); diff --git a/org.springframework.web/src/test/java/org/springframework/http/MediaTypeTests.java b/org.springframework.web/src/test/java/org/springframework/http/MediaTypeTests.java index 01566219677..d1180c5b407 100644 --- a/org.springframework.web/src/test/java/org/springframework/http/MediaTypeTests.java +++ b/org.springframework.web/src/test/java/org/springframework/http/MediaTypeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2010 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Comparator; +import java.util.Random; import static org.junit.Assert.*; import org.junit.Test; @@ -49,11 +51,27 @@ public class MediaTypeTests { @Test public void testToString() throws Exception { - MediaType mediaType = new MediaType("text", "plain", Collections.singletonMap("q", "0.7")); + MediaType mediaType = new MediaType("text", "plain", 0.7); String result = mediaType.toString(); assertEquals("Invalid toString() returned", "text/plain;q=0.7", result); } + @Test(expected= IllegalArgumentException.class) + public void slashInType() { + new MediaType("text/plain"); + } + + @Test(expected= IllegalArgumentException.class) + public void slashInSubtype() { + new MediaType("text", "/"); + } + + @Test + public void getDefaultQualityValue() { + MediaType mediaType = new MediaType("text", "plain"); + assertEquals("Invalid quality value", 1, mediaType.getQualityValue(), 0D); + } + @Test public void parseMediaType() throws Exception { String s = "audio/*; q=0.2"; @@ -94,50 +112,104 @@ public class MediaTypeTests { } @Test - public void compareTo() throws Exception { + public void specificityComparator() throws Exception { MediaType audioBasic = new MediaType("audio", "basic"); + MediaType audioWave = new MediaType("audio", "wave"); MediaType audio = new MediaType("audio"); - MediaType audio03 = new MediaType("audio", "*", Collections.singletonMap("q", "0.3")); - MediaType audio07 = new MediaType("audio", "*", Collections.singletonMap("q", "0.7")); + MediaType audio03 = new MediaType("audio", "*", 0.3); + MediaType audio07 = new MediaType("audio", "*", 0.7); + MediaType audioBasicLevel = new MediaType("audio", "basic", Collections.singletonMap("level", "1")); + MediaType textHtml = new MediaType("text", "html"); MediaType all = MediaType.ALL; + Comparator comp = MediaType.SPECIFICITY_COMPARATOR; + // equal - assertEquals("Invalid comparison result", 0, audioBasic.compareTo(audioBasic)); - assertEquals("Invalid comparison result", 0, audio.compareTo(audio)); - assertEquals("Invalid comparison result", 0, audio07.compareTo(audio07)); + assertEquals("Invalid comparison result", 0, comp.compare(audioBasic,audioBasic)); + assertEquals("Invalid comparison result", 0, comp.compare(audio, audio)); + assertEquals("Invalid comparison result", 0, comp.compare(audio07, audio07)); + assertEquals("Invalid comparison result", 0, comp.compare(audio03, audio03)); + assertEquals("Invalid comparison result", 0, comp.compare(audioBasicLevel, audioBasicLevel)); // specific to unspecific - assertTrue("Invalid comparison result", audioBasic.compareTo(audio) < 0); - assertTrue("Invalid comparison result", audioBasic.compareTo(all) < 0); - assertTrue("Invalid comparison result", audio.compareTo(all) < 0); + assertTrue("Invalid comparison result", comp.compare(audioBasic, audio) < 0); + assertTrue("Invalid comparison result", comp.compare(audioBasic, all) < 0); + assertTrue("Invalid comparison result", comp.compare(audio, all) < 0); // unspecific to specific - assertTrue("Invalid comparison result", audio.compareTo(audioBasic) > 0); - assertTrue("Invalid comparison result", all.compareTo(audioBasic) > 0); - assertTrue("Invalid comparison result", all.compareTo(audio) > 0); + assertTrue("Invalid comparison result", comp.compare(audio, audioBasic) > 0); + assertTrue("Invalid comparison result", comp.compare(all, audioBasic) > 0); + assertTrue("Invalid comparison result", comp.compare(all, audio) > 0); // qualifiers - assertTrue("Invalid comparison result", audio.compareTo(audio07) < 0); - assertTrue("Invalid comparison result", audio07.compareTo(audio03) < 0); - assertTrue("Invalid comparison result", audio03.compareTo(all) > 0); + assertTrue("Invalid comparison result", comp.compare(audio, audio07) < 0); + assertTrue("Invalid comparison result", comp.compare(audio07, audio) > 0); + assertTrue("Invalid comparison result", comp.compare(audio07, audio03) < 0); + assertTrue("Invalid comparison result", comp.compare(audio03, audio07) > 0); + assertTrue("Invalid comparison result", comp.compare(audio03, all) < 0); + assertTrue("Invalid comparison result", comp.compare(all, audio03) > 0); + + // other parameters + assertTrue("Invalid comparison result", comp.compare(audioBasic, audioBasicLevel) > 0); + assertTrue("Invalid comparison result", comp.compare(audioBasicLevel, audioBasic) < 0); + + // different types + assertEquals("Invalid comparison result", 0, comp.compare(audioBasic, textHtml)); + assertEquals("Invalid comparison result", 0, comp.compare(textHtml, audioBasic)); + + // different subtypes + assertEquals("Invalid comparison result", 0, comp.compare(audioBasic, audioWave)); + assertEquals("Invalid comparison result", 0, comp.compare(audioWave, audioBasic)); + } + + @Test + public void sortBySpecificityRelated() { + MediaType audioBasic = new MediaType("audio", "basic"); + MediaType audio = new MediaType("audio"); + MediaType audio03 = new MediaType("audio", "*", 0.3); + MediaType audio07 = new MediaType("audio", "*", 0.7); + MediaType audioBasicLevel = new MediaType("audio", "basic", Collections.singletonMap("level", "1")); + MediaType all = MediaType.ALL; - // sort List expected = new ArrayList(); + expected.add(audioBasicLevel); expected.add(audioBasic); expected.add(audio); - expected.add(all); expected.add(audio07); expected.add(audio03); + expected.add(all); List result = new ArrayList(expected); + Random rnd = new Random(); + // shuffle & sort 10 times for (int i = 0; i < 10; i++) { - Collections.shuffle(result); - Collections.sort(result); + Collections.shuffle(result, rnd); + MediaType.sortBySpecificity(result); for (int j = 0; j < result.size(); j++) { - assertEquals("Invalid media type at " + j, expected.get(j), result.get(j)); + assertSame("Invalid media type at " + j, expected.get(j), result.get(j)); } } } + @Test + public void sortBySpecificityUnrelated() { + MediaType audioBasic = new MediaType("audio", "basic"); + MediaType audioWave = new MediaType("audio", "wave"); + MediaType textHtml = new MediaType("text", "html"); + + List expected = new ArrayList(); + expected.add(textHtml); + expected.add(audioBasic); + expected.add(audioWave); + + List result = new ArrayList(expected); + MediaType.sortBySpecificity(result); + + for (int i = 0; i < result.size(); i++) { + assertSame("Invalid media type at " + i, expected.get(i), result.get(i)); + } + + } + }