From 594f4d56b8bdb53ebc1b07e38c70ea954d2a8d1e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 19 Dec 2016 01:51:47 +0100 Subject: [PATCH] RestTemplate provides patchForObject operations Issue: SPR-14857 --- .../web/client/RestOperations.java | 117 +++++++++++++----- .../web/client/RestTemplate.java | 35 +++++- .../client/RestTemplateIntegrationTests.java | 9 ++ .../web/client/RestTemplateTests.java | 66 ++++++++++ 4 files changed, 196 insertions(+), 31 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..36161c353cd 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, @@ -28,9 +28,9 @@ import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; /** - * Interface specifying a basic set of RESTful operations. Implemented by {@link RestTemplate}. - * Not often used directly, but a useful option to enhance testability, as it can easily - * be mocked or stubbed. + * Interface specifying a basic set of RESTful operations. + * Implemented by {@link RestTemplate}. Not often used directly, but a useful + * option to enhance testability, as it can easily be mocked or stubbed. * * @author Arjen Poutsma * @author Juergen Hoeller @@ -139,13 +139,13 @@ public interface RestOperations { // POST /** - * Create a new resource by POSTing the given object to the URI template, and returns the value of the - * {@code Location} header. This header typically indicates where the new resource is stored. + * Create a new resource by POSTing the given object to the URI template, and returns the value of + * the {@code Location} header. This header typically indicates where the new resource is stored. *

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 POSTed, may be {@code null} + * @param request the Object to be POSTed (may be {@code null}) * @param uriVariables the variables to expand the template * @return the value for the {@code Location} header * @see HttpEntity @@ -153,13 +153,13 @@ public interface RestOperations { URI postForLocation(String url, Object request, Object... uriVariables) throws RestClientException; /** - * Create a new resource by POSTing the given object to the URI template, and returns the value of the - * {@code Location} header. This header typically indicates where the new resource is stored. + * Create a new resource by POSTing the given object to the URI template, and returns the value of + * the {@code Location} header. This header typically indicates where the new resource is stored. *

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 POSTed, may be {@code null} + * @param request the Object to be POSTed (may be {@code null}) * @param uriVariables the variables to expand the template * @return the value for the {@code Location} header * @see HttpEntity @@ -172,7 +172,7 @@ public interface RestOperations { *

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 request the Object to be POSTed (may be {@code null}) * @return the value for the {@code Location} header * @see HttpEntity */ @@ -185,7 +185,7 @@ public interface RestOperations { *

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 request the Object to be POSTed (may be {@code null}) * @param responseType the type of the return value * @param uriVariables the variables to expand the template * @return the converted object @@ -201,7 +201,7 @@ public interface RestOperations { *

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 request the Object to be POSTed (may be {@code null}) * @param responseType the type of the return value * @param uriVariables the variables to expand the template * @return the converted object @@ -216,7 +216,7 @@ public interface RestOperations { *

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 request the Object to be POSTed (may be {@code null}) * @param responseType the type of the return value * @return the converted object * @see HttpEntity @@ -230,11 +230,11 @@ public interface RestOperations { *

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 request the Object to be POSTed (may be {@code null}) * @param uriVariables the variables to expand the template * @return the converted object - * @see HttpEntity * @since 3.0.2 + * @see HttpEntity */ ResponseEntity postForEntity(String url, Object request, Class responseType, Object... uriVariables) throws RestClientException; @@ -246,11 +246,11 @@ public interface RestOperations { *

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 request the Object to be POSTed (may be {@code null}) * @param uriVariables the variables to expand the template * @return the converted object - * @see HttpEntity * @since 3.0.2 + * @see HttpEntity */ ResponseEntity postForEntity(String url, Object request, Class responseType, Map uriVariables) throws RestClientException; @@ -261,10 +261,10 @@ public interface RestOperations { *

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 request the Object to be POSTed (may be {@code null}) * @return the converted object - * @see HttpEntity * @since 3.0.2 + * @see HttpEntity */ ResponseEntity postForEntity(URI url, Object request, Class responseType) throws RestClientException; @@ -277,7 +277,7 @@ public interface RestOperations { *

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 PUT, may be {@code null} + * @param request the Object to be PUT (may be {@code null}) * @param uriVariables the variables to expand the template * @see HttpEntity */ @@ -289,7 +289,7 @@ public interface RestOperations { *

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 PUT, may be {@code null} + * @param request the Object to be PUT (may be {@code null}) * @param uriVariables the variables to expand the template * @see HttpEntity */ @@ -300,12 +300,64 @@ public interface RestOperations { *

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 PUT, may be {@code null} + * @param request the Object to be PUT (may be {@code null}) * @see HttpEntity */ 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 + * @since 4.3.5 + * @see HttpEntity + */ + 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 + * @since 4.3.5 + * @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 PATCHed (may be {@code null}) + * @param responseType the type of the return value + * @return the converted object + * @since 4.3.5 + * @see HttpEntity + */ + T patchForObject(URI url, Object request, Class responseType) throws RestClientException; + + + // DELETE /** @@ -368,7 +420,8 @@ public interface RestOperations { *

URI Template variables are expanded using the given URI variables, if any. * @param url the URL * @param method the HTTP method (GET, POST, etc) - * @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null} + * @param requestEntity the entity (headers and/or body) to write to the request + * may be {@code null}) * @param responseType the type of the return value * @param uriVariables the variables to expand in the template * @return the response as entity @@ -383,7 +436,8 @@ public interface RestOperations { *

URI Template variables are expanded using the given URI variables, if any. * @param url the URL * @param method the HTTP method (GET, POST, etc) - * @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null} + * @param requestEntity the entity (headers and/or body) to write to the request + * (may be {@code null}) * @param responseType the type of the return value * @param uriVariables the variables to expand in the template * @return the response as entity @@ -397,7 +451,8 @@ public interface RestOperations { * returns the response as {@link ResponseEntity}. * @param url the URL * @param method the HTTP method (GET, POST, etc) - * @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null} + * @param requestEntity the entity (headers and/or body) to write to the request + * (may be {@code null}) * @param responseType the type of the return value * @return the response as entity * @since 3.0.2 @@ -416,7 +471,7 @@ public interface RestOperations { * @param url the URL * @param method the HTTP method (GET, POST, etc) * @param requestEntity the entity (headers and/or body) to write to the - * request, may be {@code null} + * request (may be {@code null}) * @param responseType the type of the return value * @param uriVariables the variables to expand in the template * @return the response as entity @@ -435,7 +490,8 @@ public interface RestOperations { * * @param url the URL * @param method the HTTP method (GET, POST, etc) - * @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null} + * @param requestEntity the entity (headers and/or body) to write to the request + * (may be {@code null}) * @param responseType the type of the return value * @param uriVariables the variables to expand in the template * @return the response as entity @@ -454,7 +510,8 @@ public interface RestOperations { * * @param url the URL * @param method the HTTP method (GET, POST, etc) - * @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null} + * @param requestEntity the entity (headers and/or body) to write to the request + * (may be {@code null}) * @param responseType the type of the return value * @return the response as entity * @since 3.2 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 24312094fe6..0b8734443e8 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 @@ -445,6 +445,39 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat } + // 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 @Override @@ -647,8 +680,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat * @param method the HTTP method to execute (GET, POST, etc.) * @param response the resulting {@link ClientHttpResponse} * @throws IOException if propagated from {@link ResponseErrorHandler} - * @see #setErrorHandler * @since 4.1.6 + * @see #setErrorHandler */ protected void handleResponse(URI url, HttpMethod method, ClientHttpResponse response) throws IOException { ResponseErrorHandler errorHandler = getErrorHandler(); 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 9bcea2cc247..c83d91da914 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 @@ -154,6 +154,15 @@ public class RestTemplateIntegrationTests extends AbstractMockWebServerTestCase assertEquals("Invalid content", helloWorld, s); } + @Test + public void patchForObject() throws URISyntaxException { + // JDK client does not support the PATCH method + Assume.assumeThat(this.clientHttpRequestFactory, + Matchers.not(Matchers.instanceOf(SimpleClientHttpRequestFactory.class))); + 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..d7ab7acd7bd 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 @@ -67,6 +67,7 @@ public class RestTemplateTests { @SuppressWarnings("rawtypes") private HttpMessageConverter converter; + @Before public void setUp() { requestFactory = mock(ClientHttpRequestFactory.class); @@ -79,6 +80,7 @@ public class RestTemplateTests { template.setErrorHandler(errorHandler); } + @Test public void varArgsTemplateVariables() throws Exception { given(requestFactory.createRequest(new URI("http://example.com/hotels/42/bookings/21"), HttpMethod.GET)) @@ -590,6 +592,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); @@ -730,4 +795,5 @@ public class RestTemplateTests { verify(response).close(); } + }