Skip to content

Loading…

Add media types to possible handler method params #232

Closed
wants to merge 1 commit into from

2 participants

@acogoluegnes

The requested media types are now valid arguments for Spring MVC
controller methods. They are computed by the
ContentNegotiationManager (so using the HTTP Accept header).
Controllers can ask for the requested media types by adding
a Collection- or a List-typed method argument.

Issue: SPR-9980

I have signed and agree to the terms of the SpringSource Individual
Contributor License Agreement.

@acogoluegnes acogoluegnes Add media types to possible handler method params
The requested media types are now valid arguments for Spring MVC
controller methods. They are computed by the
ContentNegotiationManager (so using the HTTP Accept header).
Controllers can ask for the requested media types by adding
a Collection<MediaType>- or a List<MediaType>-typed method argument.

Issue: SPR-9980
e0fa8ff
@rstoyanchev rstoyanchev was assigned
@rstoyanchev

After some further thought, although the proposed solution is perfectly valid, I'm not sure that the end result is generally useful and should be encouraged. A major assumption I'm making is that the injected list of media types will ultimately be used to determine the response media type and that requires a fairly complex algorithm that takes into consideration the order of the requested media types, their specificity, and any quality parameters. All that information must then be intersected against the media types the application can produce, etc. etc. Consider for example the kinds of headers browsers send.

That said I'm open to considering specific use cases where injecting the requested media types could be helpful. That would at least allow documenting well when it is a good idea and when it is not.

@rstoyanchev

Taking the case of stackoverflow question attached to the original JIRA ticket, I would recommend two methods, one with produces='application/json' and another with produces=application/xml'.
Admittedly this can lead to many additional methods if it occurs throughout. I've created a ticket to consider improving it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 13, 2013
  1. @acogoluegnes

    Add media types to possible handler method params

    acogoluegnes committed
    The requested media types are now valid arguments for Spring MVC
    controller methods. They are computed by the
    ContentNegotiationManager (so using the HTTP Accept header).
    Controllers can ask for the requested media types by adding
    a Collection<MediaType>- or a List<MediaType>-typed method argument.
    
    Issue: SPR-9980
