From dc0613f487d6927d1b98d413955229ee1b47e1e4 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 27 Nov 2009 13:23:15 +0000 Subject: [PATCH] HttpMessageConverter.supports() is split into canRead/canWrite. HttpMessageConverter.write() now allows for a specific content type. --- .../AnnotationMethodHandlerAdapter.java | 123 ++++---- .../ServletAnnotationControllerTests.java | 110 ++++--- .../AbstractHttpMessageConverter.java | 107 +++++-- .../BufferedImageHttpMessageConverter.java | 104 ++++--- .../ByteArrayHttpMessageConverter.java | 13 +- .../converter/FormHttpMessageConverter.java | 3 +- .../http/converter/HttpMessageConverter.java | 36 ++- .../converter/StringHttpMessageConverter.java | 18 +- .../MappingJacksonHttpMessageConverter.java | 11 +- .../xml/AbstractXmlHttpMessageConverter.java | 9 +- .../xml/SourceHttpMessageConverter.java | 21 +- .../support/HandlerMethodInvoker.java | 163 ++++++----- .../client/HttpMessageConverterExtractor.java | 13 +- .../web/client/RestTemplate.java | 275 ++++++++---------- ...ufferedImageHttpMessageConverterTests.java | 31 +- .../ByteArrayHttpMessageConverterTests.java | 6 +- .../FormHttpMessageConverterTests.java | 6 +- .../StringHttpMessageConverterTests.java | 14 +- ...ppingJacksonHttpMessageConverterTests.java | 8 +- .../MarshallingHttpMessageConverterTests.java | 2 +- .../xml/SourceHttpMessageConverterTests.java | 2 +- .../web/client/RestTemplateTests.java | 71 ++--- 22 files changed, 628 insertions(+), 518 deletions(-) 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 13b81c0fa19..e7ba282e2a5 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 @@ -121,22 +121,24 @@ import org.springframework.web.util.WebUtils; * * @author Juergen Hoeller * @author Arjen Poutsma - * @since 2.5 * @see #setPathMatcher * @see #setMethodNameResolver * @see #setWebBindingInitializer * @see #setSessionAttributeStore + * @since 2.5 */ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implements HandlerAdapter, BeanFactoryAware { /** * Log category to use when no mapped handler is found for a request. + * * @see #pageNotFoundLogger */ public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound"; /** * Additional logger to use when no mapped handler is found for a request. + * * @see #PAGE_NOT_FOUND_LOG_CATEGORY */ protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY); @@ -162,7 +164,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen private ModelAndViewResolver[] customModelAndViewResolvers; private HttpMessageConverter[] messageConverters = - new HttpMessageConverter[] {new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(), + new HttpMessageConverter[]{new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(), new FormHttpMessageConverter(), new SourceHttpMessageConverter()}; private ConfigurableBeanFactory beanFactory; @@ -172,17 +174,16 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen private final Map, ServletHandlerMethodResolver> methodResolverCache = new ConcurrentHashMap, ServletHandlerMethodResolver>(); - public AnnotationMethodHandlerAdapter() { // no restriction of HTTP methods by default super(false); } - /** * Set if URL lookup should always use the full path within the current servlet context. Else, the path within the * current servlet mapping is used if applicable (that is, in the case of a ".../*" servlet mapping in web.xml). *

Default is "false". + * * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath */ public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { @@ -193,6 +194,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen * Set if context path and request URI should be URL-decoded. Both are returned undecoded by the Servlet API, in * contrast to the servlet path.

Uses either the request encoding or the default encoding according to the Servlet * spec (ISO-8859-1). + * * @see org.springframework.web.util.UrlPathHelper#setUrlDecode */ public void setUrlDecode(boolean urlDecode) { @@ -211,6 +213,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen /** * Set the PathMatcher implementation to use for matching URL paths against registered URL patterns. Default is * AntPathMatcher. + * * @see org.springframework.util.AntPathMatcher */ public void setPathMatcher(PathMatcher pathMatcher) { @@ -220,8 +223,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen /** * Set the MethodNameResolver to use for resolving default handler methods (carrying an empty - * @RequestMapping annotation). - *

Will only kick in when the handler method cannot be resolved uniquely + * @RequestMapping annotation).

