HttpMessageConverter.supports() is split into canRead/canWrite.

HttpMessageConverter.write() now allows for a specific content type.
This commit is contained in:
Arjen Poutsma 2009-11-27 13:23:15 +00:00
parent 18c63f70c4
commit dc0613f487
22 changed files with 628 additions and 518 deletions

View File

@ -121,22 +121,24 @@ import org.springframework.web.util.WebUtils;
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Arjen Poutsma * @author Arjen Poutsma
* @since 2.5
* @see #setPathMatcher * @see #setPathMatcher
* @see #setMethodNameResolver * @see #setMethodNameResolver
* @see #setWebBindingInitializer * @see #setWebBindingInitializer
* @see #setSessionAttributeStore * @see #setSessionAttributeStore
* @since 2.5
*/ */
public class AnnotationMethodHandlerAdapter extends WebContentGenerator implements HandlerAdapter, BeanFactoryAware { public class AnnotationMethodHandlerAdapter extends WebContentGenerator implements HandlerAdapter, BeanFactoryAware {
/** /**
* Log category to use when no mapped handler is found for a request. * Log category to use when no mapped handler is found for a request.
*
* @see #pageNotFoundLogger * @see #pageNotFoundLogger
*/ */
public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound"; 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. * Additional logger to use when no mapped handler is found for a request.
*
* @see #PAGE_NOT_FOUND_LOG_CATEGORY * @see #PAGE_NOT_FOUND_LOG_CATEGORY
*/ */
protected static final Log pageNotFoundLogger = LogFactory.getLog(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 ModelAndViewResolver[] customModelAndViewResolvers;
private HttpMessageConverter<?>[] messageConverters = private HttpMessageConverter<?>[] messageConverters =
new HttpMessageConverter[] {new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(), new HttpMessageConverter[]{new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(),
new FormHttpMessageConverter(), new SourceHttpMessageConverter()}; new FormHttpMessageConverter(), new SourceHttpMessageConverter()};
private ConfigurableBeanFactory beanFactory; private ConfigurableBeanFactory beanFactory;
@ -172,17 +174,16 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
private final Map<Class<?>, ServletHandlerMethodResolver> methodResolverCache = private final Map<Class<?>, ServletHandlerMethodResolver> methodResolverCache =
new ConcurrentHashMap<Class<?>, ServletHandlerMethodResolver>(); new ConcurrentHashMap<Class<?>, ServletHandlerMethodResolver>();
public AnnotationMethodHandlerAdapter() { public AnnotationMethodHandlerAdapter() {
// no restriction of HTTP methods by default // no restriction of HTTP methods by default
super(false); super(false);
} }
/** /**
* Set if URL lookup should always use the full path within the current servlet context. Else, the path within the * 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). * current servlet mapping is used if applicable (that is, in the case of a ".../*" servlet mapping in web.xml).
* <p>Default is "false". * <p>Default is "false".
*
* @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath * @see org.springframework.web.util.UrlPathHelper#setAlwaysUseFullPath
*/ */
public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { 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 <i>undecoded</i> by the Servlet API, in * Set if context path and request URI should be URL-decoded. Both are returned <i>undecoded</i> by the Servlet API, in
* contrast to the servlet path. <p>Uses either the request encoding or the default encoding according to the Servlet * contrast to the servlet path. <p>Uses either the request encoding or the default encoding according to the Servlet
* spec (ISO-8859-1). * spec (ISO-8859-1).
*
* @see org.springframework.web.util.UrlPathHelper#setUrlDecode * @see org.springframework.web.util.UrlPathHelper#setUrlDecode
*/ */
public void setUrlDecode(boolean urlDecode) { 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 * Set the PathMatcher implementation to use for matching URL paths against registered URL patterns. Default is
* AntPathMatcher. * AntPathMatcher.
*
* @see org.springframework.util.AntPathMatcher * @see org.springframework.util.AntPathMatcher
*/ */
public void setPathMatcher(PathMatcher pathMatcher) { 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 * Set the MethodNameResolver to use for resolving default handler methods (carrying an empty
* <code>@RequestMapping</code> annotation). * <code>@RequestMapping</code> annotation). <p>Will only kick in when the handler method cannot be resolved uniquely
* <p>Will only kick in when the handler method cannot be resolved uniquely
* through the annotation metadata already. * through the annotation metadata already.
*/ */
public void setMethodNameResolver(MethodNameResolver methodNameResolver) { 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 * Specify a WebBindingInitializer which will apply pre-configured configuration to every DataBinder that this
* DataBinder that this controller uses. * controller uses.
*/ */
public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) { public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) {
this.webBindingInitializer = webBindingInitializer; this.webBindingInitializer = webBindingInitializer;
} }
/** /**
* Specify the strategy to store session attributes with. * Specify the strategy to store session attributes with. <p>Default is {@link org.springframework.web.bind.support.DefaultSessionAttributeStore},
* <p>Default is {@link org.springframework.web.bind.support.DefaultSessionAttributeStore},
* storing session attributes in the HttpSession, using the same attribute name as in the model. * storing session attributes in the HttpSession, using the same attribute name as in the model.
*/ */
public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) { public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) {
@ -248,10 +249,10 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
/** /**
* Cache content produced by <code>@SessionAttributes</code> annotated handlers for the given number of seconds. * Cache content produced by <code>@SessionAttributes</code> annotated handlers for the given number of seconds.
* Default is 0, preventing caching completely. * Default is 0, preventing caching completely. <p>In contrast to the "cacheSeconds" property which will apply to all
* <p>In contrast to the "cacheSeconds" property which will apply to all
* general handlers (but not to <code>@SessionAttributes</code> annotated handlers), this setting will apply to * general handlers (but not to <code>@SessionAttributes</code> annotated handlers), this setting will apply to
* <code>@SessionAttributes</code> annotated handlers only. * <code>@SessionAttributes</code> annotated handlers only.
*
* @see #setCacheSeconds * @see #setCacheSeconds
* @see org.springframework.web.bind.annotation.SessionAttributes * @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 * Set if controller execution should be synchronized on the session, to serialize parallel invocations from the same
* parallel invocations from the same client. * client. <p>More specifically, the execution of each handler method will get synchronized if this flag is "true". The
* <p>More specifically, the execution of each handler method will get synchronized if this * best available session mutex will be used for the synchronization; ideally, this will be a mutex exposed by
* flag is "true". The best available session mutex will be used for the synchronization; * HttpSessionMutexListener. <p>The session mutex is guaranteed to be the same object during the entire lifetime of the
* ideally, this will be a mutex exposed by HttpSessionMutexListener. * session, available under the key defined by the <code>SESSION_MUTEX_ATTRIBUTE</code> constant. It serves as a safe
* <p>The session mutex is guaranteed to be the same object during the entire lifetime of the * reference to synchronize on for locking on the current session. <p>In many cases, the HttpSession reference itself a
* session, available under the key defined by the <code>SESSION_MUTEX_ATTRIBUTE</code> constant. * safe mutex as well, since it will always be the same object reference for the same active logical session. However,
* It serves as a safe reference to synchronize on for locking on the current session. * this is not guaranteed across different servlet containers; the only 100% safe way is a session mutex.
* <p>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.HttpSessionMutexListener
* @see org.springframework.web.util.WebUtils#getSessionMutex(javax.servlet.http.HttpSession) * @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 * Set the ParameterNameDiscoverer to use for resolving method parameter names if needed (e.g. for default attribute
* names). * names). <p>Default is a {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer}.
* <p>Default is a {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer}.
*/ */
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
this.parameterNameDiscoverer = parameterNameDiscoverer; this.parameterNameDiscoverer = parameterNameDiscoverer;
} }
/** /**
* Set a custom WebArgumentResolvers to use for special method parameter types. Such a custom WebArgumentResolver will kick * Set a custom WebArgumentResolvers to use for special method parameter types. Such a custom WebArgumentResolver will
* in first, having a chance to resolve an argument value before the standard argument handling kicks in. * kick in first, having a chance to resolve an argument value before the standard argument handling kicks in.
*/ */
public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) { public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) {
this.customArgumentResolvers = new WebArgumentResolver[]{argumentResolver}; this.customArgumentResolvers = new WebArgumentResolver[]{argumentResolver};
} }
/** /**
* Set one or more custom WebArgumentResolvers to use for special method parameter types. Any such custom WebArgumentResolver * Set one or more custom WebArgumentResolvers to use for special method parameter types. Any such custom
* will kick in first, having a chance to resolve an argument value before the standard argument handling kicks in. * 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) { public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) {
this.customArgumentResolvers = argumentResolvers; this.customArgumentResolvers = argumentResolvers;
} }
/** /**
* Set a custom ModelAndViewResolvers to use for special method return types. Such a custom ModelAndViewResolver will kick * Set a custom ModelAndViewResolvers to use for special method return types. Such a custom ModelAndViewResolver will
* in first, having a chance to resolve an return value before the standard ModelAndView handling kicks in. * kick in first, having a chance to resolve an return value before the standard ModelAndView handling kicks in.
*/ */
public void setCustomModelAndViewResolver(ModelAndViewResolver customModelAndViewResolver) { public void setCustomModelAndViewResolver(ModelAndViewResolver customModelAndViewResolver) {
this.customModelAndViewResolvers = new ModelAndViewResolver[]{customModelAndViewResolver}; this.customModelAndViewResolvers = new ModelAndViewResolver[]{customModelAndViewResolver};
} }
/** /**
* Set one or more custom ModelAndViewResolvers to use for special method return types. Any such custom ModelAndViewResolver * Set one or more custom ModelAndViewResolvers to use for special method return types. Any such custom
* will kick in first, having a chance to resolve an return value before the standard ModelAndView handling kicks in. * 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) { public void setCustomModelAndViewResolvers(ModelAndViewResolver[] customModelAndViewResolvers) {
this.customModelAndViewResolvers = customModelAndViewResolvers; this.customModelAndViewResolvers = customModelAndViewResolvers;
@ -334,7 +334,6 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
} }
} }
public boolean supports(Object handler) { public boolean supports(Object handler) {
return getMethodResolver(handler).hasHandlerMethods(); return getMethodResolver(handler).hasHandlerMethods();
} }
@ -386,9 +385,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
return -1; return -1;
} }
/** /** Build a HandlerMethodResolver for the given handler type. */
* Build a HandlerMethodResolver for the given handler type.
*/
private ServletHandlerMethodResolver getMethodResolver(Object handler) { private ServletHandlerMethodResolver getMethodResolver(Object handler) {
Class handlerClass = ClassUtils.getUserClass(handler); Class handlerClass = ClassUtils.getUserClass(handler);
ServletHandlerMethodResolver resolver = this.methodResolverCache.get(handlerClass); ServletHandlerMethodResolver resolver = this.methodResolverCache.get(handlerClass);
@ -399,10 +396,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
return resolver; return resolver;
} }
/** Servlet-specific subclass of {@link HandlerMethodResolver}. */
/**
* Servlet-specific subclass of {@link HandlerMethodResolver}.
*/
private class ServletHandlerMethodResolver extends HandlerMethodResolver { private class ServletHandlerMethodResolver extends HandlerMethodResolver {
private ServletHandlerMethodResolver(Class<?> handlerType) { private ServletHandlerMethodResolver(Class<?> handlerType) {
@ -521,17 +515,10 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
/** /**
* Determines the matched pattern for the given methodLevelPattern and path. * Determines the matched pattern for the given methodLevelPattern and path.
* *
* <p>Uses the following algorithm: * <p>Uses the following algorithm: <ol> <li>If there is a type-level mapping with path information, it is {@linkplain
* <ol> * PathMatcher#combine(String, String) combined} with the method-level pattern. <li>If there is a {@linkplain
* <li>If there is a type-level mapping with path information, it is * HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE best matching pattern} in the request, it is combined with the
* {@linkplain PathMatcher#combine(String, String) combined} with the method-level pattern. * method-level pattern. <li>Otherwise,
* <li>If there is a {@linkplain HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE best matching pattern} in the
* request, it is combined with the method-level pattern.
* <li>Otherwise,
* @param methodLevelPattern
* @param lookupPath
* @param request
* @return
*/ */
private String getMatchedPattern(String methodLevelPattern, String lookupPath, HttpServletRequest request) { private String getMatchedPattern(String methodLevelPattern, String lookupPath, HttpServletRequest request) {
if (hasTypeLevelMapping() && (!ObjectUtils.isEmpty(getTypeLevelMapping().value()))) { 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); String bestMatchingPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
if (StringUtils.hasText(bestMatchingPattern)) { if (StringUtils.hasText(bestMatchingPattern)) {
String combinedPattern = pathMatcher.combine(bestMatchingPattern, methodLevelPattern); String combinedPattern = pathMatcher.combine(bestMatchingPattern, methodLevelPattern);
if (!combinedPattern.equals(bestMatchingPattern) && (isPathMatchInternal(combinedPattern, lookupPath))) { if (!combinedPattern.equals(bestMatchingPattern) &&
(isPathMatchInternal(combinedPattern, lookupPath))) {
return combinedPattern; 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 class ServletHandlerMethodInvoker extends HandlerMethodInvoker {
private boolean responseArgumentUsed = false; private boolean responseArgumentUsed = false;
@ -797,7 +782,8 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
} }
@SuppressWarnings("unchecked") @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()); HttpInputMessage inputMessage = new ServletServerHttpRequest(webRequest.getRequest());
List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept(); List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept();
if (acceptedMediaTypes.isEmpty()) { if (acceptedMediaTypes.isEmpty()) {
@ -809,16 +795,11 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
if (messageConverters != null) { if (messageConverters != null) {
for (HttpMessageConverter messageConverter : messageConverters) { for (HttpMessageConverter messageConverter : messageConverters) {
allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
if (messageConverter.supports(returnValueType)) { for (MediaType acceptedMediaType : acceptedMediaTypes) {
for (Object o : messageConverter.getSupportedMediaTypes()) { if (messageConverter.canWrite(returnValueType, acceptedMediaType)) {
MediaType supportedMediaType = (MediaType) o; messageConverter.write(returnValue, null, outputMessage);
for (MediaType acceptedMediaType : acceptedMediaTypes) { this.responseArgumentUsed = true;
if (acceptedMediaType.includes(supportedMediaType)) { return;
messageConverter.write(returnValue, outputMessage);
this.responseArgumentUsed = true;
return;
}
}
} }
} }
} }
@ -827,7 +808,6 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen
} }
} }
static class RequestMappingInfo { static class RequestMappingInfo {
String[] paths = new String[0]; 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 * Comparator capable of sorting {@link RequestMappingInfo}s (RHIs) so that sorting a list with this comparator will
* result in: <ul> <li>RHIs with {@linkplain RequestMappingInfo#matchedPaths better matched paths} take prescedence * result in: <ul> <li>RHIs with {@linkplain RequestMappingInfo#matchedPaths better matched paths} take prescedence

View File

@ -30,12 +30,12 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.Iterator;
import javax.servlet.ServletConfig; import javax.servlet.ServletConfig;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.ServletException; import javax.servlet.ServletException;
@ -52,10 +52,10 @@ import org.junit.Test;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.aop.interceptor.SimpleTraceInterceptor; import org.springframework.aop.interceptor.SimpleTraceInterceptor;
import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.BeansException;
import org.springframework.beans.DerivedTestBean; import org.springframework.beans.DerivedTestBean;
import org.springframework.beans.ITestBean; import org.springframework.beans.ITestBean;
import org.springframework.beans.TestBean; import org.springframework.beans.TestBean;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; 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.beans.propertyeditors.CustomDateEditor;
import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage; import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; 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.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletConfig; import org.springframework.mock.web.MockServletConfig;
@ -79,9 +81,9 @@ import org.springframework.stereotype.Controller;
import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.SerializationTestUtils; import org.springframework.util.SerializationTestUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors; import org.springframework.validation.Errors;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
@ -445,7 +447,8 @@ public class ServletAnnotationControllerTests {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setBeanFactory(wac.getBeanFactory()); autoProxyCreator.setBeanFactory(wac.getBeanFactory());
wac.getBeanFactory().addBeanPostProcessor(autoProxyCreator); wac.getBeanFactory().addBeanPostProcessor(autoProxyCreator);
wac.getBeanFactory().registerSingleton("advisor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor())); wac.getBeanFactory()
.registerSingleton("advisor", new DefaultPointcutAdvisor(new SimpleTraceInterceptor()));
wac.refresh(); wac.refresh();
return wac; return wac;
} }
@ -639,8 +642,8 @@ public class ServletAnnotationControllerTests {
servlet.service(request, response); servlet.service(request, response);
assertEquals("mySurpriseView", response.getContentAsString()); assertEquals("mySurpriseView", response.getContentAsString());
MyParameterDispatchingController deserialized = (MyParameterDispatchingController) MyParameterDispatchingController deserialized = (MyParameterDispatchingController) SerializationTestUtils
SerializationTestUtils.serializeAndDeserialize(servlet.getWebApplicationContext().getBean("controller")); .serializeAndDeserialize(servlet.getWebApplicationContext().getBean("controller"));
assertNotNull(deserialized.request); assertNotNull(deserialized.request);
assertNotNull(deserialized.session); assertNotNull(deserialized.session);
} }
@ -947,7 +950,23 @@ public class ServletAnnotationControllerTests {
@Test @Test
public void responseBodyNoAcceptableMediaType() throws ServletException, IOException { 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"); MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/something");
String requestBody = "Hello World"; String requestBody = "Hello World";
@ -975,7 +994,19 @@ public class ServletAnnotationControllerTests {
@Test @Test
public void unsupportedRequestBody() throws ServletException, IOException { 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"); MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/something");
String requestBody = "Hello World"; String requestBody = "Hello World";
@ -1061,11 +1092,9 @@ public class ServletAnnotationControllerTests {
@Override @Override
protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) {
GenericWebApplicationContext wac = new GenericWebApplicationContext(); GenericWebApplicationContext wac = new GenericWebApplicationContext();
wac.registerBeanDefinition("controller", wac.registerBeanDefinition("controller", new RootBeanDefinition(ModelAndViewResolverController.class));
new RootBeanDefinition(ModelAndViewResolverController.class));
RootBeanDefinition adapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class); RootBeanDefinition adapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class);
adapterDef.getPropertyValues() adapterDef.getPropertyValues().add("customModelAndViewResolver", new MyModelAndViewResolver());
.add("customModelAndViewResolver", new MyModelAndViewResolver());
wac.registerBeanDefinition("handlerAdapter", adapterDef); wac.registerBeanDefinition("handlerAdapter", adapterDef);
wac.refresh(); wac.refresh();
return wac; return wac;
@ -1086,7 +1115,7 @@ public class ServletAnnotationControllerTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test");
request.setCookies(new Cookie("date", "2008-11-18")); request.setCookies(new Cookie("date", "2008-11-18"));
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
servlet.service(request, response); servlet.service(request, response);
assertEquals("test-108", response.getContentAsString()); assertEquals("test-108", response.getContentAsString());
} }
@ -1096,7 +1125,7 @@ public class ServletAnnotationControllerTests {
initServlet(AmbiguousParamsController.class); initServlet(AmbiguousParamsController.class);
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
servlet.service(request, response); servlet.service(request, response);
assertEquals("noParams", response.getContentAsString()); assertEquals("noParams", response.getContentAsString());
@ -1116,7 +1145,6 @@ public class ServletAnnotationControllerTests {
servlet.service(request, response); servlet.service(request, response);
} }
@Test @Test
public void requestParamMap() throws Exception { public void requestParamMap() throws Exception {
initServlet(RequestParamMapController.class); initServlet(RequestParamMapController.class);
@ -1229,11 +1257,9 @@ public class ServletAnnotationControllerTests {
assertEquals("create", response.getContentAsString()); assertEquals("create", response.getContentAsString());
} }
/* /*
* Controllers * Controllers
*/ */
@RequestMapping("/myPath.do") @RequestMapping("/myPath.do")
private static class MyController extends AbstractController { private static class MyController extends AbstractController {
@ -1470,7 +1496,8 @@ public class ServletAnnotationControllerTests {
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ModelAttribute("myCommand") @ModelAttribute("myCommand")
private ValidTestBean createTestBean(@RequestParam T defaultName, private ValidTestBean createTestBean(@RequestParam T defaultName,
Map<String, Object> model, @RequestParam Date date) { Map<String, Object> model,
@RequestParam Date date) {
model.put("myKey", "myOriginalValue"); model.put("myKey", "myOriginalValue");
ValidTestBean tb = new ValidTestBean(); ValidTestBean tb = new ValidTestBean();
tb.setName(defaultName.getClass().getSimpleName() + ":" + defaultName.toString()); tb.setName(defaultName.getClass().getSimpleName() + ":" + defaultName.toString());
@ -1740,9 +1767,10 @@ public class ServletAnnotationControllerTests {
} }
List<TestBean> testBeans = (List<TestBean>) model.get("testBeanList"); List<TestBean> testBeans = (List<TestBean>) model.get("testBeanList");
if (errors.hasFieldErrors("age")) { if (errors.hasFieldErrors("age")) {
response.getWriter().write(viewName + "-" + tb.getName() + "-" + response.getWriter()
errors.getFieldError("age").getCode() + "-" + testBeans.get(0).getName() + "-" + .write(viewName + "-" + tb.getName() + "-" + errors.getFieldError("age").getCode() +
model.get("myKey") + (model.containsKey("yourKey") ? "-" + model.get("yourKey") : "")); "-" + testBeans.get(0).getName() + "-" + model.get("myKey") +
(model.containsKey("yourKey") ? "-" + model.get("yourKey") : ""));
} }
else { else {
response.getWriter().write(viewName + "-" + tb.getName() + "-" + tb.getAge() + "-" + response.getWriter().write(viewName + "-" + tb.getName() + "-" + tb.getAge() + "-" +
@ -1760,6 +1788,7 @@ public class ServletAnnotationControllerTests {
public String getContentType() { public String getContentType() {
return null; return null;
} }
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) { public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
request.getSession().setAttribute("model", model); request.getSession().setAttribute("model", model);
} }
@ -1787,6 +1816,7 @@ public class ServletAnnotationControllerTests {
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Controller @Controller
public @interface MyControllerAnnotation { public @interface MyControllerAnnotation {
} }
@MyControllerAnnotation @MyControllerAnnotation
@ -1835,8 +1865,8 @@ public class ServletAnnotationControllerTests {
@RequestMapping("/myPath.do") @RequestMapping("/myPath.do")
public void myHandle(@RequestParam(value = "id", defaultValue = "${myKey}") String id, public void myHandle(@RequestParam(value = "id", defaultValue = "${myKey}") String id,
@RequestHeader(defaultValue = "#{systemProperties.myHeader}") String header, @RequestHeader(defaultValue = "#{systemProperties.myHeader}") String header,
@Value("#{request.contextPath}") String contextPath, HttpServletResponse response) @Value("#{request.contextPath}") String contextPath,
throws IOException { HttpServletResponse response) throws IOException {
response.getWriter().write(String.valueOf(id) + "-" + String.valueOf(header) + "-" + contextPath); response.getWriter().write(String.valueOf(id) + "-" + String.valueOf(header) + "-" + contextPath);
} }
} }
@ -1873,7 +1903,6 @@ public class ServletAnnotationControllerTests {
} }
} }
@Controller @Controller
public static class PathOrderingController { public static class PathOrderingController {
@ -1888,7 +1917,6 @@ public class ServletAnnotationControllerTests {
} }
} }
@Controller @Controller
public static class RequestBodyController { public static class RequestBodyController {
@ -1901,7 +1929,11 @@ public class ServletAnnotationControllerTests {
public static class MyMessageConverter implements HttpMessageConverter { 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; return true;
} }
@ -1914,7 +1946,7 @@ public class ServletAnnotationControllerTests {
throw new HttpMessageNotReadableException("Could not read"); 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 { throws IOException, HttpMessageNotWritableException {
throw new UnsupportedOperationException("Not implemented"); throw new UnsupportedOperationException("Not implemented");
} }
@ -1955,8 +1987,11 @@ public class ServletAnnotationControllerTests {
public static class MyModelAndViewResolver implements ModelAndViewResolver { public static class MyModelAndViewResolver implements ModelAndViewResolver {
public ModelAndView resolveModelAndView(Method handlerMethod, Class handlerType, public ModelAndView resolveModelAndView(Method handlerMethod,
Object returnValue, ExtendedModelMap implicitModel, NativeWebRequest webRequest) { Class handlerType,
Object returnValue,
ExtendedModelMap implicitModel,
NativeWebRequest webRequest) {
if (returnValue instanceof MySpecialArg) { if (returnValue instanceof MySpecialArg) {
return new ModelAndView(new View() { return new ModelAndView(new View() {
public String getContentType() { public String getContentType() {
@ -2002,8 +2037,7 @@ public class ServletAnnotationControllerTests {
} }
@RequestMapping(method = RequestMethod.GET) @RequestMapping(method = RequestMethod.GET)
public void handle(@CookieValue("date") Date date, Writer writer) public void handle(@CookieValue("date") Date date, Writer writer) throws IOException {
throws IOException {
assertEquals("Invalid path variable value", new Date(108, 10, 18), date); assertEquals("Invalid path variable value", new Date(108, 10, 18), date);
writer.write("test-" + date.getYear()); writer.write("test-" + date.getYear());
} }
@ -2043,7 +2077,8 @@ public class ServletAnnotationControllerTests {
} }
@RequestMapping("/multiValueMap") @RequestMapping("/multiValueMap")
public void multiValueMap(@RequestParam MultiValueMap<String, String> params, Writer writer) throws IOException { public void multiValueMap(@RequestParam MultiValueMap<String, String> params, Writer writer)
throws IOException {
for (Iterator<Map.Entry<String, List<String>>> it1 = params.entrySet().iterator(); it1.hasNext();) { for (Iterator<Map.Entry<String, List<String>>> it1 = params.entrySet().iterator(); it1.hasNext();) {
Map.Entry<String, List<String>> entry = it1.next(); Map.Entry<String, List<String>> entry = it1.next();
writer.write(entry.getKey() + "=["); writer.write(entry.getKey() + "=[");
@ -2078,7 +2113,8 @@ public class ServletAnnotationControllerTests {
} }
@RequestMapping("/multiValueMap") @RequestMapping("/multiValueMap")
public void multiValueMap(@RequestHeader MultiValueMap<String, String> headers, Writer writer) throws IOException { public void multiValueMap(@RequestHeader MultiValueMap<String, String> headers, Writer writer)
throws IOException {
for (Iterator<Map.Entry<String, List<String>>> it1 = headers.entrySet().iterator(); it1.hasNext();) { for (Iterator<Map.Entry<String, List<String>>> it1 = headers.entrySet().iterator(); it1.hasNext();) {
Map.Entry<String, List<String>> entry = it1.next(); Map.Entry<String, List<String>> entry = it1.next();
writer.write(entry.getKey() + "=["); writer.write(entry.getKey() + "=[");

View File

@ -56,14 +56,22 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
protected AbstractHttpMessageConverter() { 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) { 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) { protected AbstractHttpMessageConverter(MediaType... supportedMediaTypes) {
this.supportedMediaTypes = Arrays.asList(supportedMediaTypes); setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
} }
/** Set the list of {@link MediaType} objects supported by this converter. */ /** Set the list of {@link MediaType} objects supported by this converter. */
@ -77,8 +85,59 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
} }
/** /**
* {@inheritDoc} <p>This implementation simple delegates to {@link #readInternal(Class, HttpInputMessage)}. Future * {@inheritDoc}
* implementations might add some default behavior, however. *
* <p>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<? extends T> clazz, MediaType mediaType) {
return supports(clazz) && isSupported(mediaType);
}
/**
* {@inheritDoc}
*
* <p>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<? extends T> 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 <code>true</code> if supported; <code>false</code> otherwise
*/
protected abstract boolean supports(Class<? extends T> clazz);
/**
* {@inheritDoc}
*
* <p>This implementation simple delegates to {@link #readInternal(Class, HttpInputMessage)}. Future implementations
* might add some default behavior, however.
*/ */
public final T read(Class<T> clazz, HttpInputMessage inputMessage) throws IOException { public final T read(Class<T> clazz, HttpInputMessage inputMessage) throws IOException {
return readInternal(clazz, inputMessage); return readInternal(clazz, inputMessage);
@ -97,17 +156,22 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
throws IOException, HttpMessageNotReadableException; throws IOException, HttpMessageNotReadableException;
/** /**
* {@inheritDoc} <p>This implementation delegates to {@link #getContentType(Object)} and {@link * {@inheritDoc}
* #getContentLength(Object)}, and sets the corresponding headers on the output message. It then calls {@link *
* #writeInternal(Object, HttpOutputMessage)}. * <p>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(); HttpHeaders headers = outputMessage.getHeaders();
MediaType contentType = getContentType(t); if (contentType == null) {
contentType = getDefaultContentType(t);
}
if (contentType != null) { if (contentType != null) {
headers.setContentType(contentType); headers.setContentType(contentType);
} }
Long contentLength = getContentLength(t); Long contentLength = getContentLength(t, contentType);
if (contentLength != null) { if (contentLength != null) {
headers.setContentLength(contentLength); headers.setContentLength(contentLength);
} }
@ -116,30 +180,35 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
} }
/** /**
* Returns the content type for the given type. <p>By default, this returns the first element of the {@link * Returns the default content type for the given type. Called when {@link #write} is invoked without a specified
* #setSupportedMediaTypes(List) supportedMediaTypes} property, if any. Can be overriden in subclasses. * content type parameter.
*
* <p>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 * @param t the type to return the content type for
* @return the content type, or <code>null</code> if not known * @return the content type, or <code>null</code> if not known
*/ */
protected MediaType getContentType(T t) { protected MediaType getDefaultContentType(T t) {
List<MediaType> mediaTypes = getSupportedMediaTypes(); List<MediaType> mediaTypes = getSupportedMediaTypes();
return (!mediaTypes.isEmpty() ? mediaTypes.get(0) : null); return (!mediaTypes.isEmpty() ? mediaTypes.get(0) : null);
} }
/** /**
* Returns the content length for the given type. <p>By default, this returns <code>null</code>. Can be overriden in * Returns the content length for the given type.
*
* <p>By default, this returns {@code null}, meaning that the content length is unknown. Can be overriden in
* subclasses. * subclasses.
* *
* @param t the type to return the content length for * @param t the type to return the content length for
* @return the content length, or <code>null</code> 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; 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 t the object to write to the output message
* @param outputMessage the message to write to * @param outputMessage the message to write to

View File

@ -22,6 +22,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import javax.imageio.IIOImage; import javax.imageio.IIOImage;
@ -48,8 +49,7 @@ import org.springframework.util.Assert;
* <p>By default, this converter can read all media types that are supported by the {@linkplain * <p>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 * 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 * {@linkplain javax.imageio.ImageIO#getWriterMIMETypes() registered image writer}. This behavior can be overriden by
* setting the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes} and {@link * setting the #setContentType(org.springframework.http.MediaType) contentType} properties.
* #setContentType(org.springframework.http.MediaType) contentType} properties respectively.
* *
* <p>If the {@link #setCacheDir(java.io.File) cacheDir} property is set to an existing directory, this converter will * <p>If the {@link #setCacheDir(java.io.File) cacheDir} property is set to an existing directory, this converter will
* cache image data. * cache image data.
@ -60,57 +60,50 @@ import org.springframework.util.Assert;
* @author Arjen Poutsma * @author Arjen Poutsma
* @since 3.0 * @since 3.0
*/ */
public class BufferedImageHttpMessageConverter extends AbstractHttpMessageConverter<BufferedImage> { public class BufferedImageHttpMessageConverter implements HttpMessageConverter<BufferedImage> {
private MediaType contentType; private List<MediaType> readableMediaTypes = new ArrayList<MediaType>();
private MediaType defaultContentType;
private File cacheDir; private File cacheDir;
public BufferedImageHttpMessageConverter() { public BufferedImageHttpMessageConverter() {
String[] readerMediaTypes = ImageIO.getReaderMIMETypes(); String[] readerMediaTypes = ImageIO.getReaderMIMETypes();
List<MediaType> supportedMediaTypes = new ArrayList<MediaType>(readerMediaTypes.length);
for (String mediaType : readerMediaTypes) { for (String mediaType : readerMediaTypes) {
supportedMediaTypes.add(MediaType.parseMediaType(mediaType)); readableMediaTypes.add(MediaType.parseMediaType(mediaType));
} }
setSupportedMediaTypes(supportedMediaTypes);
String[] writerMediaTypes = ImageIO.getWriterMIMETypes(); String[] writerMediaTypes = ImageIO.getWriterMIMETypes();
if (writerMediaTypes.length > 0) { 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 MediaType getDefaultContentType() {
public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) { return defaultContentType;
Assert.notEmpty(supportedMediaTypes, "'supportedMediaTypes' must not be empty");
for (MediaType supportedMediaType : supportedMediaTypes) {
Iterator<ImageReader> 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);
} }
/** /**
* 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 * @throws IllegalArgumentException if the given content type is not supported by the Java Image I/O API
*/ */
public void setContentType(MediaType contentType) { public void setDefaultContentType(MediaType defaultContentType) {
Assert.notNull(contentType, "'contentType' must not be null"); Assert.notNull(defaultContentType, "'contentType' must not be null");
Iterator<ImageWriter> imageWriters = ImageIO.getImageWritersByMIMEType(contentType.toString()); Iterator<ImageWriter> imageWriters = ImageIO.getImageWritersByMIMEType(defaultContentType.toString());
if (!imageWriters.hasNext()) { if (!imageWriters.hasNext()) {
throw new IllegalArgumentException( 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. */ /** 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; this.cacheDir = cacheDir;
} }
public boolean supports(Class<? extends BufferedImage> clazz) { public boolean canRead(Class<? extends BufferedImage> clazz, MediaType mediaType) {
return BufferedImage.class.equals(clazz); if (BufferedImage.class.equals(clazz)) {
return isReadable(mediaType);
}
else {
return false;
}
} }
@Override private boolean isReadable(MediaType mediaType) {
public BufferedImage readInternal(Class<BufferedImage> clazz, HttpInputMessage inputMessage) throws IOException { if (mediaType == null) {
return true;
}
Iterator<ImageReader> imageReaders = ImageIO.getImageReadersByMIMEType(mediaType.toString());
return imageReaders.hasNext();
}
public boolean canWrite(Class<? extends BufferedImage> 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<ImageWriter> imageWriters = ImageIO.getImageWritersByMIMEType(mediaType.toString());
return imageWriters.hasNext();
}
public List<MediaType> getSupportedMediaTypes() {
return Collections.unmodifiableList(readableMediaTypes);
}
public BufferedImage read(Class<BufferedImage> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
ImageInputStream imageInputStream = null; ImageInputStream imageInputStream = null;
ImageReader imageReader = null; ImageReader imageReader = null;
try { try {
@ -168,13 +195,14 @@ public class BufferedImageHttpMessageConverter extends AbstractHttpMessageConver
} }
} }
@Override public void write(BufferedImage image, MediaType contentType, HttpOutputMessage outputMessage)
protected MediaType getContentType(BufferedImage image) { throws IOException, HttpMessageNotWritableException {
return contentType; if (contentType == null) {
} contentType = getDefaultContentType();
}
@Override Assert.notNull(contentType,
protected void writeInternal(BufferedImage image, HttpOutputMessage outputMessage) throws IOException { "Count not determine Content-Type, set one using the 'defaultContentType' property");
outputMessage.getHeaders().setContentType(contentType);
ImageOutputStream imageOutputStream = null; ImageOutputStream imageOutputStream = null;
ImageWriter imageWriter = null; ImageWriter imageWriter = null;
try { try {

View File

@ -29,8 +29,7 @@ import org.springframework.util.FileCopyUtils;
* *
* <p>By default, this converter supports all media types (<code>&#42;&#47;&#42;</code>), and writes with a {@code * <p>By default, this converter supports all media types (<code>&#42;&#47;&#42;</code>), and writes with a {@code
* Content-Type} of {@code application/octet-stream}. This can be overridden by setting the {@link * Content-Type} of {@code application/octet-stream}. This can be overridden by setting the {@link
* #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property, and overridding {@link * #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property.
* #getContentType(byte[])}.
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @since 3.0 * @since 3.0
@ -39,9 +38,10 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter<
/** Creates a new instance of the {@code ByteArrayHttpMessageConverter}. */ /** Creates a new instance of the {@code ByteArrayHttpMessageConverter}. */
public ByteArrayHttpMessageConverter() { public ByteArrayHttpMessageConverter() {
super(MediaType.ALL); super(new MediaType("application", "octet-stream"), MediaType.ALL);
} }
@Override
public boolean supports(Class<? extends byte[]> clazz) { public boolean supports(Class<? extends byte[]> clazz) {
return byte[].class.equals(clazz); return byte[].class.equals(clazz);
} }
@ -60,12 +60,7 @@ public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter<
} }
@Override @Override
protected MediaType getContentType(byte[] bytes) { protected Long getContentLength(byte[] bytes, MediaType contentType) {
return new MediaType("application", "octet-stream");
}
@Override
protected Long getContentLength(byte[] bytes) {
return (long) bytes.length; return (long) bytes.length;
} }

View File

@ -55,6 +55,7 @@ public class FormHttpMessageConverter extends AbstractHttpMessageConverter<Multi
super(new MediaType("application", "x-www-form-urlencoded")); super(new MediaType("application", "x-www-form-urlencoded"));
} }
@Override
public boolean supports(Class<? extends MultiValueMap<String, String>> clazz) { public boolean supports(Class<? extends MultiValueMap<String, String>> clazz) {
return MultiValueMap.class.isAssignableFrom(clazz); return MultiValueMap.class.isAssignableFrom(clazz);
} }
@ -87,7 +88,7 @@ public class FormHttpMessageConverter extends AbstractHttpMessageConverter<Multi
@Override @Override
protected void writeInternal(MultiValueMap<String, String> form, HttpOutputMessage outputMessage) protected void writeInternal(MultiValueMap<String, String> form, HttpOutputMessage outputMessage)
throws IOException { throws IOException {
MediaType contentType = getContentType(form); MediaType contentType = getDefaultContentType(form);
Charset charset = contentType.getCharSet() != null ? contentType.getCharSet() : DEFAULT_CHARSET; Charset charset = contentType.getCharSet() != null ? contentType.getCharSet() : DEFAULT_CHARSET;
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
for (Iterator<Map.Entry<String, List<String>>> entryIterator = form.entrySet().iterator(); for (Iterator<Map.Entry<String, List<String>>> entryIterator = form.entrySet().iterator();

View File

@ -32,20 +32,35 @@ import org.springframework.http.MediaType;
public interface HttpMessageConverter<T> { public interface HttpMessageConverter<T> {
/** /**
* 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 * @param clazz the class to test for readability
* @return <code>true</code> if supported; <code>false</code> otherwise * @param mediaType the media type to read, can be {@code null} if not specified
* @return <code>true</code> if readable; <code>false</code> otherwise
*/ */
boolean supports(Class<? extends T> clazz); boolean canRead(Class<? extends T> 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 <code>true</code> if writable; <code>false</code> otherwise
*/
boolean canWrite(Class<? extends T> clazz, MediaType mediaType);
/**
* Return the list of {@link MediaType} objects supported by this converter.
*
* @return the list of supported media types
*/
List<MediaType> getSupportedMediaTypes(); List<MediaType> getSupportedMediaTypes();
/** /**
* Read an object of the given type form the given input message, and returns it. * 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 * @param inputMessage the HTTP input message to read from
* @return the converted object * @return the converted object
* @throws IOException in case of I/O errors * @throws IOException in case of I/O errors
@ -56,11 +71,16 @@ public interface HttpMessageConverter<T> {
/** /**
* Write an given object to the given output message. * 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 * @param outputMessage the message to write to
* @throws IOException in case of I/O errors * @throws IOException in case of I/O errors
* @throws HttpMessageNotWritableException in case of conversion 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;
} }

View File

@ -32,7 +32,7 @@ import org.springframework.util.FileCopyUtils;
/** /**
* Implementation of {@link HttpMessageConverter} that can read and write strings. * Implementation of {@link HttpMessageConverter} that can read and write strings.
* *
* <p>By default, this converter supports all text media types (<code>text&#47;&#42;</code>), and writes with a {@code * <p>By default, this converter supports all media types (<code>&#42;&#47;&#42;</code>), and writes with a {@code
* Content-Type} of {@code text/plain}. This can be overridden by setting the {@link * Content-Type} of {@code text/plain}. This can be overridden by setting the {@link
* #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property. * #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property.
* *
@ -46,10 +46,11 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str
private final List<Charset> availableCharsets; private final List<Charset> availableCharsets;
public StringHttpMessageConverter() { 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>(Charset.availableCharsets().values()); this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values());
} }
@Override
public boolean supports(Class<? extends String> clazz) { public boolean supports(Class<? extends String> clazz) {
return String.class.equals(clazz); return String.class.equals(clazz);
} }
@ -62,9 +63,9 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str
} }
@Override @Override
protected Long getContentLength(String s) { protected Long getContentLength(String s, MediaType contentType) {
Charset charset = getContentType(s).getCharSet(); if (contentType != null && contentType.getCharSet() != null) {
if (charset != null) { Charset charset = contentType.getCharSet();
try { try {
return (long) s.getBytes(charset.name()).length; return (long) s.getBytes(charset.name()).length;
} }
@ -81,14 +82,15 @@ public class StringHttpMessageConverter extends AbstractHttpMessageConverter<Str
@Override @Override
protected void writeInternal(String s, HttpOutputMessage outputMessage) throws IOException { protected void writeInternal(String s, HttpOutputMessage outputMessage) throws IOException {
outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets()); outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
MediaType contentType = getContentType(s); MediaType contentType = outputMessage.getHeaders().getContentType();
Charset charset = contentType.getCharSet() != null ? contentType.getCharSet() : DEFAULT_CHARSET; Charset charset = contentType.getCharSet() != null ? contentType.getCharSet() : DEFAULT_CHARSET;
FileCopyUtils.copy(s, new OutputStreamWriter(outputMessage.getBody(), charset)); FileCopyUtils.copy(s, new OutputStreamWriter(outputMessage.getBody(), charset));
} }
/** /**
* Return the list of supported {@link Charset}. <p>By default, returns {@link Charset#availableCharsets()}. Can be * Return the list of supported {@link Charset}.
* overridden in subclasses. *
* <p>By default, returns {@link Charset#availableCharsets()}. Can be overridden in subclasses.
* *
* @return the list of accepted charsets * @return the list of accepted charsets
*/ */

View File

@ -54,9 +54,7 @@ public class MappingJacksonHttpMessageConverter<T> extends AbstractHttpMessageCo
private boolean prefixJson = false; private boolean prefixJson = false;
/** /** Construct a new {@code BindingJacksonHttpMessageConverter}, */
* Construct a new {@code BindingJacksonHttpMessageConverter},
*/
public MappingJacksonHttpMessageConverter() { public MappingJacksonHttpMessageConverter() {
super(new MediaType("application", "json")); super(new MediaType("application", "json"));
} }
@ -75,9 +73,7 @@ public class MappingJacksonHttpMessageConverter<T> extends AbstractHttpMessageCo
this.objectMapper = objectMapper; 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) { public void setEncoding(JsonEncoding encoding) {
Assert.notNull(encoding, "'encoding' must not be null"); Assert.notNull(encoding, "'encoding' must not be null");
this.encoding = encoding; this.encoding = encoding;
@ -94,6 +90,7 @@ public class MappingJacksonHttpMessageConverter<T> extends AbstractHttpMessageCo
this.prefixJson = prefixJson; this.prefixJson = prefixJson;
} }
@Override
public boolean supports(Class<? extends T> clazz) { public boolean supports(Class<? extends T> clazz) {
return objectMapper.canSerialize(clazz); return objectMapper.canSerialize(clazz);
} }
@ -105,7 +102,7 @@ public class MappingJacksonHttpMessageConverter<T> extends AbstractHttpMessageCo
} }
@Override @Override
protected MediaType getContentType(T t) { protected MediaType getDefaultContentType(T t) {
Charset charset = Charset.forName(encoding.getJavaName()); Charset charset = Charset.forName(encoding.getJavaName());
return new MediaType("application", "json", charset); return new MediaType("application", "json", charset);
} }

View File

@ -36,8 +36,9 @@ import org.springframework.http.converter.HttpMessageConversionException;
* Abstract base class for {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters} that * Abstract base class for {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters} that
* convert from/to XML. * convert from/to XML.
* *
* <p>By default, subclasses of this converter support {@code text/xml} and {@code application/xml}. This can be * <p>By default, subclasses of this converter support {@code text/xml}, {@code application/xml}, and {@code
* overridden by setting the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property. * application/*-xml}. This can be overridden by setting the {@link #setSupportedMediaTypes(java.util.List)
* supportedMediaTypes} property.
* *
* @author Arjen Poutsma * @author Arjen Poutsma
* @since 3.0 * @since 3.0
@ -48,10 +49,10 @@ public abstract class AbstractXmlHttpMessageConverter<T> extends AbstractHttpMes
/** /**
* Protected constructor that sets the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes} to {@code * 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() { 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)}. */ /** Invokes {@link #readFromSource(Class, HttpHeaders, Source)}. */

View File

@ -35,11 +35,19 @@ import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException; 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<T extends Source> extends AbstractXmlHttpMessageConverter<T> { public class SourceHttpMessageConverter<T extends Source> extends AbstractXmlHttpMessageConverter<T> {
@Override
public boolean supports(Class<? extends T> clazz) { public boolean supports(Class<? extends T> clazz) {
return Source.class.isAssignableFrom(clazz); return DOMSource.class.equals(clazz) || SAXSource.class.equals(clazz) || StreamSource.class.equals(clazz) ||
Source.class.equals(clazz);
} }
@Override @Override
@ -52,11 +60,11 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractXmlHtt
return (T) new DOMSource(domResult.getNode()); return (T) new DOMSource(domResult.getNode());
} }
else if (SAXSource.class.equals(clazz)) { else if (SAXSource.class.equals(clazz)) {
ByteArrayInputStream bis = transformToByteArray(source); ByteArrayInputStream bis = transformToByteArrayInputStream(source);
return (T) new SAXSource(new InputSource(bis)); return (T) new SAXSource(new InputSource(bis));
} }
else if (StreamSource.class.equals(clazz) || Source.class.equals(clazz)) { else if (StreamSource.class.equals(clazz) || Source.class.equals(clazz)) {
ByteArrayInputStream bis = transformToByteArray(source); ByteArrayInputStream bis = transformToByteArrayInputStream(source);
return (T) new StreamSource(bis); return (T) new StreamSource(bis);
} }
else { else {
@ -65,11 +73,12 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractXmlHtt
} }
} }
catch (TransformerException ex) { 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(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
transform(source, new StreamResult(bos)); transform(source, new StreamResult(bos));
return new ByteArrayInputStream(bos.toByteArray()); return new ByteArrayInputStream(bos.toByteArray());

View File

@ -23,11 +23,11 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.LinkedHashMap;
import java.util.Iterator;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
@ -40,17 +40,17 @@ import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors; import org.springframework.validation.Errors;
import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpMediaTypeNotSupportedException;
@ -73,16 +73,16 @@ import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.request.WebRequest;
/** /**
* Support class for invoking an annotated handler method. Operates on the introspection results of a * Support class for invoking an annotated handler method. Operates on the introspection results of a {@link
* {@link HandlerMethodResolver} for a specific handler type. * HandlerMethodResolver} for a specific handler type.
* *
* <p>Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter} and * <p>Used by {@link org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter} and {@link
* {@link org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}. * org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter}.
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Arjen Poutsma * @author Arjen Poutsma
* @since 2.5.2
* @see #invokeHandlerMethod * @see #invokeHandlerMethod
* @since 2.5.2
*/ */
public class HandlerMethodInvoker { public class HandlerMethodInvoker {
@ -103,7 +103,6 @@ public class HandlerMethodInvoker {
private final SimpleSessionStatus sessionStatus = new SimpleSessionStatus(); private final SimpleSessionStatus sessionStatus = new SimpleSessionStatus();
public HandlerMethodInvoker(HandlerMethodResolver methodResolver) { public HandlerMethodInvoker(HandlerMethodResolver methodResolver) {
this(methodResolver, null); this(methodResolver, null);
} }
@ -112,9 +111,12 @@ public class HandlerMethodInvoker {
this(methodResolver, bindingInitializer, new DefaultSessionAttributeStore(), null, null, null); this(methodResolver, bindingInitializer, new DefaultSessionAttributeStore(), null, null, null);
} }
public HandlerMethodInvoker(HandlerMethodResolver methodResolver, WebBindingInitializer bindingInitializer, public HandlerMethodInvoker(HandlerMethodResolver methodResolver,
SessionAttributeStore sessionAttributeStore, ParameterNameDiscoverer parameterNameDiscoverer, WebBindingInitializer bindingInitializer,
WebArgumentResolver[] customArgumentResolvers, HttpMessageConverter[] messageConverters) { SessionAttributeStore sessionAttributeStore,
ParameterNameDiscoverer parameterNameDiscoverer,
WebArgumentResolver[] customArgumentResolvers,
HttpMessageConverter[] messageConverters) {
this.methodResolver = methodResolver; this.methodResolver = methodResolver;
this.bindingInitializer = bindingInitializer; this.bindingInitializer = bindingInitializer;
@ -124,9 +126,10 @@ public class HandlerMethodInvoker {
this.messageConverters = messageConverters; this.messageConverters = messageConverters;
} }
public final Object invokeHandlerMethod(Method handlerMethod,
public final Object invokeHandlerMethod(Method handlerMethod, Object handler, Object handler,
NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception { NativeWebRequest webRequest,
ExtendedModelMap implicitModel) throws Exception {
Method handlerMethodToInvoke = BridgeMethodResolver.findBridgedMethod(handlerMethod); Method handlerMethodToInvoke = BridgeMethodResolver.findBridgedMethod(handlerMethod);
try { try {
@ -149,10 +152,10 @@ public class HandlerMethodInvoker {
} }
Object attrValue = doInvokeMethod(attributeMethodToInvoke, handler, args); Object attrValue = doInvokeMethod(attributeMethodToInvoke, handler, args);
if ("".equals(attrName)) { if ("".equals(attrName)) {
Class resolvedType = GenericTypeResolver.resolveReturnType( Class resolvedType =
attributeMethodToInvoke, handler.getClass()); GenericTypeResolver.resolveReturnType(attributeMethodToInvoke, handler.getClass());
attrName = Conventions.getVariableNameForReturnType( attrName =
attributeMethodToInvoke, resolvedType, attrValue); Conventions.getVariableNameForReturnType(attributeMethodToInvoke, resolvedType, attrValue);
} }
if (!implicitModel.containsAttribute(attrName)) { if (!implicitModel.containsAttribute(attrName)) {
implicitModel.addAttribute(attrName, attrValue); implicitModel.addAttribute(attrName, attrValue);
@ -171,8 +174,10 @@ public class HandlerMethodInvoker {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Object[] resolveHandlerArguments(Method handlerMethod, Object handler, private Object[] resolveHandlerArguments(Method handlerMethod,
NativeWebRequest webRequest, ExtendedModelMap implicitModel) throws Exception { Object handler,
NativeWebRequest webRequest,
ExtendedModelMap implicitModel) throws Exception {
Class[] paramTypes = handlerMethod.getParameterTypes(); Class[] paramTypes = handlerMethod.getParameterTypes();
Object[] args = new Object[paramTypes.length]; Object[] args = new Object[paramTypes.length];
@ -287,7 +292,8 @@ public class HandlerMethodInvoker {
args[i] = resolvePathVariable(pathVarName, methodParam, webRequest, handler); args[i] = resolvePathVariable(pathVarName, methodParam, webRequest, handler);
} }
else if (attrName != null) { 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])); boolean assignBindingResult = (args.length > i + 1 && Errors.class.isAssignableFrom(paramTypes[i + 1]));
if (binder.getTarget() != null) { if (binder.getTarget() != null) {
doBind(binder, webRequest, validate, !assignBindingResult); doBind(binder, webRequest, validate, !assignBindingResult);
@ -334,8 +340,10 @@ public class HandlerMethodInvoker {
} }
} }
private Object[] resolveInitBinderArguments(Object handler, Method initBinderMethod, private Object[] resolveInitBinderArguments(Object handler,
WebDataBinder binder, NativeWebRequest webRequest) throws Exception { Method initBinderMethod,
WebDataBinder binder,
NativeWebRequest webRequest) throws Exception {
Class[] initBinderParams = initBinderMethod.getParameterTypes(); Class[] initBinderParams = initBinderMethod.getParameterTypes();
Object[] initBinderArgs = new Object[initBinderParams.length]; Object[] initBinderArgs = new Object[initBinderParams.length];
@ -390,8 +398,8 @@ public class HandlerMethodInvoker {
} }
if (paramName != null) { if (paramName != null) {
initBinderArgs[i] = resolveRequestParam( initBinderArgs[i] =
paramName, paramRequired, paramDefaultValue, methodParam, webRequest, null); resolveRequestParam(paramName, paramRequired, paramDefaultValue, methodParam, webRequest, null);
} }
else if (pathVarName != null) { else if (pathVarName != null) {
initBinderArgs[i] = resolvePathVariable(pathVarName, methodParam, webRequest, null); initBinderArgs[i] = resolvePathVariable(pathVarName, methodParam, webRequest, null);
@ -402,9 +410,12 @@ public class HandlerMethodInvoker {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Object resolveRequestParam(String paramName, boolean required, String defaultValue, private Object resolveRequestParam(String paramName,
MethodParameter methodParam, NativeWebRequest webRequest, Object handlerForInitBinderCall) boolean required,
throws Exception { String defaultValue,
MethodParameter methodParam,
NativeWebRequest webRequest,
Object handlerForInitBinderCall) throws Exception {
Class<?> paramType = methodParam.getParameterType(); Class<?> paramType = methodParam.getParameterType();
if (Map.class.isAssignableFrom(paramType)) { if (Map.class.isAssignableFrom(paramType)) {
@ -460,9 +471,12 @@ public class HandlerMethodInvoker {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Object resolveRequestHeader(String headerName, boolean required, String defaultValue, private Object resolveRequestHeader(String headerName,
MethodParameter methodParam, NativeWebRequest webRequest, Object handlerForInitBinderCall) boolean required,
throws Exception { String defaultValue,
MethodParameter methodParam,
NativeWebRequest webRequest,
Object handlerForInitBinderCall) throws Exception {
Class<?> paramType = methodParam.getParameterType(); Class<?> paramType = methodParam.getParameterType();
if (Map.class.isAssignableFrom(paramType)) { if (Map.class.isAssignableFrom(paramType)) {
@ -495,7 +509,8 @@ public class HandlerMethodInvoker {
MultiValueMap<String, String> result; MultiValueMap<String, String> result;
if (HttpHeaders.class.isAssignableFrom(mapType)) { if (HttpHeaders.class.isAssignableFrom(mapType)) {
result = new HttpHeaders(); result = new HttpHeaders();
} else { }
else {
result = new LinkedMultiValueMap<String, String>(); result = new LinkedMultiValueMap<String, String>();
} }
for (Iterator<String> iterator = webRequest.getHeaderNames(); iterator.hasNext();) { for (Iterator<String> 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") @SuppressWarnings("unchecked")
protected Object resolveRequestBody(MethodParameter methodParam, NativeWebRequest webRequest, Object handler) protected Object resolveRequestBody(MethodParameter methodParam, NativeWebRequest webRequest, Object handler)
throws Exception { throws Exception {
@ -542,12 +554,8 @@ public class HandlerMethodInvoker {
if (this.messageConverters != null) { if (this.messageConverters != null) {
for (HttpMessageConverter<?> messageConverter : this.messageConverters) { for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes()); allSupportedMediaTypes.addAll(messageConverter.getSupportedMediaTypes());
if (messageConverter.supports(paramType)) { if (messageConverter.canRead(paramType, contentType)) {
for (MediaType supportedMediaType : messageConverter.getSupportedMediaTypes()) { return messageConverter.read(paramType, inputMessage);
if (supportedMediaType.includes(contentType)) {
return messageConverter.read(paramType, inputMessage);
}
}
} }
} }
} }
@ -555,16 +563,19 @@ public class HandlerMethodInvoker {
} }
/** /**
* Return a {@link HttpInputMessage} for the given {@link NativeWebRequest}. * Return a {@link HttpInputMessage} for the given {@link NativeWebRequest}. Throws an UnsupportedOperationException by
* Throws an UnsupportedOperationException by default. * default.
*/ */
protected HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest) throws Exception { protected HttpInputMessage createHttpInputMessage(NativeWebRequest webRequest) throws Exception {
throw new UnsupportedOperationException("@RequestBody not supported"); throw new UnsupportedOperationException("@RequestBody not supported");
} }
private Object resolveCookieValue(String cookieName, boolean required, String defaultValue, private Object resolveCookieValue(String cookieName,
MethodParameter methodParam, NativeWebRequest webRequest, Object handlerForInitBinderCall) boolean required,
throws Exception { String defaultValue,
MethodParameter methodParam,
NativeWebRequest webRequest,
Object handlerForInitBinderCall) throws Exception {
Class<?> paramType = methodParam.getParameterType(); Class<?> paramType = methodParam.getParameterType();
if (cookieName.length() == 0) { if (cookieName.length() == 0) {
@ -585,18 +596,17 @@ public class HandlerMethodInvoker {
return binder.convertIfNecessary(cookieValue, paramType, methodParam); 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) protected Object resolveCookieValue(String cookieName, Class paramType, NativeWebRequest webRequest)
throws Exception { throws Exception {
throw new UnsupportedOperationException("@CookieValue not supported"); throw new UnsupportedOperationException("@CookieValue not supported");
} }
private Object resolvePathVariable(String pathVarName, MethodParameter methodParam, private Object resolvePathVariable(String pathVarName,
NativeWebRequest webRequest, Object handlerForInitBinderCall) throws Exception { MethodParameter methodParam,
NativeWebRequest webRequest,
Object handlerForInitBinderCall) throws Exception {
Class<?> paramType = methodParam.getParameterType(); Class<?> paramType = methodParam.getParameterType();
if (pathVarName.length() == 0) { if (pathVarName.length() == 0) {
@ -609,8 +619,8 @@ public class HandlerMethodInvoker {
} }
/** /**
* Resolves the given {@link PathVariable @PathVariable} annotation. * Resolves the given {@link PathVariable @PathVariable} annotation. Throws an UnsupportedOperationException by
* Throws an UnsupportedOperationException by default. * default.
*/ */
protected String resolvePathVariable(String pathVarName, Class paramType, NativeWebRequest webRequest) protected String resolvePathVariable(String pathVarName, Class paramType, NativeWebRequest webRequest)
throws Exception { throws Exception {
@ -621,9 +631,9 @@ public class HandlerMethodInvoker {
private String getRequiredParameterName(MethodParameter methodParam) { private String getRequiredParameterName(MethodParameter methodParam) {
String name = methodParam.getParameterName(); String name = methodParam.getParameterName();
if (name == null) { if (name == null) {
throw new IllegalStateException("No parameter name specified for argument of type [" + throw new IllegalStateException(
methodParam.getParameterType().getName() + "No parameter name specified for argument of type [" + methodParam.getParameterType().getName() +
"], and no parameter name information found in class file either."); "], and no parameter name information found in class file either.");
} }
return name; return name;
} }
@ -642,9 +652,11 @@ public class HandlerMethodInvoker {
return value; return value;
} }
private WebRequestDataBinder resolveModelAttribute(String attrName, MethodParameter methodParam, private WebRequestDataBinder resolveModelAttribute(String attrName,
ExtendedModelMap implicitModel, NativeWebRequest webRequest, Object handler) MethodParameter methodParam,
throws Exception { ExtendedModelMap implicitModel,
NativeWebRequest webRequest,
Object handler) throws Exception {
// Bind request parameter onto object... // Bind request parameter onto object...
String name = attrName; String name = attrName;
@ -671,8 +683,10 @@ public class HandlerMethodInvoker {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public final void updateModelAttributes(Object handler, Map<String, Object> mavModel, public final void updateModelAttributes(Object handler,
ExtendedModelMap implicitModel, NativeWebRequest webRequest) throws Exception { Map<String, Object> mavModel,
ExtendedModelMap implicitModel,
NativeWebRequest webRequest) throws Exception {
if (this.methodResolver.hasSessionAttributes() && this.sessionStatus.isComplete()) { if (this.methodResolver.hasSessionAttributes() && this.sessionStatus.isComplete()) {
for (String attrName : this.methodResolver.getActualSessionAttributeNames()) { for (String attrName : this.methodResolver.getActualSessionAttributeNames()) {
@ -731,15 +745,18 @@ public class HandlerMethodInvoker {
} }
protected void raiseMissingCookieException(String cookieName, Class paramType) throws Exception { 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 { protected void raiseSessionRequiredException(String message) throws Exception {
throw new IllegalStateException(message); throw new IllegalStateException(message);
} }
protected void doBind(WebRequestDataBinder binder, NativeWebRequest webRequest, boolean validate, boolean failOnErrors) protected void doBind(WebRequestDataBinder binder,
throws Exception { NativeWebRequest webRequest,
boolean validate,
boolean failOnErrors) throws Exception {
binder.bind(webRequest); binder.bind(webRequest);
if (validate) { if (validate) {
@ -786,8 +803,10 @@ public class HandlerMethodInvoker {
return WebArgumentResolver.UNRESOLVED; return WebArgumentResolver.UNRESOLVED;
} }
protected final void addReturnValueAsModelAttribute( protected final void addReturnValueAsModelAttribute(Method handlerMethod,
Method handlerMethod, Class handlerType, Object returnValue, ExtendedModelMap implicitModel) { Class handlerType,
Object returnValue,
ExtendedModelMap implicitModel) {
ModelAttribute attr = AnnotationUtils.findAnnotation(handlerMethod, ModelAttribute.class); ModelAttribute attr = AnnotationUtils.findAnnotation(handlerMethod, ModelAttribute.class);
String attrName = (attr != null ? attr.value() : ""); String attrName = (attr != null ? attr.value() : "");

View File

@ -36,29 +36,28 @@ public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
private final Class<T> responseType; private final Class<T> responseType;
private final List<HttpMessageConverter<T>> messageConverters; private final List<HttpMessageConverter<?>> messageConverters;
/** /**
* Creates a new instance of the {@code HttpMessageConverterExtractor} with the given response type and message * Creates a new instance of the {@code HttpMessageConverterExtractor} with the given response type and message
* converters. The given converters must support the response type. * converters. The given converters must support the response type.
*/ */
public HttpMessageConverterExtractor(Class<T> responseType, List<HttpMessageConverter<T>> messageConverters) { public HttpMessageConverterExtractor(Class<T> responseType, List<HttpMessageConverter<?>> messageConverters) {
Assert.notNull(responseType, "'responseType' must not be null"); Assert.notNull(responseType, "'responseType' must not be null");
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
this.responseType = responseType; this.responseType = responseType;
this.messageConverters = messageConverters; this.messageConverters = messageConverters;
} }
@SuppressWarnings("unchecked")
public T extractData(ClientHttpResponse response) throws IOException { public T extractData(ClientHttpResponse response) throws IOException {
MediaType contentType = response.getHeaders().getContentType(); MediaType contentType = response.getHeaders().getContentType();
if (contentType == null) { if (contentType == null) {
throw new RestClientException("Cannot extract response: no Content-Type found"); throw new RestClientException("Cannot extract response: no Content-Type found");
} }
for (HttpMessageConverter<T> messageConverter : messageConverters) { for (HttpMessageConverter messageConverter : messageConverters) {
for (MediaType supportedMediaType : messageConverter.getSupportedMediaTypes()) { if (messageConverter.canRead(responseType, contentType)) {
if (supportedMediaType.includes(contentType)) { return (T) messageConverter.read(this.responseType, response);
return messageConverter.read(this.responseType, response);
}
} }
} }
throw new RestClientException( throw new RestClientException(

View File

@ -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 * enforces RESTful principles. It handles HTTP connections, leaving application code to provide URLs (with possible
* template variables) and extract results. * template variables) and extract results.
* *
* <p>The main entry points of this template are the methods named after the six main HTTP methods: * <p>The main entry points of this template are the methods named after the six main HTTP methods: <table> <tr><th>HTTP
* <table> <tr><th>HTTP
* method</th><th>RestTemplate methods</th></tr> <tr><td>DELETE</td><td>{@link #delete}</td></tr> * method</th><th>RestTemplate methods</th></tr> <tr><td>DELETE</td><td>{@link #delete}</td></tr>
* <tr><td>GET</td><td>{@link #getForObject}</td></tr> * <tr><td>GET</td><td>{@link #getForObject}</td></tr> <tr><td>HEAD</td><td>{@link #headForHeaders}</td></tr>
* <tr><td>HEAD</td><td>{@link #headForHeaders}</td></tr> * <tr><td>OPTIONS</td><td>{@link #optionsForAllow}</td></tr> <tr><td>POST</td><td>{@link #postForLocation}</td></tr>
* <tr><td>OPTIONS</td><td>{@link #optionsForAllow}</td></tr> * <tr><td></td><td>{@link #postForObject}</td></tr> <tr><td>PUT</td><td>{@link #put}</td></tr>
* <tr><td>POST</td><td>{@link #postForLocation}</td></tr> * <tr><td>any</td><td>{@link #execute}</td></tr> </table>
* <tr><td></td><td>{@link #postForObject}</td></tr>
* <tr><td>PUT</td><td>{@link #put}</td></tr> <tr><td>any</td><td>{@link #execute}</td></tr> </table>
* *
* <p>For each of these HTTP methods, there are three corresponding Java methods in the {@code RestTemplate}. * <p>For each of these HTTP methods, there are three corresponding Java methods in the {@code RestTemplate}. Two
* Two variant take a {@code String} URI as first argument (eg. {@link #getForObject(String, Class, String[])}, * variant take a {@code String} URI as first argument (eg. {@link #getForObject(String, Class, String[])}, {@link
* {@link #getForObject(String, Class, Map)}), and are capable of substituting any * #getForObject(String, Class, Map)}), and are capable of substituting any {@linkplain UriTemplate URI templates} in
* {@linkplain UriTemplate URI templates} in that URL using either a * that URL using either a {@code String} variable arguments array, or a {@code Map<String, String>}. The string varargs
* {@code String} variable arguments array, or a {@code Map<String, String>}. The string varargs variant expands the * variant expands the given template variables in order, so that
* given template variables in order, so that
* <pre> * <pre>
* String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/bookings/{booking}", String.class,"42", * String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/bookings/{booking}", String.class,"42",
* "21"); * "21");
@ -71,22 +67,22 @@ import org.springframework.web.util.UriTemplate;
* Map&lt;String, String&gt; vars = Collections.singletonMap("hotel", "42"); * Map&lt;String, String&gt; vars = Collections.singletonMap("hotel", "42");
* String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars); * String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);
* </pre> * </pre>
* will perform a GET on {@code http://example.com/hotels/42/rooms/42}. * will perform a GET on {@code http://example.com/hotels/42/rooms/42}. Alternatively, there are {@link URI} variant
* Alternatively, there are {@link URI} variant methods ({@link #getForObject(URI, Class)}), which do not allow for * methods ({@link #getForObject(URI, Class)}), which do not allow for URI templates, but allow you to reuse a single,
* URI templates, but allow you to reuse a single, expanded URI multiple times. * expanded URI multiple times.
* *
* <p>Furthermore, the {@code String}-argument methods assume that the URL String is unencoded. This means that * <p>Furthermore, the {@code String}-argument methods assume that the URL String is unencoded. This means that
* <pre> * <pre>
* restTemplate.getForObject("http://example.com/hotel list"); * restTemplate.getForObject("http://example.com/hotel list");
* </pre> * </pre>
* will perform a GET on {@code http://example.com/hotel%20list}. As a result, any URL passed that is already encoded * 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}). * will be encoded twice (i.e. {@code http://example.com/hotel%20list} will become {@code
* If this behavior is undesirable, use the {@code URI}-argument methods, which will not perform any URL encoding. * http://example.com/hotel%2520list}). If this behavior is undesirable, use the {@code URI}-argument methods, which
* will not perform any URL encoding.
* *
* <p>Objects passed to and returned from these methods are converted to and from HTTP messages by {@link * <p>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 * 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} * your own converter and register it via the {@link #setMessageConverters messageConverters} bean property.
* bean property.
* *
* <p>This template uses a {@link org.springframework.http.client.SimpleClientHttpRequestFactory} and a {@link * <p>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, * 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<HttpHeaders> headersExtractor = new HeadersExtractor(); private final ResponseExtractor<HttpHeaders> headersExtractor = new HeadersExtractor();
private HttpMessageConverter<?>[] messageConverters = private List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
new HttpMessageConverter[]{new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(),
new FormHttpMessageConverter(), new SourceHttpMessageConverter()};
private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler(); private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler();
/** Create a new instance of the {@link RestTemplate} using default settings. */ /** Create a new instance of the {@link RestTemplate} using default settings. */
public RestTemplate() { 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 * @see org.springframework.http.client.CommonsClientHttpRequestFactory
*/ */
public RestTemplate(ClientHttpRequestFactory requestFactory) { public RestTemplate(ClientHttpRequestFactory requestFactory) {
this();
setRequestFactory(requestFactory); 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 * Set the message body converters to use. These converters are used to convert from and to HTTP requests and
* responses. * responses.
*/ */
public void setMessageConverters(HttpMessageConverter<?>[] messageConverters) { public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
this.messageConverters = messageConverters; this.messageConverters = messageConverters;
} }
/** Returns the message body converters. These converters are used to convert from and to HTTP requests and responses. */ /** Returns the message body converters. These converters are used to convert from and to HTTP requests and responses. */
public HttpMessageConverter<?>[] getMessageConverters() { public List<HttpMessageConverter<?>> getMessageConverters() {
return this.messageConverters; 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 <T> List<HttpMessageConverter<T>> getSupportedMessageConverters(Class<T> type) {
HttpMessageConverter[] converters = getMessageConverters();
List<HttpMessageConverter<T>> result = new ArrayList<HttpMessageConverter<T>>(converters.length);
for (HttpMessageConverter converter : converters) {
if (converter.supports(type)) {
result.add((HttpMessageConverter<T>) converter);
}
}
return result;
}
/** Set the error handler. */ /** Set the error handler. */
public void setErrorHandler(ResponseErrorHandler errorHandler) { public void setErrorHandler(ResponseErrorHandler errorHandler) {
Assert.notNull(errorHandler, "'errorHandler' must not be null"); Assert.notNull(errorHandler, "'errorHandler' must not be null");
@ -171,27 +152,25 @@ public class RestTemplate extends HttpAccessor implements RestOperations {
// GET // GET
public <T> T getForObject(String url, Class<T> responseType, String... urlVariables) throws RestClientException { public <T> T getForObject(String url, Class<T> responseType, String... urlVariables) throws RestClientException {
AcceptHeaderRequestCallback<T> requestCallback = new AcceptHeaderRequestCallback<T>(responseType);
checkForSupportedMessageConverter(responseType); HttpMessageConverterExtractor<T> responseExtractor =
List<HttpMessageConverter<T>> supportedMessageConverters = getSupportedMessageConverters(responseType); new HttpMessageConverterExtractor<T>(responseType, getMessageConverters());
return execute(url, HttpMethod.GET, new AcceptHeaderRequestCallback<T>(supportedMessageConverters), return execute(url, HttpMethod.GET, requestCallback, responseExtractor, urlVariables);
new HttpMessageConverterExtractor<T>(responseType, supportedMessageConverters), urlVariables);
} }
public <T> T getForObject(String url, Class<T> responseType, Map<String, String> urlVariables) public <T> T getForObject(String url, Class<T> responseType, Map<String, String> urlVariables)
throws RestClientException { throws RestClientException {
AcceptHeaderRequestCallback<T> requestCallback = new AcceptHeaderRequestCallback<T>(responseType);
checkForSupportedMessageConverter(responseType); HttpMessageConverterExtractor<T> responseExtractor =
List<HttpMessageConverter<T>> supportedMessageConverters = getSupportedMessageConverters(responseType); new HttpMessageConverterExtractor<T>(responseType, getMessageConverters());
return execute(url, HttpMethod.GET, new AcceptHeaderRequestCallback<T>(supportedMessageConverters), return execute(url, HttpMethod.GET, requestCallback, responseExtractor, urlVariables);
new HttpMessageConverterExtractor<T>(responseType, supportedMessageConverters), urlVariables);
} }
public <T> T getForObject(URI url, Class<T> responseType) throws RestClientException { public <T> T getForObject(URI url, Class<T> responseType) throws RestClientException {
checkForSupportedMessageConverter(responseType); AcceptHeaderRequestCallback<T> requestCallback = new AcceptHeaderRequestCallback<T>(responseType);
List<HttpMessageConverter<T>> supportedMessageConverters = getSupportedMessageConverters(responseType); HttpMessageConverterExtractor<T> responseExtractor =
return execute(url, HttpMethod.GET, new AcceptHeaderRequestCallback<T>(supportedMessageConverters), new HttpMessageConverterExtractor<T>(responseType, getMessageConverters());
new HttpMessageConverterExtractor<T>(responseType, supportedMessageConverters)); return execute(url, HttpMethod.GET, requestCallback, responseExtractor);
} }
// HEAD // HEAD
@ -211,86 +190,62 @@ public class RestTemplate extends HttpAccessor implements RestOperations {
// POST // POST
public URI postForLocation(String url, Object request, String... urlVariables) throws RestClientException { public URI postForLocation(String url, Object request, String... urlVariables) throws RestClientException {
if (request != null) { PostPutCallback requestCallback = new PostPutCallback(request);
checkForSupportedMessageConverter(request.getClass()); HttpHeaders headers = execute(url, HttpMethod.POST, requestCallback, this.headersExtractor, urlVariables);
}
HttpHeaders headers =
execute(url, HttpMethod.POST, new PostPutCallback(request), this.headersExtractor, urlVariables);
return headers.getLocation(); return headers.getLocation();
} }
public URI postForLocation(String url, Object request, Map<String, String> urlVariables) public URI postForLocation(String url, Object request, Map<String, String> urlVariables)
throws RestClientException { throws RestClientException {
if (request != null) { PostPutCallback requestCallback = new PostPutCallback(request);
checkForSupportedMessageConverter(request.getClass()); HttpHeaders headers = execute(url, HttpMethod.POST, requestCallback, this.headersExtractor, urlVariables);
}
HttpHeaders headers =
execute(url, HttpMethod.POST, new PostPutCallback(request), this.headersExtractor, urlVariables);
return headers.getLocation(); return headers.getLocation();
} }
public URI postForLocation(URI url, Object request) public URI postForLocation(URI url, Object request) throws RestClientException {
throws RestClientException { PostPutCallback requestCallback = new PostPutCallback(request);
if (request != null) { HttpHeaders headers = execute(url, HttpMethod.POST, requestCallback, this.headersExtractor);
checkForSupportedMessageConverter(request.getClass());
}
HttpHeaders headers = execute(url, HttpMethod.POST, new PostPutCallback(request), this.headersExtractor);
return headers.getLocation(); return headers.getLocation();
} }
public <T> T postForObject(String url, Object request, Class<T> responseType, String... uriVariables) public <T> T postForObject(String url, Object request, Class<T> responseType, String... uriVariables)
throws RestClientException { throws RestClientException {
if (request != null) { PostPutCallback<T> requestCallback = new PostPutCallback<T>(request, responseType);
checkForSupportedMessageConverter(request.getClass()); HttpMessageConverterExtractor<T> responseExtractor =
} new HttpMessageConverterExtractor<T>(responseType, getMessageConverters());
checkForSupportedMessageConverter(responseType); return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables);
List<HttpMessageConverter<T>> responseMessageConverters = getSupportedMessageConverters(responseType);
return execute(url, HttpMethod.POST, new PostPutCallback<T>(request, responseMessageConverters),
new HttpMessageConverterExtractor<T>(responseType, responseMessageConverters), uriVariables);
} }
public <T> T postForObject(String url, Object request, Class<T> responseType, Map<String, String> uriVariables) public <T> T postForObject(String url, Object request, Class<T> responseType, Map<String, String> uriVariables)
throws RestClientException { throws RestClientException {
if (request != null) { PostPutCallback<T> requestCallback = new PostPutCallback<T>(request, responseType);
checkForSupportedMessageConverter(request.getClass()); HttpMessageConverterExtractor<T> responseExtractor =
} new HttpMessageConverterExtractor<T>(responseType, getMessageConverters());
checkForSupportedMessageConverter(responseType); return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables);
List<HttpMessageConverter<T>> responseMessageConverters = getSupportedMessageConverters(responseType);
return execute(url, HttpMethod.POST, new PostPutCallback<T>(request, responseMessageConverters),
new HttpMessageConverterExtractor<T>(responseType, responseMessageConverters), uriVariables);
} }
public <T> T postForObject(URI url, Object request, Class<T> responseType) throws RestClientException { public <T> T postForObject(URI url, Object request, Class<T> responseType) throws RestClientException {
if (request != null) { PostPutCallback<T> requestCallback = new PostPutCallback<T>(request, responseType);
checkForSupportedMessageConverter(request.getClass()); HttpMessageConverterExtractor<T> responseExtractor =
} new HttpMessageConverterExtractor<T>(responseType, getMessageConverters());
checkForSupportedMessageConverter(responseType); return execute(url, HttpMethod.POST, requestCallback, responseExtractor);
List<HttpMessageConverter<T>> responseMessageConverters = getSupportedMessageConverters(responseType);
return execute(url, HttpMethod.POST, new PostPutCallback<T>(request, responseMessageConverters),
new HttpMessageConverterExtractor<T>(responseType, responseMessageConverters));
} }
// PUT // PUT
public void put(String url, Object request, String... urlVariables) throws RestClientException { public void put(String url, Object request, String... urlVariables) throws RestClientException {
if (request != null) { PostPutCallback requestCallback = new PostPutCallback(request);
checkForSupportedMessageConverter(request.getClass()); execute(url, HttpMethod.PUT, requestCallback, null, urlVariables);
}
execute(url, HttpMethod.PUT, new PostPutCallback(request), null, urlVariables);
} }
public void put(String url, Object request, Map<String, String> urlVariables) throws RestClientException { public void put(String url, Object request, Map<String, String> urlVariables) throws RestClientException {
if (request != null) { PostPutCallback requestCallback = new PostPutCallback(request);
checkForSupportedMessageConverter(request.getClass()); execute(url, HttpMethod.PUT, requestCallback, null, urlVariables);
}
execute(url, HttpMethod.PUT, new PostPutCallback(request), null, urlVariables);
} }
public void put(URI url, Object request) throws RestClientException { public void put(URI url, Object request) throws RestClientException {
if (request != null) { PostPutCallback requestCallback = new PostPutCallback(request);
checkForSupportedMessageConverter(request.getClass()); execute(url, HttpMethod.PUT, requestCallback, null);
}
execute(url, HttpMethod.PUT, new PostPutCallback(request), null);
} }
// DELETE // 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) { private void logResponseStatus(HttpMethod method, URI url, ClientHttpResponse response) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
try { try {
logger.debug(method.name() + " request for \"" + url + "\" resulted in " + response.getStatusCode() + logger.debug(
" (" + response.getStatusText() + ")"); method.name() + " request for \"" + url + "\" resulted in " + response.getStatusCode() + " (" +
response.getStatusText() + ")");
} }
catch (IOException e) { catch (IOException e) {
// ignore // ignore
@ -437,8 +376,9 @@ public class RestTemplate extends HttpAccessor implements RestOperations {
private void handleResponseError(HttpMethod method, URI url, ClientHttpResponse response) throws IOException { private void handleResponseError(HttpMethod method, URI url, ClientHttpResponse response) throws IOException {
if (logger.isWarnEnabled()) { if (logger.isWarnEnabled()) {
try { try {
logger.warn(method.name() + " request for \"" + url + "\" resulted in " + response.getStatusCode() + logger.warn(
" (" + response.getStatusText() + "); invoking error handler"); method.name() + " request for \"" + url + "\" resulted in " + response.getStatusCode() + " (" +
response.getStatusText() + "); invoking error handler");
} }
catch (IOException e) { catch (IOException e) {
// ignore // ignore
@ -450,27 +390,36 @@ public class RestTemplate extends HttpAccessor implements RestOperations {
/** Request callback implementation that prepares the request's accept headers. */ /** Request callback implementation that prepares the request's accept headers. */
private class AcceptHeaderRequestCallback<T> implements RequestCallback { private class AcceptHeaderRequestCallback<T> implements RequestCallback {
private final List<HttpMessageConverter<T>> messageConverters; private final Class<T> responseType;
private AcceptHeaderRequestCallback(List<HttpMessageConverter<T>> messageConverters) { private AcceptHeaderRequestCallback() {
this.messageConverters = messageConverters; responseType = null;
} }
private AcceptHeaderRequestCallback(Class<T> responseType) {
this.responseType = responseType;
}
@SuppressWarnings("unchecked")
public void doWithRequest(ClientHttpRequest request) throws IOException { public void doWithRequest(ClientHttpRequest request) throws IOException {
List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>(); if (responseType != null) {
for (HttpMessageConverter<?> entityConverter : messageConverters) { List<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
List<MediaType> supportedMediaTypes = entityConverter.getSupportedMediaTypes(); for (HttpMessageConverter messageConverter : getMessageConverters()) {
for (MediaType supportedMediaType : supportedMediaTypes) { if (messageConverter.canRead(responseType, null)) {
if (supportedMediaType.getCharSet() != null) { List<MediaType> supportedMediaTypes = messageConverter.getSupportedMediaTypes();
supportedMediaType = for (MediaType supportedMediaType : supportedMediaTypes) {
new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype()); if (supportedMediaType.getCharSet() != null) {
supportedMediaType =
new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype());
}
allSupportedMediaTypes.add(supportedMediaType);
}
} }
allSupportedMediaTypes.add(supportedMediaType);
} }
} if (!allSupportedMediaTypes.isEmpty()) {
if (!allSupportedMediaTypes.isEmpty()) { Collections.sort(allSupportedMediaTypes);
Collections.sort(allSupportedMediaTypes); request.getHeaders().setAccept(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. */ /** Request callback implementation that writes the given object to the request stream. */
private class PostPutCallback<T> extends AcceptHeaderRequestCallback<T> { private class PostPutCallback<T> extends AcceptHeaderRequestCallback<T> {
private final Object request; private final Object requestBody;
private PostPutCallback(Object request, List<HttpMessageConverter<T>> responseMessageConverters) { private final MediaType requestContentType;
super(responseMessageConverters);
this.request = request; private PostPutCallback(Object requestBody) {
this.requestBody = requestBody;
this.requestContentType = null;
} }
private PostPutCallback(Object request) { private PostPutCallback(Object requestBody, Class<T> responseType) {
super(Collections.<HttpMessageConverter<T>>emptyList()); super(responseType);
this.request = request; this.requestBody = requestBody;
this.requestContentType = null;
}
private PostPutCallback(Object requestBody, MediaType requestContentType, Class<T> responseType) {
super(responseType);
this.requestBody = requestBody;
this.requestContentType = requestContentType;
} }
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void doWithRequest(ClientHttpRequest httpRequest) throws IOException { public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {
super.doWithRequest(httpRequest); super.doWithRequest(httpRequest);
if (request != null) { if (requestBody != null) {
HttpMessageConverter entityConverter = getSupportedMessageConverters(this.request.getClass()).get(0); Class requestType = requestBody.getClass();
entityConverter.write(this.request, httpRequest); 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 { else {
httpRequest.getHeaders().setContentLength(0L); httpRequest.getHeaders().setContentLength(0L);

View File

@ -42,8 +42,15 @@ public class BufferedImageHttpMessageConverterTests {
} }
@Test @Test
public void supports() { public void canRead() {
assertTrue("Image not supported", converter.supports(BufferedImage.class)); 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 @Test
@ -60,11 +67,25 @@ public class BufferedImageHttpMessageConverterTests {
@Test @Test
public void write() throws IOException { public void write() throws IOException {
Resource logo = new ClassPathResource("logo.jpg", BufferedImageHttpMessageConverterTests.class); Resource logo = new ClassPathResource("logo.jpg", BufferedImageHttpMessageConverterTests.class);
MediaType contentType = new MediaType("image", "png");
converter.setContentType(contentType);
BufferedImage body = ImageIO.read(logo.getFile()); BufferedImage body = ImageIO.read(logo.getFile());
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); 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()); assertEquals("Invalid content type", contentType, outputMessage.getHeaders().getContentType());
assertTrue("Invalid size", outputMessage.getBodyAsBytes().length > 0); assertTrue("Invalid size", outputMessage.getBodyAsBytes().length > 0);
BufferedImage result = ImageIO.read(new ByteArrayInputStream(outputMessage.getBodyAsBytes())); BufferedImage result = ImageIO.read(new ByteArrayInputStream(outputMessage.getBodyAsBytes()));

View File

@ -26,9 +26,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpInputMessage;
import org.springframework.http.MockHttpOutputMessage; import org.springframework.http.MockHttpOutputMessage;
/** /** @author Arjen Poutsma */
* @author Arjen Poutsma
*/
public class ByteArrayHttpMessageConverterTests { public class ByteArrayHttpMessageConverterTests {
private ByteArrayHttpMessageConverter converter; private ByteArrayHttpMessageConverter converter;
@ -51,7 +49,7 @@ public class ByteArrayHttpMessageConverterTests {
public void write() throws IOException { public void write() throws IOException {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
byte[] body = new byte[]{0x1, 0x2}; byte[] body = new byte[]{0x1, 0x2};
converter.write(body, outputMessage); converter.write(body, null, outputMessage);
assertArrayEquals("Invalid result", body, outputMessage.getBodyAsBytes()); assertArrayEquals("Invalid result", body, outputMessage.getBodyAsBytes());
assertEquals("Invalid content-type", new MediaType("application", "octet-stream"), assertEquals("Invalid content-type", new MediaType("application", "octet-stream"),
outputMessage.getHeaders().getContentType()); outputMessage.getHeaders().getContentType());

View File

@ -30,9 +30,7 @@ import org.springframework.http.MockHttpOutputMessage;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
/** /** @author Arjen Poutsma */
* @author Arjen Poutsma
*/
public class FormHttpMessageConverterTests { public class FormHttpMessageConverterTests {
private FormHttpMessageConverter converter; private FormHttpMessageConverter converter;
@ -67,7 +65,7 @@ public class FormHttpMessageConverterTests {
body.add("name 2", "value 2+2"); body.add("name 2", "value 2+2");
body.add("name 3", null); body.add("name 3", null);
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
converter.write(body, outputMessage); converter.write(body, null, outputMessage);
Charset iso88591 = Charset.forName("ISO-8859-1"); 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", assertEquals("Invalid result", "name+1=value+1&name+2=value+2%2B1&name+2=value+2%2B2&name+3",
outputMessage.getBodyAsString(iso88591)); outputMessage.getBodyAsString(iso88591));

View File

@ -18,7 +18,6 @@ package org.springframework.http.converter;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Collections;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import org.junit.Before; import org.junit.Before;
@ -28,9 +27,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpInputMessage;
import org.springframework.http.MockHttpOutputMessage; import org.springframework.http.MockHttpOutputMessage;
/** /** @author Arjen Poutsma */
* @author Arjen Poutsma
*/
public class StringHttpMessageConverterTests { public class StringHttpMessageConverterTests {
private StringHttpMessageConverter converter; private StringHttpMessageConverter converter;
@ -55,7 +52,7 @@ public class StringHttpMessageConverterTests {
Charset iso88591 = Charset.forName("ISO-8859-1"); Charset iso88591 = Charset.forName("ISO-8859-1");
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
String body = "H\u00e9llo W\u00f6rld"; String body = "H\u00e9llo W\u00f6rld";
converter.write(body, outputMessage); converter.write(body, null, outputMessage);
assertEquals("Invalid result", body, outputMessage.getBodyAsString(iso88591)); assertEquals("Invalid result", body, outputMessage.getBodyAsString(iso88591));
assertEquals("Invalid content-type", new MediaType("text", "plain", iso88591), assertEquals("Invalid content-type", new MediaType("text", "plain", iso88591),
outputMessage.getHeaders().getContentType()); outputMessage.getHeaders().getContentType());
@ -67,13 +64,12 @@ public class StringHttpMessageConverterTests {
@Test @Test
public void writeUTF8() throws IOException { public void writeUTF8() throws IOException {
Charset utf8 = Charset.forName("UTF-8"); 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(); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
String body = "H\u00e9llo W\u00f6rld"; String body = "H\u00e9llo W\u00f6rld";
converter.write(body, outputMessage); converter.write(body, contentType, outputMessage);
assertEquals("Invalid result", body, outputMessage.getBodyAsString(utf8)); assertEquals("Invalid result", body, outputMessage.getBodyAsString(utf8));
assertEquals("Invalid content-type", new MediaType("text", "plain", utf8), assertEquals("Invalid content-type", contentType, outputMessage.getHeaders().getContentType());
outputMessage.getHeaders().getContentType());
assertEquals("Invalid content-length", body.getBytes(utf8).length, assertEquals("Invalid content-length", body.getBytes(utf8).length,
outputMessage.getHeaders().getContentLength()); outputMessage.getHeaders().getContentLength());
assertFalse("Invalid accept-charset", outputMessage.getHeaders().getAcceptCharset().isEmpty()); assertFalse("Invalid accept-charset", outputMessage.getHeaders().getAcceptCharset().isEmpty());

View File

@ -68,10 +68,10 @@ public class MappingJacksonHttpMessageConverterTests {
"{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}"; "{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8"));
inputMessage.getHeaders().setContentType(new MediaType("application", "json")); inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
HashMap<String,Object> result = converter.read(HashMap.class, inputMessage); HashMap<String, Object> result = converter.read(HashMap.class, inputMessage);
assertEquals("Foo", result.get("string")); assertEquals("Foo", result.get("string"));
assertEquals(42, result.get("number")); assertEquals(42, result.get("number"));
assertEquals(42D, (Double)result.get("fraction"), 0D); assertEquals(42D, (Double) result.get("fraction"), 0D);
List array = new ArrayList(); List array = new ArrayList();
array.add("Foo"); array.add("Foo");
array.add("Bar"); array.add("Bar");
@ -90,7 +90,7 @@ public class MappingJacksonHttpMessageConverterTests {
body.setArray(new String[]{"Foo", "Bar"}); body.setArray(new String[]{"Foo", "Bar"});
body.setBool(true); body.setBool(true);
body.setBytes(new byte[]{0x1, 0x2}); body.setBytes(new byte[]{0x1, 0x2});
converter.write(body, outputMessage); converter.write(body, null, outputMessage);
Charset utf8 = Charset.forName("UTF-8"); Charset utf8 = Charset.forName("UTF-8");
String result = outputMessage.getBodyAsString(utf8); String result = outputMessage.getBodyAsString(utf8);
assertTrue(result.contains("\"string\":\"Foo\"")); assertTrue(result.contains("\"string\":\"Foo\""));

View File

@ -68,7 +68,7 @@ public class MarshallingHttpMessageConverterTests {
marshaller.marshal(eq(body), isA(StreamResult.class)); marshaller.marshal(eq(body), isA(StreamResult.class));
replay(marshaller, unmarshaller); replay(marshaller, unmarshaller);
converter.write(body, outputMessage); converter.write(body, null, outputMessage);
assertEquals("Invalid content-type", new MediaType("application", "xml"), assertEquals("Invalid content-type", new MediaType("application", "xml"),
outputMessage.getHeaders().getContentType()); outputMessage.getHeaders().getContentType());
verify(marshaller, unmarshaller); verify(marshaller, unmarshaller);

View File

@ -94,7 +94,7 @@ public class SourceHttpMessageConverterTests {
SourceHttpMessageConverter<Source> converter = new SourceHttpMessageConverter<Source>(); SourceHttpMessageConverter<Source> converter = new SourceHttpMessageConverter<Source>();
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
converter.write(domSource, outputMessage); converter.write(domSource, null, outputMessage);
assertXMLEqual("Invalid result", "<root>Hello World</root>", assertXMLEqual("Invalid result", "<root>Hello World</root>",
outputMessage.getBodyAsString(Charset.forName("UTF-8"))); outputMessage.getBodyAsString(Charset.forName("UTF-8")));
assertEquals("Invalid content-type", new MediaType("application", "xml"), assertEquals("Invalid content-type", new MediaType("application", "xml"),

View File

@ -20,7 +20,6 @@ import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -36,9 +35,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
/** @author Arjen Poutsma */ /** @author Arjen Poutsma */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -65,18 +62,7 @@ public class RestTemplateTests {
converter = createMock(HttpMessageConverter.class); converter = createMock(HttpMessageConverter.class);
template = new RestTemplate(requestFactory); template = new RestTemplate(requestFactory);
template.setErrorHandler(errorHandler); template.setErrorHandler(errorHandler);
template.setMessageConverters(new HttpMessageConverter<?>[]{converter}); template.setMessageConverters(Collections.<HttpMessageConverter<?>>singletonList(converter));
}
@Test
public void getSupportedMessageBodyConverters() {
ByteArrayHttpMessageConverter byteArrayConverter = new ByteArrayHttpMessageConverter();
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
template.setMessageConverters(new HttpMessageConverter<?>[]{byteArrayConverter, stringConverter});
List<HttpMessageConverter<String>> result = template.getSupportedMessageConverters(String.class);
assertEquals("Invalid amount of String converters", 1, result.size());
assertEquals("Invalid String converters", stringConverter, result.get(0));
} }
@Test @Test
@ -136,9 +122,9 @@ public class RestTemplateTests {
@Test @Test
public void getForObject() throws Exception { 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"); 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); expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.GET)).andReturn(request);
HttpHeaders requestHeaders = new HttpHeaders(); HttpHeaders requestHeaders = new HttpHeaders();
expect(request.getHeaders()).andReturn(requestHeaders); expect(request.getHeaders()).andReturn(requestHeaders);
@ -147,6 +133,7 @@ public class RestTemplateTests {
HttpHeaders responseHeaders = new HttpHeaders(); HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(textPlain); responseHeaders.setContentType(textPlain);
expect(response.getHeaders()).andReturn(responseHeaders); expect(response.getHeaders()).andReturn(responseHeaders);
expect(converter.canRead(String.class, textPlain)).andReturn(true);
String expected = "Hello World"; String expected = "Hello World";
expect(converter.read(String.class, response)).andReturn(expected); expect(converter.read(String.class, response)).andReturn(expected);
response.close(); response.close();
@ -160,28 +147,11 @@ public class RestTemplateTests {
verifyMocks(); 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 @Test
public void getUnsupportedMediaType() throws Exception { 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"); 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); expect(requestFactory.createRequest(new URI("http://example.com/resource"), HttpMethod.GET)).andReturn(request);
HttpHeaders requestHeaders = new HttpHeaders(); HttpHeaders requestHeaders = new HttpHeaders();
expect(request.getHeaders()).andReturn(requestHeaders); expect(request.getHeaders()).andReturn(requestHeaders);
@ -191,6 +161,7 @@ public class RestTemplateTests {
MediaType contentType = new MediaType("bar", "baz"); MediaType contentType = new MediaType("bar", "baz");
responseHeaders.setContentType(contentType); responseHeaders.setContentType(contentType);
expect(response.getHeaders()).andReturn(responseHeaders); expect(response.getHeaders()).andReturn(responseHeaders);
expect(converter.canRead(String.class, contentType)).andReturn(false);
response.close(); response.close();
replayMocks(); replayMocks();
@ -224,10 +195,10 @@ public class RestTemplateTests {
@Test @Test
public void postForLocation() throws Exception { 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); expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(request);
String helloWorld = "Hello World"; 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(request.execute()).andReturn(response);
expect(errorHandler.hasError(response)).andReturn(false); expect(errorHandler.hasError(response)).andReturn(false);
HttpHeaders responseHeaders = new HttpHeaders(); HttpHeaders responseHeaders = new HttpHeaders();
@ -246,10 +217,10 @@ public class RestTemplateTests {
@Test @Test
public void postForLocationNoLocation() throws Exception { 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); expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(request);
String helloWorld = "Hello World"; 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(request.execute()).andReturn(response);
expect(errorHandler.hasError(response)).andReturn(false); expect(errorHandler.hasError(response)).andReturn(false);
HttpHeaders responseHeaders = new HttpHeaders(); HttpHeaders responseHeaders = new HttpHeaders();
@ -284,21 +255,22 @@ public class RestTemplateTests {
@Test @Test
public void postForObject() throws Exception { 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"); 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); expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(this.request);
HttpHeaders requestHeaders = new HttpHeaders(); HttpHeaders requestHeaders = new HttpHeaders();
expect(this.request.getHeaders()).andReturn(requestHeaders); expect(this.request.getHeaders()).andReturn(requestHeaders);
String request = "Hello World"; 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(this.request.execute()).andReturn(response);
expect(errorHandler.hasError(response)).andReturn(false); expect(errorHandler.hasError(response)).andReturn(false);
HttpHeaders responseHeaders = new HttpHeaders(); HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(textPlain); responseHeaders.setContentType(textPlain);
expect(response.getHeaders()).andReturn(responseHeaders); expect(response.getHeaders()).andReturn(responseHeaders);
Integer expected = 42; Integer expected = 42;
expect(converter.canRead(Integer.class, textPlain)).andReturn(true);
expect(converter.read(Integer.class, response)).andReturn(expected); expect(converter.read(Integer.class, response)).andReturn(expected);
response.close(); response.close();
@ -313,9 +285,9 @@ public class RestTemplateTests {
@Test @Test
public void postForObjectNull() throws Exception { public void postForObjectNull() throws Exception {
expect(converter.supports(Integer.class)).andReturn(true).times(2);
MediaType textPlain = new MediaType("text", "plain"); 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); expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(request);
HttpHeaders requestHeaders = new HttpHeaders(); HttpHeaders requestHeaders = new HttpHeaders();
expect(request.getHeaders()).andReturn(requestHeaders).times(2); expect(request.getHeaders()).andReturn(requestHeaders).times(2);
@ -324,6 +296,7 @@ public class RestTemplateTests {
HttpHeaders responseHeaders = new HttpHeaders(); HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(textPlain); responseHeaders.setContentType(textPlain);
expect(response.getHeaders()).andReturn(responseHeaders); expect(response.getHeaders()).andReturn(responseHeaders);
expect(converter.canRead(Integer.class, textPlain)).andReturn(true);
expect(converter.read(Integer.class, response)).andReturn(null); expect(converter.read(Integer.class, response)).andReturn(null);
response.close(); response.close();
@ -336,10 +309,10 @@ public class RestTemplateTests {
@Test @Test
public void put() throws Exception { 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); expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.PUT)).andReturn(request);
String helloWorld = "Hello World"; String helloWorld = "Hello World";
converter.write(helloWorld, request); converter.write(helloWorld, null, request);
expect(request.execute()).andReturn(response); expect(request.execute()).andReturn(response);
expect(errorHandler.hasError(response)).andReturn(false); expect(errorHandler.hasError(response)).andReturn(false);
response.close(); response.close();
@ -402,7 +375,7 @@ public class RestTemplateTests {
@Test @Test
public void ioException() throws Exception { 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"); MediaType mediaType = new MediaType("foo", "bar");
expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(mediaType)); expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(mediaType));
expect(requestFactory.createRequest(new URI("http://example.com/resource"), HttpMethod.GET)).andReturn(request); expect(requestFactory.createRequest(new URI("http://example.com/resource"), HttpMethod.GET)).andReturn(request);