diff --git a/build.gradle b/build.gradle index cd33239f50..d6118cd820 100644 --- a/build.gradle +++ b/build.gradle @@ -360,7 +360,7 @@ project('spring-web') { compile("commons-fileupload:commons-fileupload:1.2", optional) runtime("commons-io:commons-io:1.3", optional) compile("commons-httpclient:commons-httpclient:3.1", optional) - compile("org.apache.httpcomponents:httpclient:4.1.1", optional) + compile("org.apache.httpcomponents:httpclient:4.2", optional) compile("org.codehaus.jackson:jackson-mapper-asl:1.4.2", optional) compile("com.fasterxml.jackson.core:jackson-databind:2.0.1", optional) compile("taglibs:standard:1.1.2", optional) diff --git a/spring-web/src/main/java/org/springframework/http/HttpMethod.java b/spring-web/src/main/java/org/springframework/http/HttpMethod.java index 361784b4ab..f006c703cc 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpMethod.java +++ b/spring-web/src/main/java/org/springframework/http/HttpMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,6 @@ package org.springframework.http; */ public enum HttpMethod { - GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE + GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE } diff --git a/spring-web/src/main/java/org/springframework/http/client/CommonsClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/CommonsClientHttpRequestFactory.java index e70c8f36e8..afd7bdb3cc 100644 --- a/spring-web/src/main/java/org/springframework/http/client/CommonsClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/CommonsClientHttpRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -141,6 +141,9 @@ public class CommonsClientHttpRequestFactory implements ClientHttpRequestFactory return new PutMethod(uri); case TRACE: return new TraceMethod(uri); + case PATCH: + throw new IllegalArgumentException( + "HTTP method PATCH not available before Apache HttpComponents HttpClient 4.2"); default: throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod); } diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java index f5dee91ec9..7e8f6c5d0b 100644 --- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,9 @@ package org.springframework.http.client; import java.io.IOException; +import java.lang.reflect.Constructor; import java.net.URI; -import org.springframework.beans.factory.DisposableBean; -import org.springframework.http.HttpMethod; -import org.springframework.util.Assert; - import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; @@ -40,6 +37,10 @@ import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.params.CoreConnectionPNames; import org.apache.http.protocol.HttpContext; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * {@link org.springframework.http.client.ClientHttpRequestFactory} implementation that uses @@ -155,11 +156,30 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest return new HttpPut(uri); case TRACE: return new HttpTrace(uri); + case PATCH: + return createHttpPatch(uri); default: throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod); } } + private HttpUriRequest createHttpPatch(URI uri) { + String className = "org.apache.http.client.methods.HttpPatch"; + ClassLoader classloader = this.getClass().getClassLoader(); + if (!ClassUtils.isPresent(className, classloader)) { + throw new IllegalArgumentException( + "HTTP method PATCH not available before Apache HttpComponents HttpClient 4.2"); + } + try { + Class clazz = classloader.loadClass(className); + Constructor constructor = clazz.getConstructor(URI.class); + return (HttpUriRequest) constructor.newInstance(uri); + } + catch (Throwable ex) { + throw new IllegalStateException("Unable to instantiate " + className, ex); + } + } + /** * Template method that allows for manipulating the {@link HttpUriRequest} before it is * returned as part of a {@link HttpComponentsClientHttpRequest}. diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java index a0b8474f71..22d17b50f0 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -152,7 +152,7 @@ public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory else { connection.setInstanceFollowRedirects(false); } - if ("PUT".equals(httpMethod) || "POST".equals(httpMethod)) { + if ("PUT".equals(httpMethod) || "POST".equals(httpMethod) || "PATCH".equals(httpMethod)) { connection.setDoOutput(true); } else { diff --git a/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java b/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java index 68fe27551c..731c64ac30 100644 --- a/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java +++ b/spring-web/src/main/java/org/springframework/web/HttpMediaTypeNotSupportedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import java.util.List; import org.springframework.http.MediaType; /** - * Exception thrown when a client POSTs or PUTs content + * Exception thrown when a client POSTs, PUTs, or PATCHes content of a type * not supported by request handler. * * @author Arjen Poutsma diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index 00c9a52f92..f7ba780479 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -271,7 +271,7 @@ public @interface RequestMapping { /** * The HTTP request methods to map to, narrowing the primary mapping: - * GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE. + * GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE. *

Supported at the type level as well as at the method level! * When used at the type level, all method-level mappings inherit * this HTTP method restriction (i.e. the type-level restriction diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java index 3695cb3425..814687ca65 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ package org.springframework.web.bind.annotation; * {@link RequestMapping} annotation. * *

Note that, by default, {@link org.springframework.web.servlet.DispatcherServlet} - * supports GET, HEAD, POST, PUT and DELETE only. DispatcherServlet will + * supports GET, HEAD, POST, PUT, PATCH and DELETE only. DispatcherServlet will * process TRACE and OPTIONS with the default HttpServlet behavior unless * explicitly told to dispatch those request types as well: Check out * the "dispatchOptionsRequest" and "dispatchTraceRequest" properties, @@ -36,6 +36,6 @@ package org.springframework.web.bind.annotation; */ public enum RequestMethod { - GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE } diff --git a/spring-web/src/main/java/org/springframework/web/filter/HttpPutFormContentFilter.java b/spring-web/src/main/java/org/springframework/web/filter/HttpPutFormContentFilter.java index 540ed24741..74982a361c 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/HttpPutFormContentFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/HttpPutFormContentFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,24 +44,24 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; /** - * {@link javax.servlet.Filter} that makes form encoded data available through - * the {@code ServletRequest.getParameter*()} family of methods during HTTP PUT - * requests. - * - *

The Servlet spec requires form data to be available for HTTP POST but - * not for HTTP PUT requests. This filter intercepts HTTP PUT requests where - * content type is {@code 'application/x-www-form-urlencoded'}, reads form - * encoded content from the body of the request, and wraps the ServletRequest + * {@link javax.servlet.Filter} that makes form encoded data available through + * the {@code ServletRequest.getParameter*()} family of methods during HTTP PUT + * or PATCH requests. + * + *

The Servlet spec requires form data to be available for HTTP POST but + * not for HTTP PUT or PATCH requests. This filter intercepts HTTP PUT and PATCH + * requests where content type is {@code 'application/x-www-form-urlencoded'}, + * reads form encoded content from the body of the request, and wraps the ServletRequest * in order to make the form data available as request parameters just like * it is for HTTP POST requests. - * + * * @author Rossen Stoyanchev * @since 3.1 */ public class HttpPutFormContentFilter extends OncePerRequestFilter { private final FormHttpMessageConverter formConverter = new XmlAwareFormHttpMessageConverter(); - + /** * The default character set to use for reading form data. */ @@ -73,7 +73,7 @@ public class HttpPutFormContentFilter extends OncePerRequestFilter { protected void doFilterInternal(final HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - if ("PUT".equals(request.getMethod()) && isFormContentType(request)) { + if (("PUT".equals(request.getMethod()) || "PATCH".equals(request.getMethod())) && isFormContentType(request)) { HttpInputMessage inputMessage = new ServletServerHttpRequest(request) { @Override public InputStream getBody() throws IOException { @@ -102,7 +102,7 @@ public class HttpPutFormContentFilter extends OncePerRequestFilter { } private static class HttpPutFormContentRequestWrapper extends HttpServletRequestWrapper { - + private MultiValueMap formParameters; public HttpPutFormContentRequestWrapper(HttpServletRequest request, MultiValueMap parameters) { diff --git a/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java b/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java index 8fe6752e20..58ed4ebbe0 100644 --- a/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java +++ b/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,6 +68,7 @@ public abstract class AbstractHttpRequestFactoryTestCase { jettyContext.addServlet(new ServletHolder(new MethodServlet("OPTIONS")), "/methods/options"); jettyContext.addServlet(new ServletHolder(new PostServlet()), "/methods/post"); jettyContext.addServlet(new ServletHolder(new MethodServlet("PUT")), "/methods/put"); + jettyContext.addServlet(new ServletHolder(new MethodServlet("PATCH")), "/methods/patch"); jettyServer.start(); } @@ -160,7 +161,7 @@ public abstract class AbstractHttpRequestFactoryTestCase { assertHttpMethod("delete", HttpMethod.DELETE); } - private void assertHttpMethod(String path, HttpMethod method) throws Exception { + protected void assertHttpMethod(String path, HttpMethod method) throws Exception { ClientHttpResponse response = null; try { ClientHttpRequest request = factory.createRequest(new URI(baseUrl + "/methods/" + path), method); diff --git a/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleHttpRequestFactoryTests.java index 6ffd7fc8d9..7408552ff5 100644 --- a/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleHttpRequestFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleHttpRequestFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,26 @@ package org.springframework.http.client; +import java.net.ProtocolException; + +import org.junit.Test; +import org.springframework.http.HttpMethod; + public class BufferedSimpleHttpRequestFactoryTests extends AbstractHttpRequestFactoryTestCase { @Override protected ClientHttpRequestFactory createRequestFactory() { return new SimpleClientHttpRequestFactory(); } + + @Test + public void httpMethods() throws Exception { + try { + assertHttpMethod("patch", HttpMethod.PATCH); + } + catch (ProtocolException ex) { + // Currently HttpURLConnection does not support HTTP PATCH + } + } + } diff --git a/spring-web/src/test/java/org/springframework/http/client/CommonsHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/CommonsHttpRequestFactoryTests.java index 0f7f5de40d..4fd3017ca2 100644 --- a/spring-web/src/test/java/org/springframework/http/client/CommonsHttpRequestFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/CommonsHttpRequestFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,10 @@ package org.springframework.http.client; -import org.springframework.http.client.AbstractHttpRequestFactoryTestCase; -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.http.client.CommonsClientHttpRequestFactory; +import java.net.URI; + +import org.junit.Test; +import org.springframework.http.HttpMethod; public class CommonsHttpRequestFactoryTests extends AbstractHttpRequestFactoryTestCase { @@ -26,4 +27,10 @@ public class CommonsHttpRequestFactoryTests extends AbstractHttpRequestFactoryTe protected ClientHttpRequestFactory createRequestFactory() { return new CommonsClientHttpRequestFactory(); } + + @Test(expected=IllegalArgumentException.class) + public void httpPatch() throws Exception { + factory.createRequest(new URI(baseUrl + "/methods/PATCH"), HttpMethod.PATCH); + } + } diff --git a/spring-web/src/test/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactoryTests.java index 399db694a4..917e5de114 100644 --- a/spring-web/src/test/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,19 @@ package org.springframework.http.client; +import org.junit.Test; +import org.springframework.http.HttpMethod; + public class HttpComponentsClientHttpRequestFactoryTests extends AbstractHttpRequestFactoryTestCase { @Override protected ClientHttpRequestFactory createRequestFactory() { return new HttpComponentsClientHttpRequestFactory(); } + + @Test + public void httpMethods() throws Exception { + assertHttpMethod("patch", HttpMethod.PATCH); + } + } diff --git a/spring-web/src/test/java/org/springframework/web/filter/HttpPutFormContentFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/HttpPutFormContentFilterTests.java index 17fdb03517..d73cc484e8 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/HttpPutFormContentFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/HttpPutFormContentFilterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,25 +29,26 @@ import java.util.Map; import org.junit.Before; import org.junit.Test; +import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; /** * Test fixture for {@link HttpPutFormContentFilter}. - * + * * @author Rossen Stoyanchev */ public class HttpPutFormContentFilterTests { private HttpPutFormContentFilter filter; - + private MockHttpServletRequest request; - + private MockHttpServletResponse response; - + private MockFilterChain filterChain; - + @Before public void setup() { filter = new HttpPutFormContentFilter(); @@ -59,14 +60,18 @@ public class HttpPutFormContentFilterTests { } @Test - public void wrapPutOnly() throws Exception { + public void wrapPutAndPatchOnly() throws Exception { request.setContent("".getBytes("ISO-8859-1")); - String[] methods = new String[] {"GET", "POST", "DELETE", "HEAD", "OPTIONS", "TRACE"}; - for (String method : methods) { - request.setMethod(method); + for (HttpMethod method : HttpMethod.values()) { + request.setMethod(method.name()); filterChain = new MockFilterChain(); filter.doFilter(request, response, filterChain); - assertSame("Should not wrap for HTTP method " + method, request, filterChain.getRequest()); + if (method.equals(HttpMethod.PUT) || method.equals(HttpMethod.PATCH)) { + assertNotSame("Should wrap HTTP method " + method, request, filterChain.getRequest()); + } + else { + assertSame("Should not wrap for HTTP method " + method, request, filterChain.getRequest()); + } } } @@ -81,31 +86,31 @@ public class HttpPutFormContentFilterTests { assertSame("Should not wrap for content type " + contentType, request, filterChain.getRequest()); } } - + @Test public void getParameter() throws Exception { request.setContent("name=value".getBytes("ISO-8859-1")); filter.doFilter(request, response, filterChain); - + assertEquals("value", filterChain.getRequest().getParameter("name")); } - + @Test public void getParameterFromQueryString() throws Exception { request.addParameter("name", "value1"); request.setContent("name=value2".getBytes("ISO-8859-1")); filter.doFilter(request, response, filterChain); - + assertNotSame("Request not wrapped", request, filterChain.getRequest()); - assertEquals("Query string parameters should be listed ahead of form parameters", + assertEquals("Query string parameters should be listed ahead of form parameters", "value1", filterChain.getRequest().getParameter("name")); } - + @Test public void getParameterNullValue() throws Exception { request.setContent("name=value".getBytes("ISO-8859-1")); filter.doFilter(request, response, filterChain); - + assertNotSame("Request not wrapped", request, filterChain.getRequest()); assertNull(filterChain.getRequest().getParameter("noSuchParam")); } @@ -115,27 +120,27 @@ public class HttpPutFormContentFilterTests { request.addParameter("name1", "value1"); request.addParameter("name2", "value2"); request.setContent("name1=value1&name3=value3&name4=value4".getBytes("ISO-8859-1")); - + filter.doFilter(request, response, filterChain); List names = Collections.list(filterChain.getRequest().getParameterNames()); assertNotSame("Request not wrapped", request, filterChain.getRequest()); assertEquals(Arrays.asList("name1", "name2", "name3", "name4"), names); } - + @Test public void getParameterValues() throws Exception { request.addParameter("name", "value1"); request.addParameter("name", "value2"); request.setContent("name=value3&name=value4".getBytes("ISO-8859-1")); - + filter.doFilter(request, response, filterChain); String[] values = filterChain.getRequest().getParameterValues("name"); assertNotSame("Request not wrapped", request, filterChain.getRequest()); assertArrayEquals(new String[]{"value1", "value2", "value3", "value4"}, values); } - + @Test public void getParameterValuesFromQueryString() throws Exception { request.addParameter("name", "value1"); @@ -148,33 +153,33 @@ public class HttpPutFormContentFilterTests { assertNotSame("Request not wrapped", request, filterChain.getRequest()); assertArrayEquals(new String[]{"value1", "value2"}, values); } - + @Test public void getParameterValuesFromFormContent() throws Exception { request.addParameter("name", "value1"); request.addParameter("name", "value2"); request.setContent("anotherName=anotherValue".getBytes("ISO-8859-1")); - + filter.doFilter(request, response, filterChain); String[] values = filterChain.getRequest().getParameterValues("anotherName"); assertNotSame("Request not wrapped", request, filterChain.getRequest()); assertArrayEquals(new String[]{"anotherValue"}, values); } - + @Test public void getParameterValuesInvalidName() throws Exception { request.addParameter("name", "value1"); request.addParameter("name", "value2"); request.setContent("anotherName=anotherValue".getBytes("ISO-8859-1")); - + filter.doFilter(request, response, filterChain); String[] values = filterChain.getRequest().getParameterValues("noSuchParameter"); assertNotSame("Request not wrapped", request, filterChain.getRequest()); assertNull(values); } - + @Test public void getParameterMap() throws Exception { request.addParameter("name", "value1"); @@ -189,5 +194,5 @@ public class HttpPutFormContentFilterTests { assertArrayEquals(new String[] {"value1", "value2", "value3"}, parameters.get("name")); assertArrayEquals(new String[] {"value4"}, parameters.get("name4")); } - + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index b8d7855ed2..c50b827608 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -41,6 +41,7 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.context.ConfigurableWebApplicationContext; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestAttributes; @@ -767,6 +768,25 @@ public abstract class FrameworkServlet extends HttpServletBean { } + /** + * Override the parent class implementation in order to intercept PATCH + * requests. + * + * @see #doPatch(HttpServletRequest, HttpServletResponse) + */ + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + String method = request.getMethod(); + if (method.equalsIgnoreCase(RequestMethod.PATCH.name())) { + doPatch(request, response); + } + else { + super.service(request, response); + } + } + /** * Delegate GET requests to processRequest/doService. *

Will also be invoked by HttpServlet's default implementation of doHead, @@ -803,6 +823,16 @@ public abstract class FrameworkServlet extends HttpServletBean { processRequest(request, response); } + /** + * Delegate PATCH requests to {@link #processRequest}. + * @see #doService + */ + protected final void doPatch(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + processRequest(request, response); + } + /** * Delegate DELETE requests to {@link #processRequest}. * @see #doService diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index ef81648a07..c8e6a1083c 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -144,20 +144,20 @@ import org.springframework.web.servlet.view.InternalResourceViewResolver; /** * The origin of this test class is {@link ServletAnnotationControllerHandlerMethodTests}. - * + * * Tests in this class run against the {@link HandlerMethod} infrastructure: *

- * + * *

Rather than against the existing infrastructure: *

+ * * * @author Rossen Stoyanchev */ @@ -249,7 +249,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl context.registerBeanDefinition("ppc", ppc); } }, DefaultExpressionValueParamController.class); - + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/myApp/myPath.do"); request.setContextPath("/myApp"); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -656,7 +656,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl final MockServletContext servletContext = new MockServletContext(); final MockServletConfig servletConfig = new MockServletConfig(servletContext); - WebApplicationContext webAppContext = + WebApplicationContext webAppContext = initServlet(new ApplicationContextInitializer() { public void initialize(GenericWebApplicationContext wac) { wac.setServletContext(servletContext); @@ -705,7 +705,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl getServlet().service(request, response); assertEquals("mySurpriseView", response.getContentAsString()); - MyParameterDispatchingController deserialized = + MyParameterDispatchingController deserialized = (MyParameterDispatchingController) SerializationTestUtils.serializeAndDeserialize( webAppContext.getBean(MyParameterDispatchingController.class.getSimpleName())); assertNotNull(deserialized.request); @@ -813,6 +813,21 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertEquals(requestBody, response.getContentAsString()); } + @Test + public void httpPatch() throws ServletException, IOException { + initServletWithControllers(RequestResponseBodyController.class); + + MockHttpServletRequest request = new MockHttpServletRequest("PATCH", "/something"); + String requestBody = "Hello world!"; + request.setContent(requestBody.getBytes("UTF-8")); + request.addHeader("Content-Type", "text/plain; charset=utf-8"); + request.addHeader("Accept", "text/*, */*"); + MockHttpServletResponse response = new MockHttpServletResponse(); + getServlet().service(request, response); + assertEquals(200, response.getStatus()); + assertEquals(requestBody, response.getContentAsString()); + } + @Test public void responseBodyNoAcceptableMediaType() throws ServletException, IOException { initServlet(new ApplicationContextInitializer() { @@ -1248,7 +1263,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertEquals("Content-Type=[text/html],Custom-Header=[value21,value22]", response.getContentAsString()); } - + @Test public void requestMappingInterface() throws Exception { initServletWithControllers(IMyControllerImpl.class); @@ -1298,7 +1313,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl assertEquals("handle", response.getContentAsString()); } - + @Test public void trailingSlash() throws Exception { initServletWithControllers(TrailingSlashController.class); @@ -1444,7 +1459,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); - + assertEquals(200, response.getStatus()); assertEquals("home", response.getForwardedUrl()); @@ -1462,11 +1477,11 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl request.addHeader("Accept", "application/json"); response = new MockHttpServletResponse(); getServlet().service(request, response); - + assertEquals(200, response.getStatus()); assertEquals("application/json", response.getHeader("Content-Type")); assertEquals("homeJson", response.getContentAsString()); - } + } @Test public void redirectAttribute() throws Exception { @@ -1479,7 +1494,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl // POST -> bind error getServlet().service(request, response); - + assertEquals(200, response.getStatus()); assertEquals("messages/new", response.getForwardedUrl()); assertTrue(RequestContextUtils.getOutputFlashMap(request).isEmpty()); @@ -1516,20 +1531,20 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl context.registerBeanDefinition("controller", beanDef); } }); - + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); request.addParameter("param", "1"); MockHttpServletResponse response = new MockHttpServletResponse(); getServlet().service(request, response); - + assertEquals("count:3", response.getContentAsString()); response = new MockHttpServletResponse(); getServlet().service(request, response); - + assertEquals("count:3", response.getContentAsString()); } - + /* * Controllers */ @@ -2335,6 +2350,12 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl public String handle(@RequestBody String body) throws IOException { return body; } + + @RequestMapping(value = "/something", method = RequestMethod.PATCH) + @ResponseBody + public String handlePartialUpdate(@RequestBody String content) throws IOException { + return content; + } } @Controller @@ -2366,7 +2387,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl @XmlRootElement public static class A { - + } @XmlRootElement @@ -2729,7 +2750,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl public void root(Writer writer) throws IOException { writer.write("root"); } - + @RequestMapping(value = "/{templatePath}/", method = RequestMethod.GET) public void templatePath(Writer writer) throws IOException { writer.write("templatePath"); @@ -2839,7 +2860,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl @Controller static class HeadersConditionController { - + @RequestMapping(value = "/", method = RequestMethod.GET) public String home() { return "home"; @@ -2887,7 +2908,7 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl public void initBinder(WebDataBinder dataBinder) { this.count++; } - + @ModelAttribute public void populate(Model model) { this.count++; @@ -2899,16 +2920,16 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl writer.write("count:" + this.count); } } - + // Test cases deleted from the original SevletAnnotationControllerTests: - -// @Ignore("Controller interface => no method-level @RequestMapping annotation") + +// @Ignore("Controller interface => no method-level @RequestMapping annotation") // public void standardHandleMethod() throws Exception { - + // @Ignore("ControllerClassNameHandlerMapping") // public void emptyRequestMapping() throws Exception { -// @Ignore("Controller interface => no method-level @RequestMapping annotation") +// @Ignore("Controller interface => no method-level @RequestMapping annotation") // public void proxiedStandardHandleMethod() throws Exception { // @Ignore("ServletException no longer thrown for unmatched parameter constraints") @@ -2919,10 +2940,10 @@ public class ServletAnnotationControllerHandlerMethodTests extends AbstractServl // @Ignore("Method name dispatching") // public void methodNameDispatchingControllerWithSuffix() throws Exception { - + // @Ignore("ControllerClassNameHandlerMapping") // public void controllerClassNamePlusMethodNameDispatchingController() throws Exception { - + // @Ignore("Method name dispatching") // public void postMethodNameDispatchingController() throws Exception { diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt index 9093f76eb7..41190821ca 100644 --- a/src/dist/changelog.txt +++ b/src/dist/changelog.txt @@ -11,6 +11,7 @@ Changes in version 3.2 M2 * add JacksonObjectMapperFactoryBean for configuring a Jackson ObjectMapper in XML * infer return type of parameterized factory methods (SPR-9493) * add ContentNegotiationManager/ContentNegotiationStrategy to resolve requested media types +* add support for the HTTP PATCH method Changes in version 3.2 M1 (2012-05-28) -------------------------------------- diff --git a/src/reference/docbook/mvc.xml b/src/reference/docbook/mvc.xml index 9b5133a8ee..3b58280cb1 100644 --- a/src/reference/docbook/mvc.xml +++ b/src/reference/docbook/mvc.xml @@ -1975,7 +1975,7 @@ public class EditPetForm { ServletRequest.getParameter*() family of methods to support form field access only for HTTP POST, not for HTTP PUT. - To support HTTP PUT requests, the spring-web + To support HTTP PUT and PATCH requests, the spring-web module provides the filter HttpPutFormContentFilter, which can be configured in web.xml: @@ -1995,7 +1995,7 @@ public class EditPetForm { <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> </servlet> - The above filter intercepts HTTP PUT requests with content type + The above filter intercepts HTTP PUT and PATCH requests with content type application/x-www-form-urlencoded, reads the form data from the body of the request, and wraps the ServletRequest in order to make the form data