Will only kick in when the handler method cannot be resolved uniquely * through the annotation metadata already. */ public void setMethodNameResolver(MethodNameResolver methodNameResolver) { @@ -229,16 +231,15 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen } /** - * Specify a WebBindingInitializer which will apply pre-configured configuration to every - * DataBinder that this controller uses. + * Specify a WebBindingInitializer which will apply pre-configured configuration to every DataBinder that this + * controller uses. */ public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) { this.webBindingInitializer = webBindingInitializer; } /** - * Specify the strategy to store session attributes with. - *

Default is {@link org.springframework.web.bind.support.DefaultSessionAttributeStore}, + * Specify the strategy to store session attributes with.

Default is {@link org.springframework.web.bind.support.DefaultSessionAttributeStore}, * storing session attributes in the HttpSession, using the same attribute name as in the model. */ public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) { @@ -248,10 +249,10 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen /** * Cache content produced by @SessionAttributes annotated handlers for the given number of seconds. - * Default is 0, preventing caching completely. - *

In contrast to the "cacheSeconds" property which will apply to all + * Default is 0, preventing caching completely.

In contrast to the "cacheSeconds" property which will apply to all * general handlers (but not to @SessionAttributes annotated handlers), this setting will apply to * @SessionAttributes annotated handlers only. + * * @see #setCacheSeconds * @see org.springframework.web.bind.annotation.SessionAttributes */ @@ -260,17 +261,15 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen } /** - * Set if controller execution should be synchronized on the session, to serialize - * parallel invocations from the same client. - *

More specifically, the execution of each handler method will get synchronized if this - * flag is "true". The best available session mutex will be used for the synchronization; - * ideally, this will be a mutex exposed by HttpSessionMutexListener. - *

The session mutex is guaranteed to be the same object during the entire lifetime of the - * session, available under the key defined by the SESSION_MUTEX_ATTRIBUTE constant. - * It serves as a safe reference to synchronize on for locking on the current session. - *

In many cases, the HttpSession reference itself a safe mutex as well, since it will - * always be the same object reference for the same active logical session. However, this is - * not guaranteed across different servlet containers; the only 100% safe way is a session mutex. + * Set if controller execution should be synchronized on the session, to serialize parallel invocations from the same + * client.

More specifically, the execution of each handler method will get synchronized if this flag is "true". The + * best available session mutex will be used for the synchronization; ideally, this will be a mutex exposed by + * HttpSessionMutexListener.

The session mutex is guaranteed to be the same object during the entire lifetime of the + * session, available under the key defined by the SESSION_MUTEX_ATTRIBUTE constant. It serves as a safe + * reference to synchronize on for locking on the current session.

In many cases, the HttpSession reference itself a + * safe mutex as well, since it will always be the same object reference for the same active logical session. However, + * this is not guaranteed across different servlet containers; the only 100% safe way is a session mutex. + * * @see org.springframework.web.util.HttpSessionMutexListener * @see org.springframework.web.util.WebUtils#getSessionMutex(javax.servlet.http.HttpSession) */ @@ -280,40 +279,41 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen /** * Set the ParameterNameDiscoverer to use for resolving method parameter names if needed (e.g. for default attribute - * names). - *

Default is a {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer}. + * names).

Default is a {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer}. */ public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { this.parameterNameDiscoverer = parameterNameDiscoverer; } /** - * Set a custom WebArgumentResolvers to use for special method parameter types. Such a custom WebArgumentResolver will kick - * in first, having a chance to resolve an argument value before the standard argument handling kicks in. + * Set a custom WebArgumentResolvers to use for special method parameter types. Such a custom WebArgumentResolver will + * kick in first, having a chance to resolve an argument value before the standard argument handling kicks in. */ public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) { this.customArgumentResolvers = new WebArgumentResolver[]{argumentResolver}; } /** - * Set one or more custom WebArgumentResolvers to use for special method parameter types. Any such custom WebArgumentResolver - * will kick in first, having a chance to resolve an argument value before the standard argument handling kicks in. + * Set one or more custom WebArgumentResolvers to use for special method parameter types. Any such custom + * WebArgumentResolver will kick in first, having a chance to resolve an argument value before the standard argument + * handling kicks in. */ public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) { this.customArgumentResolvers = argumentResolvers; } /** - * Set a custom ModelAndViewResolvers to use for special method return types. Such a custom ModelAndViewResolver will kick - * in first, having a chance to resolve an return value before the standard ModelAndView handling kicks in. + * Set a custom ModelAndViewResolvers to use for special method return types. Such a custom ModelAndViewResolver will + * kick in first, having a chance to resolve an return value before the standard ModelAndView handling kicks in. */ public void setCustomModelAndViewResolver(ModelAndViewResolver customModelAndViewResolver) { this.customModelAndViewResolvers = new ModelAndViewResolver[]{customModelAndViewResolver}; } /** - * Set one or more custom ModelAndViewResolvers to use for special method return types. Any such custom ModelAndViewResolver - * will kick in first, having a chance to resolve an return value before the standard ModelAndView handling kicks in. + * Set one or more custom ModelAndViewResolvers to use for special method return types. Any such custom + * ModelAndViewResolver will kick in first, having a chance to resolve an return value before the standard ModelAndView + * handling kicks in. */ public void setCustomModelAndViewResolvers(ModelAndViewResolver[] customModelAndViewResolvers) { this.customModelAndViewResolvers = customModelAndViewResolvers; @@ -334,7 +334,6 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen } } - public boolean supports(Object handler) { return getMethodResolver(handler).hasHandlerMethods(); } @@ -386,9 +385,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen return -1; } - /** - * Build a HandlerMethodResolver for the given handler type. - */ + /** Build a HandlerMethodResolver for the given handler type. */ private ServletHandlerMethodResolver getMethodResolver(Object handler) { Class handlerClass = ClassUtils.getUserClass(handler); ServletHandlerMethodResolver resolver = this.methodResolverCache.get(handlerClass); @@ -399,10 +396,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen return resolver; } - - /** - * Servlet-specific subclass of {@link HandlerMethodResolver}. - */ + /** Servlet-specific subclass of {@link HandlerMethodResolver}. */ private class ServletHandlerMethodResolver extends HandlerMethodResolver { private ServletHandlerMethodResolver(Class handlerType) { @@ -459,7 +453,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen else { for (RequestMethod requestMethod : mappingInfo.methods) { allowedMethods.add(requestMethod.toString()); - } + } } } if (match) { @@ -521,17 +515,10 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen /** * Determines the matched pattern for the given methodLevelPattern and path. * - *

Uses the following algorithm: - *

    - *
  1. If there is a type-level mapping with path information, it is - * {@linkplain PathMatcher#combine(String, String) combined} with the method-level pattern. - *
  2. If there is a {@linkplain HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE best matching pattern} in the - * request, it is combined with the method-level pattern. - *
  3. Otherwise, - * @param methodLevelPattern - * @param lookupPath - * @param request - * @return + *

    Uses the following algorithm:

    1. If there is a type-level mapping with path information, it is {@linkplain + * PathMatcher#combine(String, String) combined} with the method-level pattern.
    2. If there is a {@linkplain + * HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE best matching pattern} in the request, it is combined with the + * method-level pattern.
    3. Otherwise, */ private String getMatchedPattern(String methodLevelPattern, String lookupPath, HttpServletRequest request) { if (hasTypeLevelMapping() && (!ObjectUtils.isEmpty(getTypeLevelMapping().value()))) { @@ -550,7 +537,8 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen String bestMatchingPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); if (StringUtils.hasText(bestMatchingPattern)) { String combinedPattern = pathMatcher.combine(bestMatchingPattern, methodLevelPattern); - if (!combinedPattern.equals(bestMatchingPattern) && (isPathMatchInternal(combinedPattern, lookupPath))) { + if (!combinedPattern.equals(bestMatchingPattern) && + (isPathMatchInternal(combinedPattern, lookupPath))) { return combinedPattern; } } @@ -610,10 +598,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen } } - - /** - * Servlet-specific subclass of {@link HandlerMethodInvoker}. - */ + /** Servlet-specific subclass of {@link HandlerMethodInvoker}. */ private class ServletHandlerMethodInvoker extends HandlerMethodInvoker { private boolean responseArgumentUsed = false; @@ -729,7 +714,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen Object returnValue, ExtendedModelMap implicitModel, ServletWebRequest webRequest) throws Exception { - + ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod, ResponseStatus.class); if (responseStatusAnn != null) { HttpStatus responseStatus = responseStatusAnn.value(); @@ -797,7 +782,8 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen } @SuppressWarnings("unchecked") - private void handleResponseBody(Object returnValue, ServletWebRequest webRequest) throws ServletException, IOException { + private void handleResponseBody(Object returnValue, ServletWebRequest webRequest) + throws ServletException, IOException { HttpInputMessage inputMessage = new ServletServerHttpRequest(webRequest.getRequest()); List acceptedMediaTypes = inputMessage.getHeaders().getAccept(); if (acceptedMediaTypes.isEmpty()) { @@ -809,16 +795,11 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen if (messageConverters != null) { for (HttpMessageConverter messageConverter : messageConverters) { allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); - if (messageConverter.supports(returnValueType)) { - for (Object o : messageConverter.getSupportedMediaTypes()) { - MediaType supportedMediaType = (MediaType) o; - for (MediaType acceptedMediaType : acceptedMediaTypes) { - if (acceptedMediaType.includes(supportedMediaType)) { - messageConverter.write(returnValue, outputMessage); - this.responseArgumentUsed = true; - return; - } - } + for (MediaType acceptedMediaType : acceptedMediaTypes) { + if (messageConverter.canWrite(returnValueType, acceptedMediaType)) { + messageConverter.write(returnValue, null, outputMessage); + this.responseArgumentUsed = true; + return; } } } @@ -827,7 +808,6 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen } } - static class RequestMappingInfo { String[] paths = new String[0]; @@ -864,7 +844,6 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen } } - /** * Comparator capable of sorting {@link RequestMappingInfo}s (RHIs) so that sorting a list with this comparator will * result in:
      • RHIs with {@linkplain RequestMappingInfo#matchedPaths better matched paths} take prescedence diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java index 26e8760e50a..25d06cdae35 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java @@ -30,12 +30,12 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.Iterator; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; @@ -52,10 +52,10 @@ import org.junit.Test; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.aop.interceptor.SimpleTraceInterceptor; import org.springframework.aop.support.DefaultPointcutAdvisor; +import org.springframework.beans.BeansException; import org.springframework.beans.DerivedTestBean; import org.springframework.beans.ITestBean; import org.springframework.beans.TestBean; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; @@ -63,14 +63,16 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.propertyeditors.CustomDateEditor; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.HttpHeaders; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletConfig; @@ -79,9 +81,9 @@ import org.springframework.stereotype.Controller; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.ui.ModelMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.SerializationTestUtils; import org.springframework.util.StringUtils; -import org.springframework.util.MultiValueMap; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.validation.FieldError; @@ -445,7 +447,8 @@ public class ServletAnnotationControllerTests { DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); autoProxyCreator.setBeanFactory(wac.getBeanFactory()); wac.getBeanFactory().addBeanPostProcessor(autoProxyCreator); - wac.getBeanFactory().registerSingleton("advisor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); + wac.getBeanFactory() + .registerSingleton("advisor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); wac.refresh(); return wac; } @@ -639,8 +642,8 @@ public class ServletAnnotationControllerTests { servlet.service(request, response); assertEquals("mySurpriseView", response.getContentAsString()); - MyParameterDispatchingController deserialized = (MyParameterDispatchingController) - SerializationTestUtils.serializeAndDeserialize(servlet.getWebApplicationContext().getBean("controller")); + MyParameterDispatchingController deserialized = (MyParameterDispatchingController) SerializationTestUtils + .serializeAndDeserialize(servlet.getWebApplicationContext().getBean("controller")); assertNotNull(deserialized.request); assertNotNull(deserialized.session); } @@ -947,7 +950,23 @@ public class ServletAnnotationControllerTests { @Test public void responseBodyNoAcceptableMediaType() throws ServletException, IOException { - initServlet(RequestBodyController.class); + @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override + protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { + GenericWebApplicationContext wac = new GenericWebApplicationContext(); + wac.registerBeanDefinition("controller", new RootBeanDefinition(RequestBodyController.class)); + RootBeanDefinition converterDef = new RootBeanDefinition(StringHttpMessageConverter.class); + converterDef.getPropertyValues().add("supportedMediaTypes", new MediaType("text", "plain")); + RootBeanDefinition adapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class); + StringHttpMessageConverter converter = new StringHttpMessageConverter(); + converter.setSupportedMediaTypes(Collections.singletonList(new MediaType("text", "plain"))); + adapterDef.getPropertyValues().add("messageConverters", converter); + wac.registerBeanDefinition("handlerAdapter", adapterDef); + wac.refresh(); + return wac; + } + }; + servlet.init(new MockServletConfig()); MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/something"); String requestBody = "Hello World"; @@ -975,7 +994,19 @@ public class ServletAnnotationControllerTests { @Test public void unsupportedRequestBody() throws ServletException, IOException { - initServlet(RequestBodyController.class); + @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override + protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { + GenericWebApplicationContext wac = new GenericWebApplicationContext(); + wac.registerBeanDefinition("controller", new RootBeanDefinition(RequestBodyController.class)); + RootBeanDefinition adapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class); + adapterDef.getPropertyValues().add("messageConverters", new ByteArrayHttpMessageConverter()); + wac.registerBeanDefinition("handlerAdapter", adapterDef); + wac.refresh(); + return wac; + } + }; + servlet.init(new MockServletConfig()); MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/something"); String requestBody = "Hello World"; @@ -1061,11 +1092,9 @@ public class ServletAnnotationControllerTests { @Override protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { GenericWebApplicationContext wac = new GenericWebApplicationContext(); - wac.registerBeanDefinition("controller", - new RootBeanDefinition(ModelAndViewResolverController.class)); + wac.registerBeanDefinition("controller", new RootBeanDefinition(ModelAndViewResolverController.class)); RootBeanDefinition adapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class); - adapterDef.getPropertyValues() - .add("customModelAndViewResolver", new MyModelAndViewResolver()); + adapterDef.getPropertyValues().add("customModelAndViewResolver", new MyModelAndViewResolver()); wac.registerBeanDefinition("handlerAdapter", adapterDef); wac.refresh(); return wac; @@ -1086,7 +1115,7 @@ public class ServletAnnotationControllerTests { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test"); request.setCookies(new Cookie("date", "2008-11-18")); - MockHttpServletResponse response = new MockHttpServletResponse(); + MockHttpServletResponse response = new MockHttpServletResponse(); servlet.service(request, response); assertEquals("test-108", response.getContentAsString()); } @@ -1096,7 +1125,7 @@ public class ServletAnnotationControllerTests { initServlet(AmbiguousParamsController.class); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test"); - MockHttpServletResponse response = new MockHttpServletResponse(); + MockHttpServletResponse response = new MockHttpServletResponse(); servlet.service(request, response); assertEquals("noParams", response.getContentAsString()); @@ -1116,7 +1145,6 @@ public class ServletAnnotationControllerTests { servlet.service(request, response); } - @Test public void requestParamMap() throws Exception { initServlet(RequestParamMapController.class); @@ -1229,11 +1257,9 @@ public class ServletAnnotationControllerTests { assertEquals("create", response.getContentAsString()); } - - /* - * Controllers - */ + * Controllers + */ @RequestMapping("/myPath.do") private static class MyController extends AbstractController { @@ -1470,7 +1496,8 @@ public class ServletAnnotationControllerTests { @SuppressWarnings("unused") @ModelAttribute("myCommand") private ValidTestBean createTestBean(@RequestParam T defaultName, - Map model, @RequestParam Date date) { + Map model, + @RequestParam Date date) { model.put("myKey", "myOriginalValue"); ValidTestBean tb = new ValidTestBean(); tb.setName(defaultName.getClass().getSimpleName() + ":" + defaultName.toString()); @@ -1740,9 +1767,10 @@ public class ServletAnnotationControllerTests { } List testBeans = (List) model.get("testBeanList"); if (errors.hasFieldErrors("age")) { - response.getWriter().write(viewName + "-" + tb.getName() + "-" + - errors.getFieldError("age").getCode() + "-" + testBeans.get(0).getName() + "-" + - model.get("myKey") + (model.containsKey("yourKey") ? "-" + model.get("yourKey") : "")); + response.getWriter() + .write(viewName + "-" + tb.getName() + "-" + errors.getFieldError("age").getCode() + + "-" + testBeans.get(0).getName() + "-" + model.get("myKey") + + (model.containsKey("yourKey") ? "-" + model.get("yourKey") : "")); } else { response.getWriter().write(viewName + "-" + tb.getName() + "-" + tb.getAge() + "-" + @@ -1760,6 +1788,7 @@ public class ServletAnnotationControllerTests { public String getContentType() { return null; } + public void render(Map model, HttpServletRequest request, HttpServletResponse response) { request.getSession().setAttribute("model", model); } @@ -1787,6 +1816,7 @@ public class ServletAnnotationControllerTests { @Retention(RetentionPolicy.RUNTIME) @Controller public @interface MyControllerAnnotation { + } @MyControllerAnnotation @@ -1835,8 +1865,8 @@ public class ServletAnnotationControllerTests { @RequestMapping("/myPath.do") public void myHandle(@RequestParam(value = "id", defaultValue = "${myKey}") String id, @RequestHeader(defaultValue = "#{systemProperties.myHeader}") String header, - @Value("#{request.contextPath}") String contextPath, HttpServletResponse response) - throws IOException { + @Value("#{request.contextPath}") String contextPath, + HttpServletResponse response) throws IOException { response.getWriter().write(String.valueOf(id) + "-" + String.valueOf(header) + "-" + contextPath); } } @@ -1873,7 +1903,6 @@ public class ServletAnnotationControllerTests { } } - @Controller public static class PathOrderingController { @@ -1888,7 +1917,6 @@ public class ServletAnnotationControllerTests { } } - @Controller public static class RequestBodyController { @@ -1901,7 +1929,11 @@ public class ServletAnnotationControllerTests { public static class MyMessageConverter implements HttpMessageConverter { - public boolean supports(Class clazz) { + public boolean canRead(Class clazz, MediaType mediaType) { + return true; + } + + public boolean canWrite(Class clazz, MediaType mediaType) { return true; } @@ -1914,7 +1946,7 @@ public class ServletAnnotationControllerTests { throw new HttpMessageNotReadableException("Could not read"); } - public void write(Object o, HttpOutputMessage outputMessage) + public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { throw new UnsupportedOperationException("Not implemented"); } @@ -1955,8 +1987,11 @@ public class ServletAnnotationControllerTests { public static class MyModelAndViewResolver implements ModelAndViewResolver { - public ModelAndView resolveModelAndView(Method handlerMethod, Class handlerType, - Object returnValue, ExtendedModelMap implicitModel, NativeWebRequest webRequest) { + public ModelAndView resolveModelAndView(Method handlerMethod, + Class handlerType, + Object returnValue, + ExtendedModelMap implicitModel, + NativeWebRequest webRequest) { if (returnValue instanceof MySpecialArg) { return new ModelAndView(new View() { public String getContentType() { @@ -2002,8 +2037,7 @@ public class ServletAnnotationControllerTests { } @RequestMapping(method = RequestMethod.GET) - public void handle(@CookieValue("date") Date date, Writer writer) - throws IOException { + public void handle(@CookieValue("date") Date date, Writer writer) throws IOException { assertEquals("Invalid path variable value", new Date(108, 10, 18), date); writer.write("test-" + date.getYear()); } @@ -2043,7 +2077,8 @@ public class ServletAnnotationControllerTests { } @RequestMapping("/multiValueMap") - public void multiValueMap(@RequestParam MultiValueMap params, Writer writer) throws IOException { + public void multiValueMap(@RequestParam MultiValueMap params, Writer writer) + throws IOException { for (Iterator>> it1 = params.entrySet().iterator(); it1.hasNext();) { Map.Entry> entry = it1.next(); writer.write(entry.getKey() + "=["); @@ -2078,7 +2113,8 @@ public class ServletAnnotationControllerTests { } @RequestMapping("/multiValueMap") - public void multiValueMap(@RequestHeader MultiValueMap headers, Writer writer) throws IOException { + public void multiValueMap(@RequestHeader MultiValueMap headers, Writer writer) + throws IOException { for (Iterator>> it1 = headers.entrySet().iterator(); it1.hasNext();) { Map.Entry> entry = it1.next(); writer.write(entry.getKey() + "=["); @@ -2104,5 +2140,5 @@ public class ServletAnnotationControllerTests { } - + } diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java index 726bec1aa4e..555631e2bd7 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java @@ -56,14 +56,22 @@ public abstract class AbstractHttpMessageConverter implements HttpMessageConv protected AbstractHttpMessageConverter() { } - /** Construct an {@code AbstractHttpMessageConverter} with one supported media type. */ + /** + * Construct an {@code AbstractHttpMessageConverter} with one supported media type. + * + * @param supportedMediaType the supported media type + */ protected AbstractHttpMessageConverter(MediaType supportedMediaType) { - this.supportedMediaTypes = Collections.singletonList(supportedMediaType); + setSupportedMediaTypes(Collections.singletonList(supportedMediaType)); } - /** Construct an {@code AbstractHttpMessageConverter} with multiple supported media type. */ + /** + * Construct an {@code AbstractHttpMessageConverter} with multiple supported media type. + * + * @param supportedMediaTypes the supported media types + */ protected AbstractHttpMessageConverter(MediaType... supportedMediaTypes) { - this.supportedMediaTypes = Arrays.asList(supportedMediaTypes); + setSupportedMediaTypes(Arrays.asList(supportedMediaTypes)); } /** Set the list of {@link MediaType} objects supported by this converter. */ @@ -77,8 +85,59 @@ public abstract class AbstractHttpMessageConverter implements HttpMessageConv } /** - * {@inheritDoc}

        This implementation simple delegates to {@link #readInternal(Class, HttpInputMessage)}. Future - * implementations might add some default behavior, however. + * {@inheritDoc} + * + *

        This implementation checks if the given class is {@linkplain #supports(Class) supported}, and if the {@linkplain + * #getSupportedMediaTypes() supported media types} {@linkplain MediaType#includes(MediaType) include} the given media + * type. + */ + public boolean canRead(Class clazz, MediaType mediaType) { + return supports(clazz) && isSupported(mediaType); + } + + /** + * {@inheritDoc} + * + *

        This implementation checks if the given class is {@linkplain #supports(Class) supported}, and if the {@linkplain + * #getSupportedMediaTypes() supported media types} {@linkplain MediaType#includes(MediaType) include} the given media + * type. + */ + public boolean canWrite(Class clazz, MediaType mediaType) { + return supports(clazz) && isSupported(mediaType); + } + + /** + * Returns true if any of the {@linkplain #setSupportedMediaTypes(List) supported media types} include the given media + * type. + * + * @param mediaType the media type + * @return true if the supported media types include the media type, or if the media type is {@code null} + */ + protected boolean isSupported(MediaType mediaType) { + if (mediaType == null) { + return true; + } + for (MediaType supportedMediaType : getSupportedMediaTypes()) { + if (supportedMediaType.includes(mediaType)) { + return true; + } + } + return false; + } + + /** + * Indicates whether the given class is supported by this converter. + * + * @param clazz the class to test for support + * @return true if supported; false otherwise + */ + protected abstract boolean supports(Class clazz); + + /** + * {@inheritDoc} + * + *

        This implementation simple delegates to {@link #readInternal(Class, HttpInputMessage)}. Future implementations + * might add some default behavior, however. */ public final T read(Class clazz, HttpInputMessage inputMessage) throws IOException { return readInternal(clazz, inputMessage); @@ -97,17 +156,22 @@ public abstract class AbstractHttpMessageConverter implements HttpMessageConv throws IOException, HttpMessageNotReadableException; /** - * {@inheritDoc}

        This implementation delegates to {@link #getContentType(Object)} and {@link - * #getContentLength(Object)}, and sets the corresponding headers on the output message. It then calls {@link - * #writeInternal(Object, HttpOutputMessage)}. + * {@inheritDoc} + * + *

        This implementation delegates to {@link #getDefaultContentType(Object)} if a content type was not provided, calls + * {@link #getContentLength}, and sets the corresponding headers on the output message. It then calls {@link + * #writeInternal}. */ - public final void write(T t, HttpOutputMessage outputMessage) throws IOException { + public final void write(T t, MediaType contentType, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { HttpHeaders headers = outputMessage.getHeaders(); - MediaType contentType = getContentType(t); + if (contentType == null) { + contentType = getDefaultContentType(t); + } if (contentType != null) { headers.setContentType(contentType); } - Long contentLength = getContentLength(t); + Long contentLength = getContentLength(t, contentType); if (contentLength != null) { headers.setContentLength(contentLength); } @@ -116,30 +180,35 @@ public abstract class AbstractHttpMessageConverter implements HttpMessageConv } /** - * Returns the content type for the given type.

        By default, this returns the first element of the {@link - * #setSupportedMediaTypes(List) supportedMediaTypes} property, if any. Can be overriden in subclasses. + * Returns the default content type for the given type. Called when {@link #write} is invoked without a specified + * content type parameter. + * + *

        By default, this returns the first element of the {@link #setSupportedMediaTypes(List) supportedMediaTypes} + * property, if any. Can be overriden in subclasses. * * @param t the type to return the content type for * @return the content type, or null if not known */ - protected MediaType getContentType(T t) { + protected MediaType getDefaultContentType(T t) { List mediaTypes = getSupportedMediaTypes(); return (!mediaTypes.isEmpty() ? mediaTypes.get(0) : null); } /** - * Returns the content length for the given type.

        By default, this returns null. Can be overriden in + * Returns the content length for the given type. + * + *

        By default, this returns {@code null}, meaning that the content length is unknown. Can be overriden in * subclasses. * * @param t the type to return the content length for - * @return the content length, or null if not known + * @return the content length, or {@code null} if not known */ - protected Long getContentLength(T t) { + protected Long getContentLength(T t, MediaType contentType) { return null; } /** - * Abstract template method that writes the actual body. Invoked from {@link #write(Object, HttpOutputMessage)}. + * Abstract template method that writes the actual body. Invoked from {@link #write}. * * @param t the object to write to the output message * @param outputMessage the message to write to diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java index bb170c91fa2..f6195456f64 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/BufferedImageHttpMessageConverter.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; import java.util.List; import javax.imageio.IIOImage; @@ -48,8 +49,7 @@ import org.springframework.util.Assert; *

        By default, this converter can read all media types that are supported by the {@linkplain * ImageIO#getReaderMIMETypes() registered image readers}, and writes using the media type of the first available * {@linkplain javax.imageio.ImageIO#getWriterMIMETypes() registered image writer}. This behavior can be overriden by - * setting the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes} and {@link - * #setContentType(org.springframework.http.MediaType) contentType} properties respectively. + * setting the #setContentType(org.springframework.http.MediaType) contentType} properties. * *

        If the {@link #setCacheDir(java.io.File) cacheDir} property is set to an existing directory, this converter will * cache image data. @@ -60,57 +60,50 @@ import org.springframework.util.Assert; * @author Arjen Poutsma * @since 3.0 */ -public class BufferedImageHttpMessageConverter extends AbstractHttpMessageConverter { +public class BufferedImageHttpMessageConverter implements HttpMessageConverter { - private MediaType contentType; + private List readableMediaTypes = new ArrayList(); + + private MediaType defaultContentType; private File cacheDir; public BufferedImageHttpMessageConverter() { String[] readerMediaTypes = ImageIO.getReaderMIMETypes(); - List supportedMediaTypes = new ArrayList(readerMediaTypes.length); for (String mediaType : readerMediaTypes) { - supportedMediaTypes.add(MediaType.parseMediaType(mediaType)); + readableMediaTypes.add(MediaType.parseMediaType(mediaType)); } - setSupportedMediaTypes(supportedMediaTypes); + String[] writerMediaTypes = ImageIO.getWriterMIMETypes(); if (writerMediaTypes.length > 0) { - contentType = MediaType.parseMediaType(writerMediaTypes[0]); + defaultContentType = MediaType.parseMediaType(writerMediaTypes[0]); } } /** - * Sets the {@link MediaType MediaTypes} supported for reading. + * Returns the default {@code Content-Type} to be used for writing. Called when {@link #write} is invoked without a + * specified content type parameter. * - * @throws IllegalArgumentException if the given media type is not supported by the Java Image I/O API + * @return the default content type */ - @Override - public void setSupportedMediaTypes(List supportedMediaTypes) { - Assert.notEmpty(supportedMediaTypes, "'supportedMediaTypes' must not be empty"); - for (MediaType supportedMediaType : supportedMediaTypes) { - Iterator imageReaders = ImageIO.getImageReadersByMIMEType(supportedMediaType.toString()); - if (!imageReaders.hasNext()) { - throw new IllegalArgumentException( - "MediaType [" + supportedMediaType + "] is not supported by the Java Image I/O API"); - } - } - super.setSupportedMediaTypes(supportedMediaTypes); + public MediaType getDefaultContentType() { + return defaultContentType; } /** - * Sets the {@code Content-Type} to be used for writing. + * Sets the default {@code Content-Type} to be used for writing. * * @throws IllegalArgumentException if the given content type is not supported by the Java Image I/O API */ - public void setContentType(MediaType contentType) { - Assert.notNull(contentType, "'contentType' must not be null"); - Iterator imageWriters = ImageIO.getImageWritersByMIMEType(contentType.toString()); + public void setDefaultContentType(MediaType defaultContentType) { + Assert.notNull(defaultContentType, "'contentType' must not be null"); + Iterator imageWriters = ImageIO.getImageWritersByMIMEType(defaultContentType.toString()); if (!imageWriters.hasNext()) { throw new IllegalArgumentException( - "ContentType [" + contentType + "] is not supported by the Java Image I/O API"); + "ContentType [" + defaultContentType + "] is not supported by the Java Image I/O API"); } - this.contentType = contentType; + this.defaultContentType = defaultContentType; } /** Sets the cache directory. If this property is set to an existing directory, this converter will cache image data. */ @@ -120,12 +113,46 @@ public class BufferedImageHttpMessageConverter extends AbstractHttpMessageConver this.cacheDir = cacheDir; } - public boolean supports(Class clazz) { - return BufferedImage.class.equals(clazz); + public boolean canRead(Class clazz, MediaType mediaType) { + if (BufferedImage.class.equals(clazz)) { + return isReadable(mediaType); + } + else { + return false; + } } - @Override - public BufferedImage readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException { + private boolean isReadable(MediaType mediaType) { + if (mediaType == null) { + return true; + } + Iterator imageReaders = ImageIO.getImageReadersByMIMEType(mediaType.toString()); + return imageReaders.hasNext(); + } + + public boolean canWrite(Class clazz, MediaType mediaType) { + if (BufferedImage.class.equals(clazz)) { + return isWritable(mediaType); + } + else { + return false; + } + } + + private boolean isWritable(MediaType mediaType) { + if (mediaType == null) { + return true; + } + Iterator imageWriters = ImageIO.getImageWritersByMIMEType(mediaType.toString()); + return imageWriters.hasNext(); + } + + public List getSupportedMediaTypes() { + return Collections.unmodifiableList(readableMediaTypes); + } + + public BufferedImage read(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { ImageInputStream imageInputStream = null; ImageReader imageReader = null; try { @@ -168,13 +195,14 @@ public class BufferedImageHttpMessageConverter extends AbstractHttpMessageConver } } - @Override - protected MediaType getContentType(BufferedImage image) { - return contentType; - } - - @Override - protected void writeInternal(BufferedImage image, HttpOutputMessage outputMessage) throws IOException { + public void write(BufferedImage image, MediaType contentType, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + if (contentType == null) { + contentType = getDefaultContentType(); + } + Assert.notNull(contentType, + "Count not determine Content-Type, set one using the 'defaultContentType' property"); + outputMessage.getHeaders().setContentType(contentType); ImageOutputStream imageOutputStream = null; ImageWriter imageWriter = null; try { diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java index 614765d3208..b5f33f5e40a 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/ByteArrayHttpMessageConverter.java @@ -29,8 +29,7 @@ import org.springframework.util.FileCopyUtils; * *

        By default, this converter supports all media types (*/*), and writes with a {@code * Content-Type} of {@code application/octet-stream}. This can be overridden by setting the {@link - * #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property, and overridding {@link - * #getContentType(byte[])}. + * #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property. * * @author Arjen Poutsma * @since 3.0 @@ -39,9 +38,10 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter< /** Creates a new instance of the {@code ByteArrayHttpMessageConverter}. */ public ByteArrayHttpMessageConverter() { - super(MediaType.ALL); + super(new MediaType("application", "octet-stream"), MediaType.ALL); } + @Override public boolean supports(Class clazz) { return byte[].class.equals(clazz); } @@ -60,12 +60,7 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter< } @Override - protected MediaType getContentType(byte[] bytes) { - return new MediaType("application", "octet-stream"); - } - - @Override - protected Long getContentLength(byte[] bytes) { + protected Long getContentLength(byte[] bytes, MediaType contentType) { return (long) bytes.length; } diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index 04115af2f97..4f4844ab7c0 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -55,6 +55,7 @@ public class FormHttpMessageConverter extends AbstractHttpMessageConverter> clazz) { return MultiValueMap.class.isAssignableFrom(clazz); } @@ -87,7 +88,7 @@ public class FormHttpMessageConverter extends AbstractHttpMessageConverter form, HttpOutputMessage outputMessage) throws IOException { - MediaType contentType = getContentType(form); + MediaType contentType = getDefaultContentType(form); Charset charset = contentType.getCharSet() != null ? contentType.getCharSet() : DEFAULT_CHARSET; StringBuilder builder = new StringBuilder(); for (Iterator>> entryIterator = form.entrySet().iterator(); diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java index 197ab4b608a..0000a48be72 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/HttpMessageConverter.java @@ -32,20 +32,35 @@ import org.springframework.http.MediaType; public interface HttpMessageConverter { /** - * Indicate whether the given class is supported by this converter. + * Indicates whether the given class can be read by this converter. * - * @param clazz the class to test for support - * @return true if supported; false otherwise + * @param clazz the class to test for readability + * @param mediaType the media type to read, can be {@code null} if not specified + * @return true if readable; false otherwise */ - boolean supports(Class clazz); + boolean canRead(Class clazz, MediaType mediaType); - /** Return the list of {@link MediaType} objects supported by this converter. */ + /** + * Indicates whether the given class can be written by this converter. + * + * @param clazz the class to test for writability + * @param mediaType the media type to write, can be {@code null} if not specified + * @return true if writable; false otherwise + */ + boolean canWrite(Class clazz, MediaType mediaType); + + /** + * Return the list of {@link MediaType} objects supported by this converter. + * + * @return the list of supported media types + */ List getSupportedMediaTypes(); /** * Read an object of the given type form the given input message, and returns it. * - * @param clazz the type of object to return + * @param clazz the type of object to return. This type must have previously been passed to the {@link #canRead + * canRead} method of this interface, which must have returned {@code true}. * @param inputMessage the HTTP input message to read from * @return the converted object * @throws IOException in case of I/O errors @@ -56,11 +71,16 @@ public interface HttpMessageConverter { /** * Write an given object to the given output message. * - * @param t the object to write to the output message + * @param t the object to write to the output message. The type of this object must have previously been passed to the + * {@link #canWrite canWrite} method of this interface, which must have returned {@code true}. + * @param contentType the content type to use when writing. May be {@code null} to indicate that the the default + * content type of the converter must be used. If not {@code null}, this media type must have previously been passed to + * the {@link #canWrite canWrite} method of this interface, which must have returned {@code true}. * @param outputMessage the message to write to * @throws IOException in case of I/O errors * @throws HttpMessageNotWritableException in case of conversion errors */ - void write(T t, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException; + void write(T t, MediaType contentType, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException; } diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java index 7d5808ddeea..f29b86a6896 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/StringHttpMessageConverter.java @@ -32,7 +32,7 @@ import org.springframework.util.FileCopyUtils; /** * Implementation of {@link HttpMessageConverter} that can read and write strings. * - *

        By default, this converter supports all text media types (text/*), and writes with a {@code + *

        By default, this converter supports all media types (*/*), and writes with a {@code * Content-Type} of {@code text/plain}. This can be overridden by setting the {@link * #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property. * @@ -46,10 +46,11 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter availableCharsets; public StringHttpMessageConverter() { - super(new MediaType("text", "plain", DEFAULT_CHARSET), new MediaType("text", "*")); + super(new MediaType("text", "plain", DEFAULT_CHARSET), MediaType.ALL); this.availableCharsets = new ArrayList(Charset.availableCharsets().values()); } + @Override public boolean supports(Class clazz) { return String.class.equals(clazz); } @@ -62,9 +63,9 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverterBy default, returns {@link Charset#availableCharsets()}. Can be - * overridden in subclasses. + * Return the list of supported {@link Charset}. + * + *

        By default, returns {@link Charset#availableCharsets()}. Can be overridden in subclasses. * * @return the list of accepted charsets */ diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java index 629d61d47d5..dfb2681b403 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java @@ -54,9 +54,7 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageCo private boolean prefixJson = false; - /** - * Construct a new {@code BindingJacksonHttpMessageConverter}, - */ + /** Construct a new {@code BindingJacksonHttpMessageConverter}, */ public MappingJacksonHttpMessageConverter() { super(new MediaType("application", "json")); } @@ -75,9 +73,7 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageCo this.objectMapper = objectMapper; } - /** - * Sets the {@code JsonEncoding} for this converter. By default, {@linkplain JsonEncoding#UTF8 UTF-8} is used. - */ + /** Sets the {@code JsonEncoding} for this converter. By default, {@linkplain JsonEncoding#UTF8 UTF-8} is used. */ public void setEncoding(JsonEncoding encoding) { Assert.notNull(encoding, "'encoding' must not be null"); this.encoding = encoding; @@ -94,6 +90,7 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageCo this.prefixJson = prefixJson; } + @Override public boolean supports(Class clazz) { return objectMapper.canSerialize(clazz); } @@ -105,7 +102,7 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageCo } @Override - protected MediaType getContentType(T t) { + protected MediaType getDefaultContentType(T t) { Charset charset = Charset.forName(encoding.getJavaName()); return new MediaType("application", "json", charset); } diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/xml/AbstractXmlHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/AbstractXmlHttpMessageConverter.java index 9220e277f4e..fea5c9650a9 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/xml/AbstractXmlHttpMessageConverter.java +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/AbstractXmlHttpMessageConverter.java @@ -36,8 +36,9 @@ import org.springframework.http.converter.HttpMessageConversionException; * Abstract base class for {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters} that * convert from/to XML. * - *

        By default, subclasses of this converter support {@code text/xml} and {@code application/xml}. This can be - * overridden by setting the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property. + *

        By default, subclasses of this converter support {@code text/xml}, {@code application/xml}, and {@code + * application/*-xml}. This can be overridden by setting the {@link #setSupportedMediaTypes(java.util.List) + * supportedMediaTypes} property. * * @author Arjen Poutsma * @since 3.0 @@ -48,10 +49,10 @@ public abstract class AbstractXmlHttpMessageConverter extends AbstractHttpMes /** * Protected constructor that sets the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes} to {@code - * text/xml} and {@code application/xml}. + * text/xml} and {@code application/xml}, and {@code application/*-xml}. */ protected AbstractXmlHttpMessageConverter() { - super(new MediaType("application", "xml"), new MediaType("text", "xml")); + super(new MediaType("application", "xml"), new MediaType("text", "xml"), new MediaType("application", "*+xml")); } /** Invokes {@link #readFromSource(Class, HttpHeaders, Source)}. */ diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java index e79e2b9a161..3255efebf13 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java @@ -35,11 +35,19 @@ import org.springframework.http.converter.HttpMessageConversionException; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; -/** @author Arjen Poutsma */ +/** + * Implementation of {@link org.springframework.http.converter.HttpMessageConverter} that can read and write {@link + * Source} objects. + * + * @author Arjen Poutsma + * @since 3.0 + */ public class SourceHttpMessageConverter extends AbstractXmlHttpMessageConverter { + @Override public boolean supports(Class clazz) { - return Source.class.isAssignableFrom(clazz); + return DOMSource.class.equals(clazz) || SAXSource.class.equals(clazz) || StreamSource.class.equals(clazz) || + Source.class.equals(clazz); } @Override @@ -52,11 +60,11 @@ public class SourceHttpMessageConverter extends AbstractXmlHtt return (T) new DOMSource(domResult.getNode()); } else if (SAXSource.class.equals(clazz)) { - ByteArrayInputStream bis = transformToByteArray(source); + ByteArrayInputStream bis = transformToByteArrayInputStream(source); return (T) new SAXSource(new InputSource(bis)); } else if (StreamSource.class.equals(clazz) || Source.class.equals(clazz)) { - ByteArrayInputStream bis = transformToByteArray(source); + ByteArrayInputStream bis = transformToByteArrayInputStream(source); return (T) new StreamSource(bis); } else { @@ -65,11 +73,12 @@ public class SourceHttpMessageConverter extends AbstractXmlHtt } } catch (TransformerException ex) { - throw new HttpMessageNotReadableException("Could not transform from [" + source + "]", ex); + throw new HttpMessageNotReadableException("Could not transform from [" + source + "] to [" + clazz + "]", + ex); } } - private ByteArrayInputStream transformToByteArray(Source source) throws TransformerException { + private ByteArrayInputStream transformToByteArrayInputStream(Source source) throws TransformerException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); transform(source, new StreamResult(bos)); return new ByteArrayInputStream(bos.toByteArray()); diff --git a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java index b6213a40021..cf25001540a 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java +++ b/org.springframework.web/src/main/java/org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java @@ -23,11 +23,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.LinkedHashMap; -import java.util.Iterator; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -40,17 +40,17 @@ import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.MediaType; -import org.springframework.http.HttpHeaders; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.util.ClassUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; -import org.springframework.util.MultiValueMap; -import org.springframework.util.LinkedMultiValueMap; import org.springframework.validation.BindingResult; import org.springframework.validation.Errors; import org.springframework.web.HttpMediaTypeNotSupportedException; @@ -73,16 +73,16 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.WebRequest; /** - * Support class for invoking an annotated handler method. Operates on the introspection results of a - * {@link HandlerMethodResolver} for a specific handler type. + * Support class for invoking an annotated handler method. Operates on the introspection results of a {@link + * HandlerMethodResolver} for a specific handler type. * - *

        Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter} and - * {@link org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}. + *

        Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter} and {@link + * org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}. * * @author Juergen Hoeller * @author Arjen Poutsma - * @since 2.5.2 * @see #invokeHandlerMethod + * @since 2.5.2 */ public class HandlerMethodInvoker { @@ -103,7 +103,6 @@ public class HandlerMethodInvoker { private final SimpleSessionStatus sessionStatus = new SimpleSessionStatus(); - public HandlerMethodInvoker(HandlerMethodResolver methodResolver) { this(methodResolver, null); } @@ -112,9 +111,12 @@ public class HandlerMethodInvoker { this(methodResolver, bindingInitializer, new DefaultSessionAttributeStore(), null, null, null); } - public HandlerMethodInvoker(HandlerMethodResolver methodResolver, WebBindingInitializer bindingInitializer, - SessionAttributeStore sessionAttributeStore, ParameterNameDiscoverer parameterNameDiscoverer, - WebArgumentResolver[] customArgumentResolvers, HttpMessageConverter[] messageConverters) { + public HandlerMethodInvoker(HandlerMethodResolver methodResolver, + WebBindingInitializer bindingInitializer, + SessionAttributeStore sessionAttributeStore, + ParameterNameDiscoverer parameterNameDiscoverer, + WebArgumentResolver[] customArgumentResolvers, + HttpMessageConverter[] messageConverters) { this.methodResolver = methodResolver; this.bindingInitializer = bindingInitializer; @@ -124,9 +126,10 @@ public class HandlerMethodInvoker { this.messageConverters = messageConverters; } - - public final Object invokeHandlerMethod(Method handlerMethod, Object handler, - NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception { + public final Object invokeHandlerMethod(Method handlerMethod, + Object handler, + NativeWebRequest webRequest, + ExtendedModelMap implicitModel) throws Exception { Method handlerMethodToInvoke = BridgeMethodResolver.findBridgedMethod(handlerMethod); try { @@ -149,10 +152,10 @@ public class HandlerMethodInvoker { } Object attrValue = doInvokeMethod(attributeMethodToInvoke, handler, args); if ("".equals(attrName)) { - Class resolvedType = GenericTypeResolver.resolveReturnType( - attributeMethodToInvoke, handler.getClass()); - attrName = Conventions.getVariableNameForReturnType( - attributeMethodToInvoke, resolvedType, attrValue); + Class resolvedType = + GenericTypeResolver.resolveReturnType(attributeMethodToInvoke, handler.getClass()); + attrName = + Conventions.getVariableNameForReturnType(attributeMethodToInvoke, resolvedType, attrValue); } if (!implicitModel.containsAttribute(attrName)) { implicitModel.addAttribute(attrName, attrValue); @@ -171,8 +174,10 @@ public class HandlerMethodInvoker { } @SuppressWarnings("unchecked") - private Object[] resolveHandlerArguments(Method handlerMethod, Object handler, - NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception { + private Object[] resolveHandlerArguments(Method handlerMethod, + Object handler, + NativeWebRequest webRequest, + ExtendedModelMap implicitModel) throws Exception { Class[] paramTypes = handlerMethod.getParameterTypes(); Object[] args = new Object[paramTypes.length]; @@ -287,7 +292,8 @@ public class HandlerMethodInvoker { args[i] = resolvePathVariable(pathVarName, methodParam, webRequest, handler); } else if (attrName != null) { - WebRequestDataBinder binder = resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler); + WebRequestDataBinder binder = + resolveModelAttribute(attrName, methodParam, implicitModel, webRequest, handler); boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1])); if (binder.getTarget() != null) { doBind(binder, webRequest, validate, !assignBindingResult); @@ -334,8 +340,10 @@ public class HandlerMethodInvoker { } } - private Object[] resolveInitBinderArguments(Object handler, Method initBinderMethod, - WebDataBinder binder, NativeWebRequest webRequest) throws Exception { + private Object[] resolveInitBinderArguments(Object handler, + Method initBinderMethod, + WebDataBinder binder, + NativeWebRequest webRequest) throws Exception { Class[] initBinderParams = initBinderMethod.getParameterTypes(); Object[] initBinderArgs = new Object[initBinderParams.length]; @@ -390,8 +398,8 @@ public class HandlerMethodInvoker { } if (paramName != null) { - initBinderArgs[i] = resolveRequestParam( - paramName, paramRequired, paramDefaultValue, methodParam, webRequest, null); + initBinderArgs[i] = + resolveRequestParam(paramName, paramRequired, paramDefaultValue, methodParam, webRequest, null); } else if (pathVarName != null) { initBinderArgs[i] = resolvePathVariable(pathVarName, methodParam, webRequest, null); @@ -402,9 +410,12 @@ public class HandlerMethodInvoker { } @SuppressWarnings("unchecked") - private Object resolveRequestParam(String paramName, boolean required, String defaultValue, - MethodParameter methodParam, NativeWebRequest webRequest, Object handlerForInitBinderCall) - throws Exception { + private Object resolveRequestParam(String paramName, + boolean required, + String defaultValue, + MethodParameter methodParam, + NativeWebRequest webRequest, + Object handlerForInitBinderCall) throws Exception { Class paramType = methodParam.getParameterType(); if (Map.class.isAssignableFrom(paramType)) { @@ -460,9 +471,12 @@ public class HandlerMethodInvoker { } @SuppressWarnings("unchecked") - private Object resolveRequestHeader(String headerName, boolean required, String defaultValue, - MethodParameter methodParam, NativeWebRequest webRequest, Object handlerForInitBinderCall) - throws Exception { + private Object resolveRequestHeader(String headerName, + boolean required, + String defaultValue, + MethodParameter methodParam, + NativeWebRequest webRequest, + Object handlerForInitBinderCall) throws Exception { Class paramType = methodParam.getParameterType(); if (Map.class.isAssignableFrom(paramType)) { @@ -495,7 +509,8 @@ public class HandlerMethodInvoker { MultiValueMap result; if (HttpHeaders.class.isAssignableFrom(mapType)) { result = new HttpHeaders(); - } else { + } + else { result = new LinkedMultiValueMap(); } for (Iterator iterator = webRequest.getHeaderNames(); iterator.hasNext();) { @@ -517,10 +532,7 @@ public class HandlerMethodInvoker { } } - - /** - * Resolves the given {@link RequestBody @RequestBody} annotation. - */ + /** Resolves the given {@link RequestBody @RequestBody} annotation. */ @SuppressWarnings("unchecked") protected Object resolveRequestBody(MethodParameter methodParam, NativeWebRequest webRequest, Object handler) throws Exception { @@ -542,12 +554,8 @@ public class HandlerMethodInvoker { if (this.messageConverters != null) { for (HttpMessageConverter messageConverter : this.messageConverters) { allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); - if (messageConverter.supports(paramType)) { - for (MediaType supportedMediaType : messageConverter.getSupportedMediaTypes()) { - if (supportedMediaType.includes(contentType)) { - return messageConverter.read(paramType, inputMessage); - } - } + if (messageConverter.canRead(paramType, contentType)) { + return messageConverter.read(paramType, inputMessage); } } } @@ -555,16 +563,19 @@ public class HandlerMethodInvoker { } /** - * Return a {@link HttpInputMessage} for the given {@link NativeWebRequest}. - * Throws an UnsupportedOperationException by default. + * Return a {@link HttpInputMessage} for the given {@link NativeWebRequest}. Throws an UnsupportedOperationException by + * default. */ protected HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest) throws Exception { throw new UnsupportedOperationException("@RequestBody not supported"); } - private Object resolveCookieValue(String cookieName, boolean required, String defaultValue, - MethodParameter methodParam, NativeWebRequest webRequest, Object handlerForInitBinderCall) - throws Exception { + private Object resolveCookieValue(String cookieName, + boolean required, + String defaultValue, + MethodParameter methodParam, + NativeWebRequest webRequest, + Object handlerForInitBinderCall) throws Exception { Class paramType = methodParam.getParameterType(); if (cookieName.length() == 0) { @@ -585,18 +596,17 @@ public class HandlerMethodInvoker { return binder.convertIfNecessary(cookieValue, paramType, methodParam); } - /** - * Resolves the given {@link CookieValue @CookieValue} annotation. - * Throws an UnsupportedOperationException by default. - */ + /** Resolves the given {@link CookieValue @CookieValue} annotation. Throws an UnsupportedOperationException by default. */ protected Object resolveCookieValue(String cookieName, Class paramType, NativeWebRequest webRequest) throws Exception { throw new UnsupportedOperationException("@CookieValue not supported"); } - private Object resolvePathVariable(String pathVarName, MethodParameter methodParam, - NativeWebRequest webRequest, Object handlerForInitBinderCall) throws Exception { + private Object resolvePathVariable(String pathVarName, + MethodParameter methodParam, + NativeWebRequest webRequest, + Object handlerForInitBinderCall) throws Exception { Class paramType = methodParam.getParameterType(); if (pathVarName.length() == 0) { @@ -609,8 +619,8 @@ public class HandlerMethodInvoker { } /** - * Resolves the given {@link PathVariable @PathVariable} annotation. - * Throws an UnsupportedOperationException by default. + * Resolves the given {@link PathVariable @PathVariable} annotation. Throws an UnsupportedOperationException by + * default. */ protected String resolvePathVariable(String pathVarName, Class paramType, NativeWebRequest webRequest) throws Exception { @@ -621,9 +631,9 @@ public class HandlerMethodInvoker { private String getRequiredParameterName(MethodParameter methodParam) { String name = methodParam.getParameterName(); if (name == null) { - throw new IllegalStateException("No parameter name specified for argument of type [" + - methodParam.getParameterType().getName() + - "], and no parameter name information found in class file either."); + throw new IllegalStateException( + "No parameter name specified for argument of type [" + methodParam.getParameterType().getName() + + "], and no parameter name information found in class file either."); } return name; } @@ -642,9 +652,11 @@ public class HandlerMethodInvoker { return value; } - private WebRequestDataBinder resolveModelAttribute(String attrName, MethodParameter methodParam, - ExtendedModelMap implicitModel, NativeWebRequest webRequest, Object handler) - throws Exception { + private WebRequestDataBinder resolveModelAttribute(String attrName, + MethodParameter methodParam, + ExtendedModelMap implicitModel, + NativeWebRequest webRequest, + Object handler) throws Exception { // Bind request parameter onto object... String name = attrName; @@ -671,8 +683,10 @@ public class HandlerMethodInvoker { } @SuppressWarnings("unchecked") - public final void updateModelAttributes(Object handler, Map mavModel, - ExtendedModelMap implicitModel, NativeWebRequest webRequest) throws Exception { + public final void updateModelAttributes(Object handler, + Map mavModel, + ExtendedModelMap implicitModel, + NativeWebRequest webRequest) throws Exception { if (this.methodResolver.hasSessionAttributes() && this.sessionStatus.isComplete()) { for (String attrName : this.methodResolver.getActualSessionAttributeNames()) { @@ -731,15 +745,18 @@ public class HandlerMethodInvoker { } protected void raiseMissingCookieException(String cookieName, Class paramType) throws Exception { - throw new IllegalStateException("Missing cookie value '" + cookieName + "' of type [" + paramType.getName() + "]"); + throw new IllegalStateException( + "Missing cookie value '" + cookieName + "' of type [" + paramType.getName() + "]"); } protected void raiseSessionRequiredException(String message) throws Exception { throw new IllegalStateException(message); } - protected void doBind(WebRequestDataBinder binder, NativeWebRequest webRequest, boolean validate, boolean failOnErrors) - throws Exception { + protected void doBind(WebRequestDataBinder binder, + NativeWebRequest webRequest, + boolean validate, + boolean failOnErrors) throws Exception { binder.bind(webRequest); if (validate) { @@ -785,9 +802,11 @@ public class HandlerMethodInvoker { } return WebArgumentResolver.UNRESOLVED; } - - protected final void addReturnValueAsModelAttribute( - Method handlerMethod, Class handlerType, Object returnValue, ExtendedModelMap implicitModel) { + + protected final void addReturnValueAsModelAttribute(Method handlerMethod, + Class handlerType, + Object returnValue, + ExtendedModelMap implicitModel) { ModelAttribute attr = AnnotationUtils.findAnnotation(handlerMethod, ModelAttribute.class); String attrName = (attr != null ? attr.value() : ""); diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java b/org.springframework.web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java index 7e79cd359fa..c5de56acdd6 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java +++ b/org.springframework.web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java @@ -36,29 +36,28 @@ public class HttpMessageConverterExtractor implements ResponseExtractor { private final Class responseType; - private final List> messageConverters; + private final List> messageConverters; /** * Creates a new instance of the {@code HttpMessageConverterExtractor} with the given response type and message * converters. The given converters must support the response type. */ - public HttpMessageConverterExtractor(Class responseType, List> messageConverters) { + public HttpMessageConverterExtractor(Class responseType, List> messageConverters) { Assert.notNull(responseType, "'responseType' must not be null"); Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); this.responseType = responseType; this.messageConverters = messageConverters; } + @SuppressWarnings("unchecked") public T extractData(ClientHttpResponse response) throws IOException { MediaType contentType = response.getHeaders().getContentType(); if (contentType == null) { throw new RestClientException("Cannot extract response: no Content-Type found"); } - for (HttpMessageConverter messageConverter : messageConverters) { - for (MediaType supportedMediaType : messageConverter.getSupportedMediaTypes()) { - if (supportedMediaType.includes(contentType)) { - return messageConverter.read(this.responseType, response); - } + for (HttpMessageConverter messageConverter : messageConverters) { + if (messageConverter.canRead(responseType, contentType)) { + return (T) messageConverter.read(this.responseType, response); } } throw new RestClientException( 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 8b464aeadfc..d5e8779117b 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 @@ -44,22 +44,18 @@ import org.springframework.web.util.UriTemplate; * enforces RESTful principles. It handles HTTP connections, leaving application code to provide URLs (with possible * template variables) and extract results. * - *

        The main entry points of this template are the methods named after the six main HTTP methods: - * + * + * + *
        HTTP + *

        The main entry points of this template are the methods named after the six main HTTP methods: - * - * - * - * - * - *
        HTTP * methodRestTemplate methods
        DELETE{@link #delete}
        GET{@link #getForObject}
        HEAD{@link #headForHeaders}
        OPTIONS{@link #optionsForAllow}
        POST{@link #postForLocation}
        {@link #postForObject}
        PUT{@link #put}
        any{@link #execute}
        + *

        GET{@link #getForObject}
        HEAD{@link #headForHeaders}
        OPTIONS{@link #optionsForAllow}
        POST{@link #postForLocation}
        {@link #postForObject}
        PUT{@link #put}
        any{@link #execute}
        * - *

        For each of these HTTP methods, there are three corresponding Java methods in the {@code RestTemplate}. - * Two variant take a {@code String} URI as first argument (eg. {@link #getForObject(String, Class, String[])}, - * {@link #getForObject(String, Class, Map)}), and are capable of substituting any - * {@linkplain UriTemplate URI templates} in that URL using either a - * {@code String} variable arguments array, or a {@code Map}. The string varargs variant expands the - * given template variables in order, so that + *

        For each of these HTTP methods, there are three corresponding Java methods in the {@code RestTemplate}. Two + * variant take a {@code String} URI as first argument (eg. {@link #getForObject(String, Class, String[])}, {@link + * #getForObject(String, Class, Map)}), and are capable of substituting any {@linkplain UriTemplate URI templates} in + * that URL using either a {@code String} variable arguments array, or a {@code Map}. The string varargs + * variant expands the given template variables in order, so that *

          * String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/bookings/{booking}", String.class,"42",
          * "21");
        @@ -71,22 +67,22 @@ import org.springframework.web.util.UriTemplate;
          * Map<String, String> vars = Collections.singletonMap("hotel", "42");
          * String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);
          * 
        - * will perform a GET on {@code http://example.com/hotels/42/rooms/42}. - * Alternatively, there are {@link URI} variant methods ({@link #getForObject(URI, Class)}), which do not allow for - * URI templates, but allow you to reuse a single, expanded URI multiple times. + * will perform a GET on {@code http://example.com/hotels/42/rooms/42}. Alternatively, there are {@link URI} variant + * methods ({@link #getForObject(URI, Class)}), which do not allow for URI templates, but allow you to reuse a single, + * expanded URI multiple times. * *

        Furthermore, the {@code String}-argument methods assume that the URL String is unencoded. This means that *

          * restTemplate.getForObject("http://example.com/hotel list");
          * 
        * will perform a GET on {@code http://example.com/hotel%20list}. As a result, any URL passed that is already encoded - * will be encoded twice (i.e. {@code http://example.com/hotel%20list} will become {@code http://example.com/hotel%2520list}). - * If this behavior is undesirable, use the {@code URI}-argument methods, which will not perform any URL encoding. + * will be encoded twice (i.e. {@code http://example.com/hotel%20list} will become {@code + * http://example.com/hotel%2520list}). If this behavior is undesirable, use the {@code URI}-argument methods, which + * will not perform any URL encoding. * *

        Objects passed to and returned from these methods are converted to and from HTTP messages by {@link * HttpMessageConverter} instances. Converters for the main mime types are registered by default, but you can also write - * your own converter and register it via the {@link #setMessageConverters(HttpMessageConverter[]) messageConverters} - * bean property. + * your own converter and register it via the {@link #setMessageConverters messageConverters} bean property. * *

        This template uses a {@link org.springframework.http.client.SimpleClientHttpRequestFactory} and a {@link * DefaultResponseErrorHandler} as default strategies for creating HTTP connections or handling HTTP errors, @@ -104,14 +100,16 @@ public class RestTemplate extends HttpAccessor implements RestOperations { private final ResponseExtractor headersExtractor = new HeadersExtractor(); - private HttpMessageConverter[] messageConverters = - new HttpMessageConverter[]{new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(), - new FormHttpMessageConverter(), new SourceHttpMessageConverter()}; + private List> messageConverters = new ArrayList>(); private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler(); /** Create a new instance of the {@link RestTemplate} using default settings. */ public RestTemplate() { + this.messageConverters.add(new ByteArrayHttpMessageConverter()); + this.messageConverters.add(new StringHttpMessageConverter()); + this.messageConverters.add(new FormHttpMessageConverter()); + this.messageConverters.add(new SourceHttpMessageConverter()); } /** @@ -122,6 +120,7 @@ public class RestTemplate extends HttpAccessor implements RestOperations { * @see org.springframework.http.client.CommonsClientHttpRequestFactory */ public RestTemplate(ClientHttpRequestFactory requestFactory) { + this(); setRequestFactory(requestFactory); } @@ -129,34 +128,16 @@ public class RestTemplate extends HttpAccessor implements RestOperations { * Set the message body converters to use. These converters are used to convert from and to HTTP requests and * responses. */ - public void setMessageConverters(HttpMessageConverter[] messageConverters) { + public void setMessageConverters(List> messageConverters) { Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); this.messageConverters = messageConverters; } /** Returns the message body converters. These converters are used to convert from and to HTTP requests and responses. */ - public HttpMessageConverter[] getMessageConverters() { + public List> getMessageConverters() { return this.messageConverters; } - /** - * Returns the message body converters that support a particular type. - * - * @param type the type to return converters for - * @return converters that support the given type - */ - @SuppressWarnings("unchecked") - protected List> getSupportedMessageConverters(Class type) { - HttpMessageConverter[] converters = getMessageConverters(); - List> result = new ArrayList>(converters.length); - for (HttpMessageConverter converter : converters) { - if (converter.supports(type)) { - result.add((HttpMessageConverter) converter); - } - } - return result; - } - /** Set the error handler. */ public void setErrorHandler(ResponseErrorHandler errorHandler) { Assert.notNull(errorHandler, "'errorHandler' must not be null"); @@ -171,27 +152,25 @@ public class RestTemplate extends HttpAccessor implements RestOperations { // GET public T getForObject(String url, Class responseType, String... urlVariables) throws RestClientException { - - checkForSupportedMessageConverter(responseType); - List> supportedMessageConverters = getSupportedMessageConverters(responseType); - return execute(url, HttpMethod.GET, new AcceptHeaderRequestCallback(supportedMessageConverters), - new HttpMessageConverterExtractor(responseType, supportedMessageConverters), urlVariables); + AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(responseType); + HttpMessageConverterExtractor responseExtractor = + new HttpMessageConverterExtractor(responseType, getMessageConverters()); + return execute(url, HttpMethod.GET, requestCallback, responseExtractor, urlVariables); } public T getForObject(String url, Class responseType, Map urlVariables) throws RestClientException { - - checkForSupportedMessageConverter(responseType); - List> supportedMessageConverters = getSupportedMessageConverters(responseType); - return execute(url, HttpMethod.GET, new AcceptHeaderRequestCallback(supportedMessageConverters), - new HttpMessageConverterExtractor(responseType, supportedMessageConverters), urlVariables); + AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(responseType); + HttpMessageConverterExtractor responseExtractor = + new HttpMessageConverterExtractor(responseType, getMessageConverters()); + return execute(url, HttpMethod.GET, requestCallback, responseExtractor, urlVariables); } public T getForObject(URI url, Class responseType) throws RestClientException { - checkForSupportedMessageConverter(responseType); - List> supportedMessageConverters = getSupportedMessageConverters(responseType); - return execute(url, HttpMethod.GET, new AcceptHeaderRequestCallback(supportedMessageConverters), - new HttpMessageConverterExtractor(responseType, supportedMessageConverters)); + AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(responseType); + HttpMessageConverterExtractor responseExtractor = + new HttpMessageConverterExtractor(responseType, getMessageConverters()); + return execute(url, HttpMethod.GET, requestCallback, responseExtractor); } // HEAD @@ -211,86 +190,62 @@ public class RestTemplate extends HttpAccessor implements RestOperations { // POST public URI postForLocation(String url, Object request, String... urlVariables) throws RestClientException { - if (request != null) { - checkForSupportedMessageConverter(request.getClass()); - } - HttpHeaders headers = - execute(url, HttpMethod.POST, new PostPutCallback(request), this.headersExtractor, urlVariables); + PostPutCallback requestCallback = new PostPutCallback(request); + HttpHeaders headers = execute(url, HttpMethod.POST, requestCallback, this.headersExtractor, urlVariables); return headers.getLocation(); } public URI postForLocation(String url, Object request, Map urlVariables) throws RestClientException { - if (request != null) { - checkForSupportedMessageConverter(request.getClass()); - } - HttpHeaders headers = - execute(url, HttpMethod.POST, new PostPutCallback(request), this.headersExtractor, urlVariables); + PostPutCallback requestCallback = new PostPutCallback(request); + HttpHeaders headers = execute(url, HttpMethod.POST, requestCallback, this.headersExtractor, urlVariables); return headers.getLocation(); } - public URI postForLocation(URI url, Object request) - throws RestClientException { - if (request != null) { - checkForSupportedMessageConverter(request.getClass()); - } - HttpHeaders headers = execute(url, HttpMethod.POST, new PostPutCallback(request), this.headersExtractor); + public URI postForLocation(URI url, Object request) throws RestClientException { + PostPutCallback requestCallback = new PostPutCallback(request); + HttpHeaders headers = execute(url, HttpMethod.POST, requestCallback, this.headersExtractor); return headers.getLocation(); } public T postForObject(String url, Object request, Class responseType, String... uriVariables) throws RestClientException { - if (request != null) { - checkForSupportedMessageConverter(request.getClass()); - } - checkForSupportedMessageConverter(responseType); - List> responseMessageConverters = getSupportedMessageConverters(responseType); - return execute(url, HttpMethod.POST, new PostPutCallback(request, responseMessageConverters), - new HttpMessageConverterExtractor(responseType, responseMessageConverters), uriVariables); + PostPutCallback requestCallback = new PostPutCallback(request, responseType); + HttpMessageConverterExtractor responseExtractor = + new HttpMessageConverterExtractor(responseType, getMessageConverters()); + return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables); } public T postForObject(String url, Object request, Class responseType, Map uriVariables) throws RestClientException { - if (request != null) { - checkForSupportedMessageConverter(request.getClass()); - } - checkForSupportedMessageConverter(responseType); - List> responseMessageConverters = getSupportedMessageConverters(responseType); - return execute(url, HttpMethod.POST, new PostPutCallback(request, responseMessageConverters), - new HttpMessageConverterExtractor(responseType, responseMessageConverters), uriVariables); + PostPutCallback requestCallback = new PostPutCallback(request, responseType); + HttpMessageConverterExtractor responseExtractor = + new HttpMessageConverterExtractor(responseType, getMessageConverters()); + return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables); } public T postForObject(URI url, Object request, Class responseType) throws RestClientException { - if (request != null) { - checkForSupportedMessageConverter(request.getClass()); - } - checkForSupportedMessageConverter(responseType); - List> responseMessageConverters = getSupportedMessageConverters(responseType); - return execute(url, HttpMethod.POST, new PostPutCallback(request, responseMessageConverters), - new HttpMessageConverterExtractor(responseType, responseMessageConverters)); + PostPutCallback requestCallback = new PostPutCallback(request, responseType); + HttpMessageConverterExtractor responseExtractor = + new HttpMessageConverterExtractor(responseType, getMessageConverters()); + return execute(url, HttpMethod.POST, requestCallback, responseExtractor); } // PUT public void put(String url, Object request, String... urlVariables) throws RestClientException { - if (request != null) { - checkForSupportedMessageConverter(request.getClass()); - } - execute(url, HttpMethod.PUT, new PostPutCallback(request), null, urlVariables); + PostPutCallback requestCallback = new PostPutCallback(request); + execute(url, HttpMethod.PUT, requestCallback, null, urlVariables); } public void put(String url, Object request, Map urlVariables) throws RestClientException { - if (request != null) { - checkForSupportedMessageConverter(request.getClass()); - } - execute(url, HttpMethod.PUT, new PostPutCallback(request), null, urlVariables); + PostPutCallback requestCallback = new PostPutCallback(request); + execute(url, HttpMethod.PUT, requestCallback, null, urlVariables); } public void put(URI url, Object request) throws RestClientException { - if (request != null) { - checkForSupportedMessageConverter(request.getClass()); - } - execute(url, HttpMethod.PUT, new PostPutCallback(request), null); + PostPutCallback requestCallback = new PostPutCallback(request); + execute(url, HttpMethod.PUT, requestCallback, null); } // DELETE @@ -405,28 +360,12 @@ public class RestTemplate extends HttpAccessor implements RestOperations { } } - /** - * Check whether any of the registered {@linkplain #setMessageConverters(HttpMessageConverter[]) message body - * converters} can convert the given type. - * - * @param type the type to check for - * @throws IllegalArgumentException if no supported entity converter can be found - * @see HttpMessageConverter#supports(Class) - */ - private void checkForSupportedMessageConverter(Class type) { - for (HttpMessageConverter entityConverter : getMessageConverters()) { - if (entityConverter.supports(type)) { - return; - } - } - throw new IllegalArgumentException("Could not resolve HttpMessageConverter for [" + type.getName() + "]"); - } - private void logResponseStatus(HttpMethod method, URI url, ClientHttpResponse response) { if (logger.isDebugEnabled()) { try { - logger.debug(method.name() + " request for \"" + url + "\" resulted in " + response.getStatusCode() + - " (" + response.getStatusText() + ")"); + logger.debug( + method.name() + " request for \"" + url + "\" resulted in " + response.getStatusCode() + " (" + + response.getStatusText() + ")"); } catch (IOException e) { // ignore @@ -437,8 +376,9 @@ public class RestTemplate extends HttpAccessor implements RestOperations { private void handleResponseError(HttpMethod method, URI url, ClientHttpResponse response) throws IOException { if (logger.isWarnEnabled()) { try { - logger.warn(method.name() + " request for \"" + url + "\" resulted in " + response.getStatusCode() + - " (" + response.getStatusText() + "); invoking error handler"); + logger.warn( + method.name() + " request for \"" + url + "\" resulted in " + response.getStatusCode() + " (" + + response.getStatusText() + "); invoking error handler"); } catch (IOException e) { // ignore @@ -450,27 +390,36 @@ public class RestTemplate extends HttpAccessor implements RestOperations { /** Request callback implementation that prepares the request's accept headers. */ private class AcceptHeaderRequestCallback implements RequestCallback { - private final List> messageConverters; + private final Class responseType; - private AcceptHeaderRequestCallback(List> messageConverters) { - this.messageConverters = messageConverters; + private AcceptHeaderRequestCallback() { + responseType = null; } + private AcceptHeaderRequestCallback(Class responseType) { + this.responseType = responseType; + } + + @SuppressWarnings("unchecked") public void doWithRequest(ClientHttpRequest request) throws IOException { - List allSupportedMediaTypes = new ArrayList(); - for (HttpMessageConverter entityConverter : messageConverters) { - List supportedMediaTypes = entityConverter.getSupportedMediaTypes(); - for (MediaType supportedMediaType : supportedMediaTypes) { - if (supportedMediaType.getCharSet() != null) { - supportedMediaType = - new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype()); + if (responseType != null) { + List allSupportedMediaTypes = new ArrayList(); + for (HttpMessageConverter messageConverter : getMessageConverters()) { + if (messageConverter.canRead(responseType, null)) { + List supportedMediaTypes = messageConverter.getSupportedMediaTypes(); + for (MediaType supportedMediaType : supportedMediaTypes) { + if (supportedMediaType.getCharSet() != null) { + supportedMediaType = + new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype()); + } + allSupportedMediaTypes.add(supportedMediaType); + } } - allSupportedMediaTypes.add(supportedMediaType); } - } - if (!allSupportedMediaTypes.isEmpty()) { - Collections.sort(allSupportedMediaTypes); - request.getHeaders().setAccept(allSupportedMediaTypes); + if (!allSupportedMediaTypes.isEmpty()) { + Collections.sort(allSupportedMediaTypes); + request.getHeaders().setAccept(allSupportedMediaTypes); + } } } } @@ -478,25 +427,45 @@ public class RestTemplate extends HttpAccessor implements RestOperations { /** Request callback implementation that writes the given object to the request stream. */ private class PostPutCallback extends AcceptHeaderRequestCallback { - private final Object request; + private final Object requestBody; - private PostPutCallback(Object request, List> responseMessageConverters) { - super(responseMessageConverters); - this.request = request; + private final MediaType requestContentType; + + private PostPutCallback(Object requestBody) { + this.requestBody = requestBody; + this.requestContentType = null; } - private PostPutCallback(Object request) { - super(Collections.>emptyList()); - this.request = request; + private PostPutCallback(Object requestBody, Class responseType) { + super(responseType); + this.requestBody = requestBody; + this.requestContentType = null; + } + + private PostPutCallback(Object requestBody, MediaType requestContentType, Class responseType) { + super(responseType); + this.requestBody = requestBody; + this.requestContentType = requestContentType; } @Override @SuppressWarnings("unchecked") public void doWithRequest(ClientHttpRequest httpRequest) throws IOException { super.doWithRequest(httpRequest); - if (request != null) { - HttpMessageConverter entityConverter = getSupportedMessageConverters(this.request.getClass()).get(0); - entityConverter.write(this.request, httpRequest); + if (requestBody != null) { + Class requestType = requestBody.getClass(); + for (HttpMessageConverter messageConverter : getMessageConverters()) { + if (messageConverter.canWrite(requestType, requestContentType)) { + messageConverter.write(requestBody, requestContentType, httpRequest); + return; + } + } + String message = "Could not write request: no suitable HttpMessageConverter found for request type [" + + requestType.getName() + "]"; + if (requestContentType != null) { + message += " and content type [" + requestContentType + "]"; + } + throw new RestClientException(message); } else { httpRequest.getHeaders().setContentLength(0L); diff --git a/org.springframework.web/src/test/java/org/springframework/http/converter/BufferedImageHttpMessageConverterTests.java b/org.springframework.web/src/test/java/org/springframework/http/converter/BufferedImageHttpMessageConverterTests.java index e8df5930e55..3fd2d97e2af 100644 --- a/org.springframework.web/src/test/java/org/springframework/http/converter/BufferedImageHttpMessageConverterTests.java +++ b/org.springframework.web/src/test/java/org/springframework/http/converter/BufferedImageHttpMessageConverterTests.java @@ -42,8 +42,15 @@ public class BufferedImageHttpMessageConverterTests { } @Test - public void supports() { - assertTrue("Image not supported", converter.supports(BufferedImage.class)); + public void canRead() { + assertTrue("Image not supported", converter.canRead(BufferedImage.class, null)); + assertTrue("Image not supported", converter.canRead(BufferedImage.class, new MediaType("image", "png"))); + } + + @Test + public void canWrite() { + assertTrue("Image not supported", converter.canWrite(BufferedImage.class, null)); + assertTrue("Image not supported", converter.canWrite(BufferedImage.class, new MediaType("image", "png"))); } @Test @@ -60,11 +67,25 @@ public class BufferedImageHttpMessageConverterTests { @Test public void write() throws IOException { Resource logo = new ClassPathResource("logo.jpg", BufferedImageHttpMessageConverterTests.class); - MediaType contentType = new MediaType("image", "png"); - converter.setContentType(contentType); BufferedImage body = ImageIO.read(logo.getFile()); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - converter.write(body, outputMessage); + MediaType contentType = new MediaType("image", "png"); + converter.write(body, contentType, outputMessage); + assertEquals("Invalid content type", contentType, outputMessage.getHeaders().getContentType()); + assertTrue("Invalid size", outputMessage.getBodyAsBytes().length > 0); + BufferedImage result = ImageIO.read(new ByteArrayInputStream(outputMessage.getBodyAsBytes())); + assertEquals("Invalid height", 500, result.getHeight()); + assertEquals("Invalid width", 750, result.getWidth()); + } + + @Test + public void writeDefaultContentType() throws IOException { + Resource logo = new ClassPathResource("logo.jpg", BufferedImageHttpMessageConverterTests.class); + MediaType contentType = new MediaType("image", "png"); + converter.setDefaultContentType(contentType); + BufferedImage body = ImageIO.read(logo.getFile()); + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + converter.write(body, contentType, outputMessage); assertEquals("Invalid content type", contentType, outputMessage.getHeaders().getContentType()); assertTrue("Invalid size", outputMessage.getBodyAsBytes().length > 0); BufferedImage result = ImageIO.read(new ByteArrayInputStream(outputMessage.getBodyAsBytes())); diff --git a/org.springframework.web/src/test/java/org/springframework/http/converter/ByteArrayHttpMessageConverterTests.java b/org.springframework.web/src/test/java/org/springframework/http/converter/ByteArrayHttpMessageConverterTests.java index 742456a1d45..3a0c22cdb1e 100644 --- a/org.springframework.web/src/test/java/org/springframework/http/converter/ByteArrayHttpMessageConverterTests.java +++ b/org.springframework.web/src/test/java/org/springframework/http/converter/ByteArrayHttpMessageConverterTests.java @@ -26,9 +26,7 @@ import org.springframework.http.MediaType; import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpOutputMessage; -/** - * @author Arjen Poutsma - */ +/** @author Arjen Poutsma */ public class ByteArrayHttpMessageConverterTests { private ByteArrayHttpMessageConverter converter; @@ -51,7 +49,7 @@ public class ByteArrayHttpMessageConverterTests { public void write() throws IOException { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); byte[] body = new byte[]{0x1, 0x2}; - converter.write(body, outputMessage); + converter.write(body, null, outputMessage); assertArrayEquals("Invalid result", body, outputMessage.getBodyAsBytes()); assertEquals("Invalid content-type", new MediaType("application", "octet-stream"), outputMessage.getHeaders().getContentType()); diff --git a/org.springframework.web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java b/org.springframework.web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java index 6a790145aa9..f795bf2e575 100644 --- a/org.springframework.web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java +++ b/org.springframework.web/src/test/java/org/springframework/http/converter/FormHttpMessageConverterTests.java @@ -30,9 +30,7 @@ import org.springframework.http.MockHttpOutputMessage; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -/** - * @author Arjen Poutsma - */ +/** @author Arjen Poutsma */ public class FormHttpMessageConverterTests { private FormHttpMessageConverter converter; @@ -67,7 +65,7 @@ public class FormHttpMessageConverterTests { body.add("name 2", "value 2+2"); body.add("name 3", null); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - converter.write(body, outputMessage); + converter.write(body, null, outputMessage); Charset iso88591 = Charset.forName("ISO-8859-1"); assertEquals("Invalid result", "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3", outputMessage.getBodyAsString(iso88591)); diff --git a/org.springframework.web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java b/org.springframework.web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java index 6c19bb12824..502edadebe4 100644 --- a/org.springframework.web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java +++ b/org.springframework.web/src/test/java/org/springframework/http/converter/StringHttpMessageConverterTests.java @@ -18,7 +18,6 @@ package org.springframework.http.converter; import java.io.IOException; import java.nio.charset.Charset; -import java.util.Collections; import static org.junit.Assert.*; import org.junit.Before; @@ -28,9 +27,7 @@ import org.springframework.http.MediaType; import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpOutputMessage; -/** - * @author Arjen Poutsma - */ +/** @author Arjen Poutsma */ public class StringHttpMessageConverterTests { private StringHttpMessageConverter converter; @@ -55,7 +52,7 @@ public class StringHttpMessageConverterTests { Charset iso88591 = Charset.forName("ISO-8859-1"); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); String body = "H\u00e9llo W\u00f6rld"; - converter.write(body, outputMessage); + converter.write(body, null, outputMessage); assertEquals("Invalid result", body, outputMessage.getBodyAsString(iso88591)); assertEquals("Invalid content-type", new MediaType("text", "plain", iso88591), outputMessage.getHeaders().getContentType()); @@ -67,13 +64,12 @@ public class StringHttpMessageConverterTests { @Test public void writeUTF8() throws IOException { Charset utf8 = Charset.forName("UTF-8"); - converter.setSupportedMediaTypes(Collections.singletonList(new MediaType("text", "plain", utf8))); + MediaType contentType = new MediaType("text", "plain", utf8); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); String body = "H\u00e9llo W\u00f6rld"; - converter.write(body, outputMessage); + converter.write(body, contentType, outputMessage); assertEquals("Invalid result", body, outputMessage.getBodyAsString(utf8)); - assertEquals("Invalid content-type", new MediaType("text", "plain", utf8), - outputMessage.getHeaders().getContentType()); + assertEquals("Invalid content-type", contentType, outputMessage.getHeaders().getContentType()); assertEquals("Invalid content-length", body.getBytes(utf8).length, outputMessage.getHeaders().getContentLength()); assertFalse("Invalid accept-charset", outputMessage.getHeaders().getAcceptCharset().isEmpty()); diff --git a/org.springframework.web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java b/org.springframework.web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java index ce50033a2c2..501d90ad981 100644 --- a/org.springframework.web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java +++ b/org.springframework.web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java @@ -39,7 +39,7 @@ public class MappingJacksonHttpMessageConverterTests { public void setUp() { converter = new MappingJacksonHttpMessageConverter(); } - + @Test public void supports() { assertTrue(converter.supports(MyBean.class)); @@ -68,10 +68,10 @@ public class MappingJacksonHttpMessageConverterTests { "{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); inputMessage.getHeaders().setContentType(new MediaType("application", "json")); - HashMap result = converter.read(HashMap.class, inputMessage); + HashMap result = converter.read(HashMap.class, inputMessage); assertEquals("Foo", result.get("string")); assertEquals(42, result.get("number")); - assertEquals(42D, (Double)result.get("fraction"), 0D); + assertEquals(42D, (Double) result.get("fraction"), 0D); List array = new ArrayList(); array.add("Foo"); array.add("Bar"); @@ -90,7 +90,7 @@ public class MappingJacksonHttpMessageConverterTests { body.setArray(new String[]{"Foo", "Bar"}); body.setBool(true); body.setBytes(new byte[]{0x1, 0x2}); - converter.write(body, outputMessage); + converter.write(body, null, outputMessage); Charset utf8 = Charset.forName("UTF-8"); String result = outputMessage.getBodyAsString(utf8); assertTrue(result.contains("\"string\":\"Foo\"")); diff --git a/org.springframework.web/src/test/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverterTests.java b/org.springframework.web/src/test/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverterTests.java index 845d5d897aa..7fd414b627c 100644 --- a/org.springframework.web/src/test/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverterTests.java +++ b/org.springframework.web/src/test/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverterTests.java @@ -68,7 +68,7 @@ public class MarshallingHttpMessageConverterTests { marshaller.marshal(eq(body), isA(StreamResult.class)); replay(marshaller, unmarshaller); - converter.write(body, outputMessage); + converter.write(body, null, outputMessage); assertEquals("Invalid content-type", new MediaType("application", "xml"), outputMessage.getHeaders().getContentType()); verify(marshaller, unmarshaller); diff --git a/org.springframework.web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTests.java b/org.springframework.web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTests.java index f063b6decd8..6153d3386bd 100644 --- a/org.springframework.web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTests.java +++ b/org.springframework.web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTests.java @@ -94,7 +94,7 @@ public class SourceHttpMessageConverterTests { SourceHttpMessageConverter converter = new SourceHttpMessageConverter(); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - converter.write(domSource, outputMessage); + converter.write(domSource, null, outputMessage); assertXMLEqual("Invalid result", "Hello World", outputMessage.getBodyAsString(Charset.forName("UTF-8"))); assertEquals("Invalid content-type", new MediaType("application", "xml"), diff --git a/org.springframework.web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/org.springframework.web/src/test/java/org/springframework/web/client/RestTemplateTests.java index 4343566485d..82038361e17 100644 --- a/org.springframework.web/src/test/java/org/springframework/web/client/RestTemplateTests.java +++ b/org.springframework.web/src/test/java/org/springframework/web/client/RestTemplateTests.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.net.URI; import java.util.Collections; import java.util.EnumSet; -import java.util.List; import java.util.Map; import java.util.Set; @@ -36,9 +35,7 @@ import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpResponse; -import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.StringHttpMessageConverter; /** @author Arjen Poutsma */ @SuppressWarnings("unchecked") @@ -65,18 +62,7 @@ public class RestTemplateTests { converter = createMock(HttpMessageConverter.class); template = new RestTemplate(requestFactory); template.setErrorHandler(errorHandler); - template.setMessageConverters(new HttpMessageConverter[]{converter}); - } - - @Test - public void getSupportedMessageBodyConverters() { - ByteArrayHttpMessageConverter byteArrayConverter = new ByteArrayHttpMessageConverter(); - StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(); - template.setMessageConverters(new HttpMessageConverter[]{byteArrayConverter, stringConverter}); - - List> result = template.getSupportedMessageConverters(String.class); - assertEquals("Invalid amount of String converters", 1, result.size()); - assertEquals("Invalid String converters", stringConverter, result.get(0)); + template.setMessageConverters(Collections.>singletonList(converter)); } @Test @@ -136,9 +122,9 @@ public class RestTemplateTests { @Test public void getForObject() throws Exception { - expect(converter.supports(String.class)).andReturn(true).times(2); + expect(converter.canRead(String.class, null)).andReturn(true); MediaType textPlain = new MediaType("text", "plain"); - expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(textPlain)).times(2); + expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(textPlain)); expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.GET)).andReturn(request); HttpHeaders requestHeaders = new HttpHeaders(); expect(request.getHeaders()).andReturn(requestHeaders); @@ -147,6 +133,7 @@ public class RestTemplateTests { HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.setContentType(textPlain); expect(response.getHeaders()).andReturn(responseHeaders); + expect(converter.canRead(String.class, textPlain)).andReturn(true); String expected = "Hello World"; expect(converter.read(String.class, response)).andReturn(expected); response.close(); @@ -160,28 +147,11 @@ public class RestTemplateTests { verifyMocks(); } - @Test - public void getForObjectUnsupportedClass() throws Exception { - expect(converter.supports(String.class)).andReturn(false); - - replayMocks(); - - try { - template.getForObject("http://example.com/{p}", String.class, "resource"); - fail("IllegalArgumentException expected"); - } - catch (IllegalArgumentException ex) { - // expected - } - - verifyMocks(); - } - @Test public void getUnsupportedMediaType() throws Exception { - expect(converter.supports(String.class)).andReturn(true).times(2); + expect(converter.canRead(String.class, null)).andReturn(true); MediaType supportedMediaType = new MediaType("foo", "bar"); - expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(supportedMediaType)).times(2); + expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(supportedMediaType)); expect(requestFactory.createRequest(new URI("http://example.com/resource"), HttpMethod.GET)).andReturn(request); HttpHeaders requestHeaders = new HttpHeaders(); expect(request.getHeaders()).andReturn(requestHeaders); @@ -191,6 +161,7 @@ public class RestTemplateTests { MediaType contentType = new MediaType("bar", "baz"); responseHeaders.setContentType(contentType); expect(response.getHeaders()).andReturn(responseHeaders); + expect(converter.canRead(String.class, contentType)).andReturn(false); response.close(); replayMocks(); @@ -224,10 +195,10 @@ public class RestTemplateTests { @Test public void postForLocation() throws Exception { - expect(converter.supports(String.class)).andReturn(true).times(2); expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(request); String helloWorld = "Hello World"; - converter.write(helloWorld, request); + expect(converter.canWrite(String.class, null)).andReturn(true); + converter.write(helloWorld, null, request); expect(request.execute()).andReturn(response); expect(errorHandler.hasError(response)).andReturn(false); HttpHeaders responseHeaders = new HttpHeaders(); @@ -246,10 +217,10 @@ public class RestTemplateTests { @Test public void postForLocationNoLocation() throws Exception { - expect(converter.supports(String.class)).andReturn(true).times(2); expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(request); String helloWorld = "Hello World"; - converter.write(helloWorld, request); + expect(converter.canWrite(String.class, null)).andReturn(true); + converter.write(helloWorld, null, request); expect(request.execute()).andReturn(response); expect(errorHandler.hasError(response)).andReturn(false); HttpHeaders responseHeaders = new HttpHeaders(); @@ -284,21 +255,22 @@ public class RestTemplateTests { @Test public void postForObject() throws Exception { - expect(converter.supports(String.class)).andReturn(true).times(2); - expect(converter.supports(Integer.class)).andReturn(true).times(2); MediaType textPlain = new MediaType("text", "plain"); - expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(textPlain)).times(2); + expect(converter.canRead(Integer.class, null)).andReturn(true); + expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(textPlain)); expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(this.request); HttpHeaders requestHeaders = new HttpHeaders(); expect(this.request.getHeaders()).andReturn(requestHeaders); String request = "Hello World"; - converter.write(request, this.request); + expect(converter.canWrite(String.class, null)).andReturn(true); + converter.write(request, null, this.request); expect(this.request.execute()).andReturn(response); expect(errorHandler.hasError(response)).andReturn(false); HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.setContentType(textPlain); expect(response.getHeaders()).andReturn(responseHeaders); Integer expected = 42; + expect(converter.canRead(Integer.class, textPlain)).andReturn(true); expect(converter.read(Integer.class, response)).andReturn(expected); response.close(); @@ -313,9 +285,9 @@ public class RestTemplateTests { @Test public void postForObjectNull() throws Exception { - expect(converter.supports(Integer.class)).andReturn(true).times(2); MediaType textPlain = new MediaType("text", "plain"); - expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(textPlain)).times(2); + expect(converter.canRead(Integer.class, null)).andReturn(true); + expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(textPlain)); expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(request); HttpHeaders requestHeaders = new HttpHeaders(); expect(request.getHeaders()).andReturn(requestHeaders).times(2); @@ -324,6 +296,7 @@ public class RestTemplateTests { HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.setContentType(textPlain); expect(response.getHeaders()).andReturn(responseHeaders); + expect(converter.canRead(Integer.class, textPlain)).andReturn(true); expect(converter.read(Integer.class, response)).andReturn(null); response.close(); @@ -336,10 +309,10 @@ public class RestTemplateTests { @Test public void put() throws Exception { - expect(converter.supports(String.class)).andReturn(true).times(2); + expect(converter.canWrite(String.class, null)).andReturn(true); expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.PUT)).andReturn(request); String helloWorld = "Hello World"; - converter.write(helloWorld, request); + converter.write(helloWorld, null, request); expect(request.execute()).andReturn(response); expect(errorHandler.hasError(response)).andReturn(false); response.close(); @@ -402,7 +375,7 @@ public class RestTemplateTests { @Test public void ioException() throws Exception { - expect(converter.supports(String.class)).andReturn(true).times(2); + expect(converter.canRead(String.class, null)).andReturn(true); MediaType mediaType = new MediaType("foo", "bar"); expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(mediaType)); expect(requestFactory.createRequest(new URI("http://example.com/resource"), HttpMethod.GET)).andReturn(request);