View
4 spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java
@@ -157,6 +157,10 @@
* (Servlet-only, {@literal @MVC 3.1-only})
* for preparing a URL relative to the current request's host, port, scheme,
* context path, and the literal part of the servlet mapping.
+ * <li>{@link java.util.Collection} or {@link java.util.List} of
+ * {@link org.springframework.http.MediaType} for the requested media types, computed
+ * by the {@link org.springframework.web.accept.ContentNegotiationManager},
+ * using the Accept HTTP header.
* </ul>
*
* <p>The following return types are supported for handler methods:
View
95 ...n/java/org/springframework/web/method/annotation/RequestedMediaTypesArgumentResolver.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2002-2013 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.web.method.annotation;
+
+import java.lang.reflect.ParameterizedType;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.http.MediaType;
+import org.springframework.web.accept.ContentNegotiationStrategy;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+
+/**
+ * Resolves {@link Collection} or {@link List} of {@link MediaType}s method arguments.
+ *
+ * Delegates the resolution of requested media types to a {@link ContentNegotiationStrategy}.
+ * The commonly used implementation is {@link ContentNegociationManager}.
+ *
+ * @author Arnaud Cogoluègnes
+ * @since 3.2
+ */
+public class RequestedMediaTypesArgumentResolver implements HandlerMethodArgumentResolver {
+
+ private final ContentNegotiationStrategy contentNegotiationStrategy;
+
+ /**
+ * @param contentNegotiationStrategy
+ */
+ public RequestedMediaTypesArgumentResolver(
+ ContentNegotiationStrategy contentNegotiationStrategy) {
+ super();
+ this.contentNegotiationStrategy = contentNegotiationStrategy;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
+ */
+ @Override
+ public boolean supportsParameter(MethodParameter parameter) {
+ return isCollectionOfMediaTypes(parameter);
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument(org.springframework.core.MethodParameter, org.springframework.web.method.support.ModelAndViewContainer, org.springframework.web.context.request.NativeWebRequest, org.springframework.web.bind.support.WebDataBinderFactory)
+ */
+ @Override
+ public Object resolveArgument(MethodParameter parameter,
+ ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
+ WebDataBinderFactory binderFactory) throws Exception {
+ List<MediaType> mediaTypes = this.contentNegotiationStrategy.resolveMediaTypes(webRequest);
+ return mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes;
+ }
+
+ private boolean isCollectionOfMediaTypes(MethodParameter parameter) {
+ return isCollection(parameter) && isMediaTypeParameterized(parameter);
+ }
+
+ private boolean isCollection(MethodParameter parameter) {
+ return Collection.class.isAssignableFrom(parameter.getParameterType());
+ }
+
+ private boolean isMediaTypeParameterized(MethodParameter parameter) {
+ if(parameter.getGenericParameterType() instanceof ParameterizedType) {
+ ParameterizedType parameterizedType = (ParameterizedType) parameter.getGenericParameterType();
+ if(parameterizedType.getActualTypeArguments().length > 0) {
+ if(parameterizedType.getActualTypeArguments()[0] instanceof Class) {
+ Class<?> genericTypeOfTheCollection = (Class<?> )parameterizedType.getActualTypeArguments()[0];
+ return MediaType.class.isAssignableFrom(genericTypeOfTheCollection);
+ }
+ }
+ }
+ return false;
+ }
+
+}
View
110 ...a/org/springframework/web/method/annotation/RequestedMediaTypesArgumentResolverTests.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2002-2013 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.web.method.annotation;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.core.MethodParameter;
+import org.springframework.http.MediaType;
+import org.springframework.web.accept.ContentNegotiationStrategy;
+import org.springframework.web.context.request.NativeWebRequest;
+
+
+/**
+ * Test fixture with {@link org.springframework.web.method.annotation.RequestedMediaTypesArgumentResolver}.
+ * @author Arnaud Cogoluègnes
+ */
+public class RequestedMediaTypesArgumentResolverTests {
+
+ private RequestedMediaTypesArgumentResolver resolver;
+
+ private ContentNegotiationStrategy contentNegotiationStrategy;
+
+ @Before
+ public void setUp() throws Exception {
+ contentNegotiationStrategy = Mockito.mock(ContentNegotiationStrategy.class);
+ resolver = new RequestedMediaTypesArgumentResolver(contentNegotiationStrategy);
+ }
+
+ @Test public void supportParametersCollectionOfMediaTypes() throws Exception {
+ MethodParameter methodParameter = methodParameter("collectionOfMediaTypes", Collection.class);
+ Assert.assertTrue(resolver.supportsParameter(methodParameter));
+ }
+
+ @Test public void supportParametersCollectionOfStrings() throws Exception {
+ MethodParameter methodParameter = methodParameter("collectionOfStrings", Collection.class);
+ Assert.assertFalse(resolver.supportsParameter(methodParameter));
+ }
+
+ @Test public void supportParametersListOfMediaTypes() throws Exception {
+ MethodParameter methodParameter = methodParameter("listOfMediaTypes", List.class);
+ Assert.assertTrue(resolver.supportsParameter(methodParameter));
+ }
+
+ @Test public void supportParametersListOfStrings() throws Exception {
+ MethodParameter methodParameter = methodParameter("listOfStrings", List.class);
+ Assert.assertFalse(resolver.supportsParameter(methodParameter));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test public void resolveArgument() throws Exception {
+ Mockito.when(contentNegotiationStrategy.resolveMediaTypes(Mockito.any(NativeWebRequest.class)))
+ .thenReturn(Arrays.asList(MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML));
+
+ List<MediaType> mediaTypes = (List<MediaType>) resolver.resolveArgument(null, null, null, null);
+ Assert.assertEquals(2,mediaTypes.size());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test public void resolveArgumentEmptyNegociation() throws Exception {
+ Mockito.when(contentNegotiationStrategy.resolveMediaTypes(Mockito.any(NativeWebRequest.class)))
+ .thenReturn(new ArrayList<MediaType>());
+
+ List<MediaType> mediaTypes = (List<MediaType>) resolver.resolveArgument(null, null, null, null);
+ Assert.assertEquals(1,mediaTypes.size());
+ Assert.assertEquals(MediaType.ALL,mediaTypes.get(0));
+
+ }
+
+ private MethodParameter methodParameter(String methodName,Class<?> paramClass) throws Exception {
+ Method method = getClass().getDeclaredMethod(methodName, paramClass);
+ MethodParameter methodParam = new MethodParameter(method,0);
+ return methodParam;
+ }
+
+
+ @SuppressWarnings("unused")
+ private void collectionOfMediaTypes(Collection<MediaType> mediaTypes) { }
+
+ @SuppressWarnings("unused")
+ private void collectionOfStrings(Collection<String> mediaTypes) { }
+
+ @SuppressWarnings("unused")
+ private void listOfMediaTypes(List<MediaType> mediaTypes) { }
+
+ @SuppressWarnings("unused")
+ private void listOfStrings(List<String> mediaTypes) { }
+
+}
View
2 ...a/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java
@@ -82,6 +82,7 @@
import org.springframework.web.method.annotation.RequestHeaderMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver;
+import org.springframework.web.method.annotation.RequestedMediaTypesArgumentResolver;
import org.springframework.web.method.annotation.SessionAttributesHandler;
import org.springframework.web.method.annotation.SessionStatusMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
@@ -526,6 +527,7 @@ public void afterPropertiesSet() {
resolvers.add(new ErrorsMethodArgumentResolver());
resolvers.add(new SessionStatusMethodArgumentResolver());
resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
+ resolvers.add(new RequestedMediaTypesArgumentResolver(contentNegotiationManager));
// Custom arguments
if (getCustomArgumentResolvers() != null) {
View
18 ...ework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapterIntegrationTests.java
@@ -44,18 +44,19 @@
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
-import org.springframework.tests.sample.beans.TestBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.mock.web.test.MockMultipartFile;
import org.springframework.mock.web.test.MockMultipartHttpServletRequest;
+import org.springframework.tests.sample.beans.TestBean;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
@@ -143,13 +144,14 @@ public void handle() throws Exception {
Class<?>[] parameterTypes = new Class<?>[] { int.class, String.class, String.class, String.class, Map.class,
Date.class, Map.class, String.class, String.class, TestBean.class, Errors.class, TestBean.class,
Color.class, HttpServletRequest.class, HttpServletResponse.class, User.class, OtherUser.class,
- Model.class, UriComponentsBuilder.class };
+ Model.class, UriComponentsBuilder.class,List.class };
String datePattern = "yyyy.MM.dd";
String formattedDate = "2011.03.16";
Date date = new GregorianCalendar(2011, Calendar.MARCH, 16).getTime();
request.addHeader("Content-Type", "text/plain; charset=utf-8");
+ request.addHeader("Accept",MediaType.APPLICATION_JSON_VALUE+","+MediaType.APPLICATION_XML_VALUE);
request.addHeader("header", "headerValue");
request.addHeader("anotherHeader", "anotherHeaderValue");
request.addParameter("datePattern", datePattern);
@@ -210,6 +212,12 @@ public void handle() throws Exception {
assertEquals(OtherUser.class, model.get("otherUser").getClass());
assertEquals(new URI("http://localhost/contextPath/main/path"), model.get("url"));
+
+ @SuppressWarnings("unchecked")
+ List<MediaType> requestedMediaTypes = (List<MediaType>) model.get("requestedMediaTypes");
+ assertEquals(2,requestedMediaTypes.size());
+ assertEquals(MediaType.APPLICATION_JSON,requestedMediaTypes.get(0));
+ assertEquals(MediaType.APPLICATION_XML,requestedMediaTypes.get(1));
}
@Test
@@ -343,14 +351,16 @@ public String handle(
User user,
@ModelAttribute OtherUser otherUser,
Model model,
- UriComponentsBuilder builder) throws Exception {
+ UriComponentsBuilder builder,
+ List<MediaType> requestedMediaTypes) throws Exception {
model.addAttribute("cookie", cookie).addAttribute("pathvar", pathvar).addAttribute("header", header)
.addAttribute("systemHeader", systemHeader).addAttribute("headerMap", headerMap)
.addAttribute("dateParam", dateParam).addAttribute("paramMap", paramMap)
.addAttribute("paramByConvention", paramByConvention).addAttribute("value", value)
.addAttribute("customArg", customArg).addAttribute(user)
- .addAttribute("url", builder.path("/path").build().toUri());
+ .addAttribute("url", builder.path("/path").build().toUri())
+ .addAttribute("requestedMediaTypes",requestedMediaTypes);
assertNotNull(request);
assertNotNull(response);
Something went wrong with that request. Please try again.