From dbe81bef52e1bcaf802663d2bb3236c69f922512 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 17 Nov 2016 10:14:44 +0100 Subject: [PATCH] Add PATCH method operation to RestTemplate This commit adds a HTTP PATCH operation to the RestTemplate: patchForObject. As with most operations, there are three variants: varargs, Map, and URI based. Issue: SPR-14857 --- .../web/client/RestOperations.java | 54 ++++++++++++- .../web/client/RestTemplate.java | 31 ++++++++ .../client/AbstractJettyServerTestCase.java | 39 +++++++++- .../client/RestTemplateIntegrationTests.java | 14 +++- .../web/client/RestTemplateTests.java | 76 ++++++++++++++++++- 5 files changed, 208 insertions(+), 6 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java index e684dd6aa19..2f453a8e743 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java @@ -1,11 +1,11 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -306,6 +306,56 @@ public interface RestOperations { void put(URI url, Object request) throws RestClientException; + // PATCH + + /** + * Update a resource by PATCHing the given object to the URI template, + * and returns the representation found in the response. + *

URI Template variables are expanded using the given URI variables, if any. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be PATCHed, may be {@code null} + * @param responseType the type of the return value + * @param uriVariables the variables to expand the template + * @return the converted object + * @see HttpEntity + * @since 5.0 + */ + T patchForObject(String url, Object request, Class responseType, Object... uriVariables) + throws RestClientException; + + /** + * Update a resource by PATCHing the given object to the URI template, + * and returns the representation found in the response. + *

URI Template variables are expanded using the given map. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be PATCHed, may be {@code null} + * @param responseType the type of the return value + * @param uriVariables the variables to expand the template + * @return the converted object + * @see HttpEntity + */ + T patchForObject(String url, Object request, Class responseType, Map uriVariables) + throws RestClientException; + + /** + * Update a resource by PATCHing the given object to the URL, + * and returns the representation found in the response. + *

The {@code request} parameter can be a {@link HttpEntity} in order to + * add additional HTTP headers to the request. + * @param url the URL + * @param request the Object to be POSTed, may be {@code null} + * @param responseType the type of the return value + * @return the converted object + * @see HttpEntity + */ + T patchForObject(URI url, Object request, Class responseType) throws RestClientException; + + + // DELETE /** diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index 45f52de57ae..df4e868b8ff 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -458,6 +458,37 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat execute(url, HttpMethod.PUT, requestCallback, null); } + // PATCH + + @Override + public T patchForObject(String url, Object request, Class responseType, + Object... uriVariables) throws RestClientException { + + RequestCallback requestCallback = httpEntityCallback(request, responseType); + HttpMessageConverterExtractor responseExtractor = + new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger); + return execute(url, HttpMethod.PATCH, requestCallback, responseExtractor, uriVariables); + } + + @Override + public T patchForObject(String url, Object request, Class responseType, + Map uriVariables) throws RestClientException { + + RequestCallback requestCallback = httpEntityCallback(request, responseType); + HttpMessageConverterExtractor responseExtractor = + new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger); + return execute(url, HttpMethod.PATCH, requestCallback, responseExtractor, uriVariables); + } + + @Override + public T patchForObject(URI url, Object request, Class responseType) + throws RestClientException { + + RequestCallback requestCallback = httpEntityCallback(request, responseType); + HttpMessageConverterExtractor responseExtractor = + new HttpMessageConverterExtractor<>(responseType, getMessageConverters()); + return execute(url, HttpMethod.PATCH, requestCallback, responseExtractor); + } // DELETE diff --git a/spring-web/src/test/java/org/springframework/web/client/AbstractJettyServerTestCase.java b/spring-web/src/test/java/org/springframework/web/client/AbstractJettyServerTestCase.java index 0b58c740965..9da3a8f7ccb 100644 --- a/spring-web/src/test/java/org/springframework/web/client/AbstractJettyServerTestCase.java +++ b/spring-web/src/test/java/org/springframework/web/client/AbstractJettyServerTestCase.java @@ -44,7 +44,10 @@ import org.junit.BeforeClass; import org.springframework.http.MediaType; import org.springframework.util.FileCopyUtils; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; /** * @author Arjen Poutsma @@ -92,6 +95,8 @@ public class AbstractJettyServerTestCase { handler.addServlet(new ServletHolder(new MultipartServlet()), "/multipart"); handler.addServlet(new ServletHolder(new FormServlet()), "/form"); handler.addServlet(new ServletHolder(new DeleteServlet()), "/delete"); + handler.addServlet(new ServletHolder(new PatchServlet(helloWorld, bytes, textContentType)), + "/patch"); handler.addServlet( new ServletHolder(new PutServlet(helloWorld, bytes, textContentType)), "/put"); @@ -333,4 +338,36 @@ public class AbstractJettyServerTestCase { } } + @SuppressWarnings("serial") + private static class PatchServlet extends GenericServlet { + + private final String content; + + private final byte[] buf; + + private final MediaType contentType; + + public PatchServlet(String content, byte[] buf, MediaType contentType) { + this.content = content; + this.buf = buf; + this.contentType = contentType; + } + + @Override + public void service(ServletRequest req, ServletResponse res) + throws ServletException, IOException { + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + assertEquals("PATCH", request.getMethod()); + assertTrue("Invalid request content-length", request.getContentLength() > 0); + assertNotNull("No content-type", request.getContentType()); + String body = FileCopyUtils.copyToString(request.getReader()); + assertEquals("Invalid request body", content, body); + response.setStatus(HttpServletResponse.SC_CREATED); + response.setContentLength(buf.length); + response.setContentType(contentType.toString()); + FileCopyUtils.copy(buf, response.getOutputStream()); + } + } + } diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java index 8b7176f437a..140469398d2 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java @@ -45,7 +45,13 @@ import org.springframework.http.converter.json.MappingJacksonValue; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import static org.junit.Assert.*; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * @author Arjen Poutsma @@ -123,6 +129,12 @@ public class RestTemplateIntegrationTests extends AbstractJettyServerTestCase { assertEquals("Invalid content", helloWorld, s); } + @Test + public void patchForObject() throws URISyntaxException { + String s = template.patchForObject(baseUrl + "/{method}", helloWorld, String.class, "patch"); + assertEquals("Invalid content", helloWorld, s); + } + @Test public void notFound() { try { diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java index 5674ad55fe9..1b3f4fe24c1 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java @@ -44,8 +44,17 @@ import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.util.DefaultUriTemplateHandler; -import static org.junit.Assert.*; -import static org.mockito.BDDMockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.fail; +import static org.mockito.BDDMockito.any; +import static org.mockito.BDDMockito.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.mock; +import static org.mockito.BDDMockito.verify; +import static org.mockito.BDDMockito.willThrow; /** * @author Arjen Poutsma @@ -590,6 +599,69 @@ public class RestTemplateTests { verify(response).close(); } + @Test + public void patchForObject() throws Exception { + MediaType textPlain = new MediaType("text", "plain"); + given(converter.canRead(Integer.class, null)).willReturn(true); + given(converter.getSupportedMediaTypes()).willReturn(Collections.singletonList(textPlain)); + given(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.PATCH)).willReturn(this.request); + HttpHeaders requestHeaders = new HttpHeaders(); + given(this.request.getHeaders()).willReturn(requestHeaders); + String request = "Hello World"; + given(converter.canWrite(String.class, null)).willReturn(true); + converter.write(request, null, this.request); + given(this.request.execute()).willReturn(response); + given(errorHandler.hasError(response)).willReturn(false); + Integer expected = 42; + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentType(textPlain); + responseHeaders.setContentLength(10); + given(response.getStatusCode()).willReturn(HttpStatus.OK); + given(response.getHeaders()).willReturn(responseHeaders); + given(response.getBody()).willReturn(new ByteArrayInputStream(expected.toString().getBytes())); + given(converter.canRead(Integer.class, textPlain)).willReturn(true); + given(converter.read(eq(Integer.class), any(HttpInputMessage.class))).willReturn(expected); + HttpStatus status = HttpStatus.OK; + given(response.getStatusCode()).willReturn(status); + given(response.getStatusText()).willReturn(status.getReasonPhrase()); + + Integer result = template.patchForObject("http://example.com", request, Integer.class); + assertEquals("Invalid POST result", expected, result); + assertEquals("Invalid Accept header", textPlain.toString(), requestHeaders.getFirst("Accept")); + + verify(response).close(); + } + + @Test + public void patchForObjectNull() throws Exception { + MediaType textPlain = new MediaType("text", "plain"); + given(converter.canRead(Integer.class, null)).willReturn(true); + given(converter.getSupportedMediaTypes()).willReturn(Collections.singletonList(textPlain)); + given(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.PATCH)).willReturn(request); + HttpHeaders requestHeaders = new HttpHeaders(); + given(request.getHeaders()).willReturn(requestHeaders); + given(request.execute()).willReturn(response); + given(errorHandler.hasError(response)).willReturn(false); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentType(textPlain); + responseHeaders.setContentLength(10); + given(response.getStatusCode()).willReturn(HttpStatus.OK); + given(response.getHeaders()).willReturn(responseHeaders); + given(converter.canRead(Integer.class, textPlain)).willReturn(true); + given(converter.read(Integer.class, response)).willReturn(null); + HttpStatus status = HttpStatus.OK; + given(response.getStatusCode()).willReturn(status); + given(response.getStatusText()).willReturn(status.getReasonPhrase()); + + Integer result = template.patchForObject("http://example.com", null, Integer.class); + assertNull("Invalid POST result", result); + assertEquals("Invalid content length", 0, requestHeaders.getContentLength()); + + verify(response).close(); + } + + + @Test public void delete() throws Exception { given(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.DELETE)).willReturn(request);