From 3e59c244f90c59aa98bed773cbae95416b591d36 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 14 May 2015 14:42:04 -0400 Subject: [PATCH] Add UriTemplateHandler This change introduces a strategy for expanding a URI template into a URI and makes it a property of the RestTemplate and AsyncRestTemplate so that they can be pre-configured with such a strategy. The DefaultUriTemplateHandler relies on UriComponentsBuilder internally and provides functionality equivalent to using the UriTemplate. A DefaultUriTemplateHandler can also be configured to parse the path of a URI template into path segments in order to allow expanding URI variables according to path segment encoding rules. Issue: SPR-12750 --- .../web/client/AsyncRestTemplate.java | 25 +++++- .../web/client/RestTemplate.java | 27 ++++++- .../web/util/DefaultUriTemplateHandler.java | 80 +++++++++++++++++++ .../web/util/UriTemplateHandler.java | 45 +++++++++++ .../web/client/RestTemplateTests.java | 30 +++++++ .../util/DefaultUriTemplateHandlerTests.java | 70 ++++++++++++++++ 6 files changed, 270 insertions(+), 7 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java create mode 100644 spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java create mode 100644 spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java diff --git a/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java index 6ed6132b69b..3a714fe6395 100644 --- a/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -49,7 +49,8 @@ import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.ListenableFutureAdapter; import org.springframework.util.concurrent.ListenableFutureCallback; import org.springframework.util.concurrent.SuccessCallback; -import org.springframework.web.util.UriTemplate; +import org.springframework.web.util.DefaultUriTemplateHandler; +import org.springframework.web.util.UriTemplateHandler; /** * Spring's central class for asynchronous client-side HTTP access. @@ -146,6 +147,22 @@ public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOpe return this.syncTemplate.getErrorHandler(); } + /** + * Set a custom {@link UriTemplateHandler} for expanding URI templates. + *

By default, RestTemplate uses {@link DefaultUriTemplateHandler}. + * @param handler the URI template handler to use + */ + public void setUriTemplateHandler(UriTemplateHandler handler) { + this.syncTemplate.setUriTemplateHandler(handler); + } + + /** + * Return the configured URI template handler. + */ + public UriTemplateHandler getUriTemplateHandler() { + return this.syncTemplate.getUriTemplateHandler(); + } + @Override public RestOperations getRestOperations() { return this.syncTemplate; @@ -493,7 +510,7 @@ public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOpe public ListenableFuture execute(String url, HttpMethod method, AsyncRequestCallback requestCallback, ResponseExtractor responseExtractor, Object... urlVariables) throws RestClientException { - URI expanded = new UriTemplate(url).expand(urlVariables); + URI expanded = getUriTemplateHandler().expand(url, urlVariables); return doExecute(expanded, method, requestCallback, responseExtractor); } @@ -501,7 +518,7 @@ public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOpe public ListenableFuture execute(String url, HttpMethod method, AsyncRequestCallback requestCallback, ResponseExtractor responseExtractor, Map urlVariables) throws RestClientException { - URI expanded = new UriTemplate(url).expand(urlVariables); + URI expanded = getUriTemplateHandler().expand(url, urlVariables); return doExecute(expanded, method, requestCallback, responseExtractor); } 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 e5c254e83b9..504df69d014 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 @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; + import javax.xml.transform.Source; import org.springframework.core.ParameterizedTypeReference; @@ -51,7 +52,8 @@ import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConve import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.web.util.UriTemplate; +import org.springframework.web.util.DefaultUriTemplateHandler; +import org.springframework.web.util.UriTemplateHandler; /** * Spring's central class for synchronous client-side HTTP access. @@ -135,6 +137,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler(); + private UriTemplateHandler uriTemplateHandler = new DefaultUriTemplateHandler(); + private final ResponseExtractor headersExtractor = new HeadersExtractor(); @@ -226,6 +230,23 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat return this.errorHandler; } + /** + * Set a custom {@link UriTemplateHandler} for expanding URI templates. + *

By default, RestTemplate uses {@link DefaultUriTemplateHandler}. + * @param handler the URI template handler to use + */ + public void setUriTemplateHandler(UriTemplateHandler handler) { + Assert.notNull(handler, "'uriTemplateHandler' is required."); + this.uriTemplateHandler = handler; + } + + /** + * Return the configured URI template handler. + */ + public UriTemplateHandler getUriTemplateHandler() { + return this.uriTemplateHandler; + } + // GET @@ -526,7 +547,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat public T execute(String url, HttpMethod method, RequestCallback requestCallback, ResponseExtractor responseExtractor, Object... urlVariables) throws RestClientException { - URI expanded = new UriTemplate(url).expand(urlVariables); + URI expanded = getUriTemplateHandler().expand(url, urlVariables); return doExecute(expanded, method, requestCallback, responseExtractor); } @@ -534,7 +555,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat public T execute(String url, HttpMethod method, RequestCallback requestCallback, ResponseExtractor responseExtractor, Map urlVariables) throws RestClientException { - URI expanded = new UriTemplate(url).expand(urlVariables); + URI expanded = getUriTemplateHandler().expand(url, urlVariables); return doExecute(expanded, method, requestCallback, responseExtractor); } diff --git a/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java new file mode 100644 index 00000000000..9b7b838247b --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/DefaultUriTemplateHandler.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2015 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.util; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +/** + * Default implementation of {@link UriTemplateHandler} that relies on + * {@link UriComponentsBuilder} internally. + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +public class DefaultUriTemplateHandler implements UriTemplateHandler { + + private boolean parsePath; + + + /** + * Whether to parse the path of a URI template string into path segments. + *

If set to {@code true} the path of parsed URI templates is decomposed + * into path segments so that URI variables expanded into the path are + * treated according to path segment encoding rules. In effect that means the + * "/" character is percent encoded. + *

By default this is set to {@code false} in which case the path is kept + * as a full path and expanded URI variables will preserve "/" characters. + * @param parsePath whether to parse the path into path segments + */ + public void setParsePath(boolean parsePath) { + this.parsePath = parsePath; + } + + /** + * Whether the handler is configured to parse the path into path segments. + */ + public boolean shouldParsePath() { + return this.parsePath; + } + + + @Override + public URI expand(String uriTemplate, Map uriVariables) { + UriComponentsBuilder builder = initBuilder(uriTemplate); + return builder.build().expand(uriVariables).encode().toUri(); + } + + @Override + public URI expand(String uriTemplate, Object... uriVariableValues) { + UriComponentsBuilder builder = initBuilder(uriTemplate); + return builder.build().expand(uriVariableValues).encode().toUri(); + } + + protected UriComponentsBuilder initBuilder(String uriTemplate) { + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(uriTemplate); + if (shouldParsePath()) { + List pathSegments = builder.build().getPathSegments(); + builder.replacePath(null); + for (String pathSegment : pathSegments) { + builder.pathSegment(pathSegment); + } + } + return builder; + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java b/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java new file mode 100644 index 00000000000..fd43aea671c --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/util/UriTemplateHandler.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2015 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.util; + +import java.net.URI; +import java.util.Map; + +/** + * A strategy for expanding a URI template with URI variables into a {@link URI}. + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +public interface UriTemplateHandler { + + /** + * Expand the give URI template with a map of URI variables. + * @param uriTemplate the URI template string + * @param uriVariables the URI variables + * @return the resulting URI + */ + URI expand(String uriTemplate, Map uriVariables); + + /** + * Expand the give URI template with an array of URI variable values. + * @param uriTemplate the URI template string + * @param uriVariableValues the URI variable values + * @return the resulting URI + */ + URI expand(String uriTemplate, Object... uriVariableValues); + +} 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 86e55af404f..1ecfc48e5fb 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 @@ -42,6 +42,7 @@ import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpResponse; 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.*; @@ -259,6 +260,35 @@ public class RestTemplateTests { verify(response).close(); } + @Test + public void getForObjectWithCustomUriTemplateHandler() throws Exception { + + DefaultUriTemplateHandler uriTemplateHandler = new DefaultUriTemplateHandler(); + uriTemplateHandler.setParsePath(true); + template.setUriTemplateHandler(uriTemplateHandler); + + URI expectedUri = new URI("http://example.com/hotels/1/pic/pics%2Flogo.png/size/150x150"); + given(requestFactory.createRequest(expectedUri, HttpMethod.GET)).willReturn(request); + + given(request.getHeaders()).willReturn(new HttpHeaders()); + given(request.execute()).willReturn(response); + given(errorHandler.hasError(response)).willReturn(false); + + given(response.getStatusCode()).willReturn(HttpStatus.OK); + given(response.getHeaders()).willReturn(new HttpHeaders()); + given(response.getBody()).willReturn(null); + + Map uriVariables = new HashMap(2); + uriVariables.put("hotel", "1"); + uriVariables.put("publicpath", "pics/logo.png"); + uriVariables.put("scale", "150x150"); + + String url = "http://example.com/hotels/{hotel}/pic/{publicpath}/size/{scale}"; + template.getForObject(url, String.class, uriVariables); + + verify(response).close(); + } + @Test public void headForHeaders() throws Exception { diff --git a/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java b/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java new file mode 100644 index 00000000000..18eae557fcb --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/util/DefaultUriTemplateHandlerTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2015 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.util; + +import static org.junit.Assert.assertEquals; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests for {@link DefaultUriTemplateHandler}. + * @author Rossen Stoyanchev + */ +public class DefaultUriTemplateHandlerTests { + + private DefaultUriTemplateHandler handler; + + + @Before + public void setUp() throws Exception { + this.handler = new DefaultUriTemplateHandler(); + } + + + @Test + public void expandWithFullPath() throws Exception { + Map vars = new HashMap(2); + vars.put("hotel", "1"); + vars.put("publicpath", "pics/logo.png"); + String template = "http://example.com/hotels/{hotel}/pic/{publicpath}"; + + URI actual = this.handler.expand(template, vars); + + URI expected = new URI("http://example.com/hotels/1/pic/pics/logo.png"); + assertEquals("Invalid expanded template", expected, actual); + } + + @Test + public void expandWithFullPathParsedIntoPathSegments() throws Exception { + Map vars = new HashMap(2); + vars.put("hotel", "1"); + vars.put("publicpath", "pics/logo.png"); + vars.put("scale", "150x150"); + String template = "http://example.com/hotels/{hotel}/pic/{publicpath}/size/{scale}"; + + this.handler.setParsePath(true); + URI actual = this.handler.expand(template, vars); + + URI expected = new URI("http://example.com/hotels/1/pic/pics%2Flogo.png/size/150x150"); + assertEquals("Invalid expanded template", expected, actual); + } + +}