From ca535bb1d06ab6527a333e8d87698beea7adb2d6 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Sun, 22 Feb 2009 14:51:00 +0000 Subject: [PATCH] SPR-5260: RestTemplate --- .../web/client/HttpClientErrorException.java | 48 ++ .../web/client/HttpClientException.java | 47 ++ .../web/client/HttpIOException.java | 48 ++ .../web/client/HttpServerErrorException.java | 49 ++ .../web/client/HttpStatusCodeException.java | 69 +++ .../web/client/core/HttpErrorHandler.java | 53 +++ .../web/client/core/HttpRequestCallback.java | 45 ++ .../client/core/HttpResponseExtractor.java | 45 ++ .../web/client/core/RestOperations.java | 206 ++++++++ .../web/client/core/RestTemplate.java | 449 ++++++++++++++++++ .../client/core/SimpleHttpErrorHandler.java | 74 +++ .../web/client/core/package.html | 8 + .../core/support/RestGatewaySupport.java | 81 ++++ .../web/client/core/support/package.html | 8 + .../springframework/web/client/package.html | 7 + .../web/client/support/HttpAccessor.java | 79 +++ .../web/client/support/package.html | 8 + .../core/RestTemplateIntegrationTests.java | 171 +++++++ .../web/client/core/RestTemplateTest.java | 349 ++++++++++++++ 19 files changed, 1844 insertions(+) create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/HttpClientErrorException.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/HttpClientException.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/HttpIOException.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/HttpServerErrorException.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/core/HttpErrorHandler.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/core/HttpRequestCallback.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/core/HttpResponseExtractor.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/core/RestOperations.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/core/RestTemplate.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/core/SimpleHttpErrorHandler.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/core/package.html create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/core/support/RestGatewaySupport.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/core/support/package.html create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/package.html create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/support/HttpAccessor.java create mode 100644 org.springframework.web/src/main/java/org/springframework/web/client/support/package.html create mode 100644 org.springframework.web/src/test/java/org/springframework/web/client/core/RestTemplateIntegrationTests.java create mode 100644 org.springframework.web/src/test/java/org/springframework/web/client/core/RestTemplateTest.java diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/HttpClientErrorException.java b/org.springframework.web/src/main/java/org/springframework/web/client/HttpClientErrorException.java new file mode 100644 index 0000000000..6549bf820f --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/HttpClientErrorException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2009 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.client; + +import org.springframework.web.http.HttpStatus; + +/** + * Exception thrown when a HTTP 4xx is received. + * + * @author Arjen Poutsma + * @see org.springframework.web.client.core.SimpleHttpErrorHandler + * @since 3.0 + */ +public class HttpClientErrorException extends HttpStatusCodeException { + + /** + * Constructs a new instance of {@code HttpClientErrorException} based on a {@link HttpStatus}. + * + * @param statusCode the status code + */ + public HttpClientErrorException(HttpStatus statusCode) { + super(statusCode); + } + + /** + * Constructs a new instance of {@code HttpClientErrorException} based on a {@link HttpStatus} and status text. + * + * @param statusCode the status code + * @param statusText the status text + */ + public HttpClientErrorException(HttpStatus statusCode, String statusText) { + super(statusCode, statusText); + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/HttpClientException.java b/org.springframework.web/src/main/java/org/springframework/web/client/HttpClientException.java new file mode 100644 index 0000000000..89f4e0d3ba --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/HttpClientException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2009 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.client; + +import org.springframework.core.NestedRuntimeException; + +/** + * Base class for exceptions thrown by the framework whenever it encounters client-side HTTP errors. + * + * @author Arjen Poutsma + * @since 3.0 + */ +public class HttpClientException extends NestedRuntimeException { + + /** + * Constructs a new instance of {@code HttpClientException} with the given message. + * + * @param msg the message + */ + public HttpClientException(String msg) { + super(msg); + } + + /** + * Constructs a new instance of {@code HttpClientException} with the given message and exception. + * + * @param msg the message + * @param ex the exception + */ + public HttpClientException(String msg, Throwable ex) { + super(msg, ex); + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/HttpIOException.java b/org.springframework.web/src/main/java/org/springframework/web/client/HttpIOException.java new file mode 100644 index 0000000000..865078cbf1 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/HttpIOException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2009 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.client; + +import java.io.IOException; + +/** + * Exception thrown when a I/O error occurs. + * + * @author Arjen Poutsma + * @since 3.0 + */ +public class HttpIOException extends HttpClientException { + + /** + * Constructs a new {@code HttpIOException} with the given message. + * + * @param msg the message + */ + public HttpIOException(String msg) { + super(msg); + } + + /** + * Constructs a new {@code HttpIOException} with the given message and {@link IOException}. + * + * @param msg the message + * @param ex the {@code IOException} + */ + public HttpIOException(String msg, IOException ex) { + super(msg, ex); + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/HttpServerErrorException.java b/org.springframework.web/src/main/java/org/springframework/web/client/HttpServerErrorException.java new file mode 100644 index 0000000000..62e5f6f8fc --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/HttpServerErrorException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2009 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.client; + +import org.springframework.web.http.HttpStatus; + +/** + * Exception thrown when a HTTP 5xx is received. + * + * @author Arjen Poutsma + * @see org.springframework.web.client.core.SimpleHttpErrorHandler + * @since 3.0 + */ +public class HttpServerErrorException extends HttpStatusCodeException { + + /** + * Constructs a new instance of {@code HttpServerErrorException} based on a {@link HttpStatus}. + * + * @param statusCode the status code + */ + public HttpServerErrorException(HttpStatus statusCode) { + super(statusCode); + } + + /** + * Constructs a new instance of {@code HttpServerErrorException} based on a {@link HttpStatus} and status text. + * + * @param statusCode the status code + * @param statusText the status text + */ + public HttpServerErrorException(HttpStatus statusCode, String statusText) { + super(statusCode, statusText); + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java b/org.springframework.web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java new file mode 100644 index 0000000000..249ff795e8 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/HttpStatusCodeException.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2009 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.client; + +import org.springframework.web.http.HttpStatus; + +/** + * Abstract base class for exceptions based on a {@link HttpStatus}. + * + * @author Arjen Poutsma + * @since 3.0 + */ +public abstract class HttpStatusCodeException extends HttpClientException { + + private final HttpStatus statusCode; + + private final String statusText; + + /** + * Constructs a new instance of {@code HttpStatusCodeException} based on a {@link HttpStatus}. + * + * @param statusCode the status code + */ + protected HttpStatusCodeException(HttpStatus statusCode) { + super(statusCode.toString()); + this.statusCode = statusCode; + this.statusText = statusCode.name(); + } + + /** + * Constructs a new instance of {@code HttpStatusCodeException} based on a {@link HttpStatus} and status text. + * + * @param statusCode the status code + * @param statusText the status text + */ + protected HttpStatusCodeException(HttpStatus statusCode, String statusText) { + super(statusCode.value() + " " + statusText); + this.statusCode = statusCode; + this.statusText = statusText; + } + + /** + * Returns the HTTP status code. + */ + public HttpStatus getStatusCode() { + return statusCode; + } + + /** + * Returns the HTTP status text. + */ + public String getStatusText() { + return statusText; + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/core/HttpErrorHandler.java b/org.springframework.web/src/main/java/org/springframework/web/client/core/HttpErrorHandler.java new file mode 100644 index 0000000000..d7550e9aad --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/core/HttpErrorHandler.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2009 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.client.core; + +import java.io.IOException; + +import org.springframework.web.http.client.ClientHttpResponse; + +/** + * Strategy interface used by the {@link RestTemplate} to determine whether a particular response has an error or not. + * + * @author Arjen Poutsma + * @since 3.0 + */ +public interface HttpErrorHandler { + + /** + * Indicates whether the given response has any errors. + * + * Implementations will typically inspect the {@link ClientHttpResponse#getStatusCode() HttpStatus} of the response. + * + * @param response the response to inspect + * @return true if the response has an error; false otherwise + * @throws IOException in case of I/O errors + */ + boolean hasError(ClientHttpResponse response) throws IOException; + + /** + * Handles the error in the given response. + * + * This method is only called when {@link #hasError(ClientHttpResponse)} has returned true. + * + * @param response the response with the error + * @throws IOException in case of I/O errors + * @throws org.springframework.web.client.HttpClientException + * typically thrown by implementations of this interface + */ + void handleError(ClientHttpResponse response) throws IOException; +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/core/HttpRequestCallback.java b/org.springframework.web/src/main/java/org/springframework/web/client/core/HttpRequestCallback.java new file mode 100644 index 0000000000..51c141e1d1 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/core/HttpRequestCallback.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2009 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.client.core; + +import java.io.IOException; + +import org.springframework.web.http.client.ClientHttpRequest; + +/** + * Callback interface for code that operates on a {@link ClientHttpRequest}. Allows to manipulate the request + * headers, and write to the request body. + * + *

Used internally by the {@link RestTemplate}, but also useful for application code. + * + * @author Arjen Poutsma + * @see RestTemplate#execute + * @since 3.0 + */ +public interface HttpRequestCallback { + + /** + * Gets called by {@link RestTemplate#execute} with an opened {@code ClientHttpRequest}. Does not need to care about + * closing the request, handling I/O errors, or about handling errors: this will all be handled by the {@code + * RestTemplate}. + * + * @param request the active HTTP request + * @throws IOException in case of I/O errors + */ + void doWithRequest(ClientHttpRequest request) throws IOException; + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/core/HttpResponseExtractor.java b/org.springframework.web/src/main/java/org/springframework/web/client/core/HttpResponseExtractor.java new file mode 100644 index 0000000000..29b2b57bd1 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/core/HttpResponseExtractor.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2009 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.client.core; + +import java.io.IOException; + +import org.springframework.web.http.client.ClientHttpResponse; + +/** + * Generic callback interface used by {@link RestTemplate}'s retrieval methods. Implementations of this interface + * perform the actual work of extracting data from a {@link ClientHttpResponse}, but don't need to worry about exception + * handling or closing resources. + * + *

Used internally by the {@link RestTemplate}, but also useful for application code. + * + * @author Arjen Poutsma + * @see RestTemplate#execute + * @since 3.0 + */ +public interface HttpResponseExtractor { + + /** + * Extracts data from the given {@code ClientHttpResponse} and returns it. + * + * @param response the HTTP response + * @return the extracted data + * @throws IOException in case of I/O errors + */ + T extractData(ClientHttpResponse response) throws IOException; + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/core/RestOperations.java b/org.springframework.web/src/main/java/org/springframework/web/client/core/RestOperations.java new file mode 100644 index 0000000000..52bba14cc2 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/core/RestOperations.java @@ -0,0 +1,206 @@ +/* + * Copyright 2002-2009 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.client.core; + +import java.net.URI; +import java.util.EnumSet; +import java.util.Map; + +import org.springframework.web.http.HttpHeaders; +import org.springframework.web.http.HttpMethod; + +/** + * 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 + * @see RestTemplate + * @since 3.0 + */ +public interface RestOperations { + + // GET + + /** + * Retrieves a representation by doing a GET on the specified URL. URI Template variables are expanded using the + * given URI variables, if any. + * + * @param uri the URI to GET + * @param responseType the type of the return value + * @param uriVariables the variables to expand the template + * @return the converted object + */ + T getForObject(String uri, Class responseType, String... uriVariables); + + /** + * Retrieves a representation by doing a GET on the URI template. URI Template variables are expanded using the + * given map. + * + * @param uri the URI to GET + * @param responseType the type of the return value + * @param uriVariables the map containing variables for the URI template + * @return the converted object + */ + T getForObject(String uri, Class responseType, Map uriVariables); + + // HEAD + + /** + * Retrieves all headers of the resource specified by the URI template. URI Template variables are expanded using + * the given URI variables, if any. + * + * @param uri the URI + * @param uriVariables the variables to expand the template + * @return all HTTP headers of that resource + */ + HttpHeaders headForHeaders(String uri, String... uriVariables); + + /** + * Retrieves all headers of the resource specified by the URI template. URI Template variables are expanded using + * the given map. + * + * @param uri the URI + * @param uriVariables the map containing variables for the URI template + * @return all HTTP headers of that resource + */ + HttpHeaders headForHeaders(String uri, Map uriVariables); + + // POST + + /** + * Creates a new resource by POSTing the given object to the URI template. The value of the Location, + * indicating where the new resource is stored, is returned. URI Template variables are expanded using the given URI + * variables, if any. + * + * @param uri the URI + * @param request the Object to be POSTED + * @return the value for the Location header + */ + URI postForLocation(String uri, Object request, String... uriVariables); + + /** + * Creates a new resource by POSTing the given object to URI template. The value of the Location, + * indicating where the new resource is stored, is returned. URI Template variables are expanded using the given + * map. + * + * @param uri the URI + * @param request the Object to be POSTed + * @param uriVariables the variables to expand the template + * @return the value for the Location header + */ + URI postForLocation(String uri, Object request, Map uriVariables); + + // PUT + + /** + * Creates or updates a resource by PUTting the given object to the URI. URI Template variables are expanded using + * the given URI variables, if any. + * + * @param uri the URI + * @param request the Object to be POSTed + * @param uriVariables the variables to expand the template + */ + void put(String uri, Object request, String... uriVariables); + + /** + * Creates a new resource by PUTting the given object to URI template. URI Template variables are expanded using the + * given map. + * + * @param uri the URI + * @param request the Object to be POSTed + * @param uriVariables the variables to expand the template + */ + void put(String uri, Object request, Map uriVariables); + + // DELETE + + /** + * Deletes the resources at the specified URI. URI Template variables are expanded using the given URI variables, if + * any. + * + * @param uri the URI + * @param uriVariables the variables to expand in the template + */ + void delete(String uri, String... uriVariables); + + /** + * Deletes the resources at the specified URI. URI Template variables are expanded using the given map. + * + * @param uri the URI + * @param uriVariables the variables to expand the template + */ + void delete(String uri, Map uriVariables); + + //OPTIONS + + /** + * Returns value of the Allow header for the given URI. URI Template variables are expanded using the given URI + * variables, if any. + * + * @param uri the URI + * @param uriVariables the variables to expand in the template + * @return the value of the allow header + */ + EnumSet optionsForAllow(String uri, String... uriVariables); + + /** + * Returns value of the Allow header for the given URI. URI Template variables are expanded using the given map. + * + * @param uri the URI + * @param uriVariables the variables to expand in the template + * @return the value of the allow header + */ + EnumSet optionsForAllow(String uri, Map uriVariables); + + /** + * Executes the HTTP methods to the given URI, preparing the request with the {@link HttpRequestCallback}, and + * reading the response with a {@link HttpResponseExtractor}. URI Template variables are expanded using the + * given URI variables, if any. + * + * @param uri the URI + * @param method the HTTP method (GET, POST, etc) + * @param requestCallback object that prepares the request + * @param responseExtractor object that extracts the return value from the response + * @param uriVariables the variables to expand in the template + * @return an arbitrary object, as returned by the {@link HttpResponseExtractor} + */ + T execute(String uri, + HttpMethod method, + HttpRequestCallback requestCallback, + HttpResponseExtractor responseExtractor, + String... uriVariables); + + /** + * Executes the HTTP methods to the given URI, preparing the request with the {@link HttpRequestCallback}, and + * reading the response with a {@link HttpResponseExtractor}. URI Template variables are expanded using the + * given URI variables map. + * + * @param uri the URI + * @param method the HTTP method (GET, POST, etc) + * @param requestCallback object that prepares the request + * @param responseExtractor object that extracts the return value from the response + * @param uriVariables the variables to expand in the template + * @return an arbitrary object, as returned by the {@link HttpResponseExtractor} + */ + T execute(String uri, + HttpMethod method, + HttpRequestCallback requestCallback, + HttpResponseExtractor responseExtractor, + Map uriVariables); + + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/core/RestTemplate.java b/org.springframework.web/src/main/java/org/springframework/web/client/core/RestTemplate.java new file mode 100644 index 0000000000..09a56956e3 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/core/RestTemplate.java @@ -0,0 +1,449 @@ +/* + * Copyright 2002-2009 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.client.core; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.MediaType; +import org.springframework.web.client.HttpClientException; +import org.springframework.web.client.HttpIOException; +import org.springframework.web.client.support.HttpAccessor; +import org.springframework.web.converter.ByteArrayHttpMessageConverter; +import org.springframework.web.converter.HttpMessageConverter; +import org.springframework.web.converter.StringHttpMessageConverter; +import org.springframework.web.http.HttpHeaders; +import org.springframework.web.http.HttpMethod; +import org.springframework.web.http.client.ClientHttpRequest; +import org.springframework.web.http.client.ClientHttpRequestFactory; +import org.springframework.web.http.client.ClientHttpResponse; +import org.springframework.web.util.UriTemplate; + +/** + * The central class for client-side HTTP access.. It simplifies communication with HTTP servers, and + * enforces RESTful principles. It handles HTTP connections, leaving application code to provide URLs (with possible + * template variables) and extract results. + * + *

The main entry points of this template are the methods named after the five main HTTP methods: + * + * + * + * + * + * + * + * + * + *
HTTP methodRestTemplate methods
DELETE{@link #delete}
GET{@link #getForObject}
HEAD{@link #headForHeaders}
OPTIONS{@link #optionsForAllow}
POST{@link #postForLocation}
PUT{@link #put}
any{@link #execute}
+ * + *

Each of these methods takes {@linkplain UriTemplate uri template} arguments in two forms: as a {@code String} + * variable arguments array, or as a {@code Map}. The string varargs variant expands the given template + * variables in order, so that + *

+ * String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/bookings/{booking}", String.class,"42", "21");
+ * 
+ * will perform a GET on {@code http://example.com/hotels/42/bookings/21}. The map variant is explands the template + * based on variable name, and is therefore more useful when using many variables, or when a single variable is used + * multiple times. For example: + *
+ * Map<String, String> vars = Collections.singletonMap("hotel", 42);
+ * String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);
+ * 
+ * will perform a GET on {@code http://example.com/hotels/42/rooms/42}. + * + *

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 + * your own converter and register it via the {@link #setMessageConverters(HttpMessageConverter[]) messageConverters} + * bean property. + * + *

This template uses a {@link org.springframework.web.http.client.SimpleClientHttpRequestFactory} and a {@link + * SimpleHttpErrorHandler} as default strategies for for creating HTTP connections or handling HTTP errors, respectively. + * These defaults can be overridden through the {@link #setRequestFactory(ClientHttpRequestFactory) requestFactory} and + * {@link #setErrorHandler(HttpErrorHandler) errorHandler} bean properties. + * + * @author Arjen Poutsma + * @see HttpMessageConverter + * @see HttpRequestCallback + * @see HttpResponseExtractor + * @see HttpErrorHandler + * @since 3.0 + */ +public class RestTemplate extends HttpAccessor implements RestOperations { + + private final HttpResponseExtractor headersExtractor = new HeadersExtractor(); + + private HttpMessageConverter[] messageConverters; + + private HttpErrorHandler errorHandler; + + /** + * Creates a new instance of the {@link RestTemplate} using default settings. + * + * @see #initDefaultStrategies() + */ + public RestTemplate() { + initDefaultStrategies(); + } + + /** + * Creates a new instance of the {@link RestTemplate} based on the given {@link ClientHttpRequestFactory}. + * + * @param requestFactory HTTP request factory to use + * @see org.springframework.web.http.client.SimpleClientHttpRequestFactory + * @see org.springframework.web.http.client.commons.CommonsClientHttpRequestFactory + */ + public RestTemplate(ClientHttpRequestFactory requestFactory) { + initDefaultStrategies(); + setRequestFactory(requestFactory); + } + + /** + * Initializes the default stragegies for this template. + * + *

Default implementation sets up the {@link SimpleHttpErrorHandler} and the {@link ByteArrayHttpMessageConverter} and + * {@link StringHttpMessageConverter}. + */ + protected void initDefaultStrategies() { + errorHandler = new SimpleHttpErrorHandler(); + messageConverters = + new HttpMessageConverter[]{new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter()}; + } + + /** + * Returns the array of message body converters. These converters are used to covert from and to HTTP requests and + * responses. + */ + public HttpMessageConverter[] getMessageConverters() { + return messageConverters; + } + + /** + * Returns the list of message body converters that support a particular type. + * + * @param type the type to return converters for + * @return converts that support the given type + */ + @SuppressWarnings("unchecked") + protected List> getSupportedMessageConverters(Class type) { + HttpMessageConverter[] converters = getMessageConverters(); + List> result = new ArrayList>(converters.length); + for (HttpMessageConverter converter : converters) { + if (converter.supports(type)) { + result.add((HttpMessageConverter) converter); + } + } + return result; + } + + /** + * Sets the array of message body converters to use. These converters are used to covert from and to HTTP requests and + * responses. + * + * Note that setting this property overrides the {@linkplain #initDefaultStrategies() default strategies}. + */ + public void setMessageConverters(HttpMessageConverter[] messageConverters) { + Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); + this.messageConverters = messageConverters; + } + + /** + * Returns the error handler. By default, this is the {@link SimpleHttpErrorHandler}. + */ + public HttpErrorHandler getErrorHandler() { + return errorHandler; + } + + /** + * Sets the error handler. + */ + public void setErrorHandler(HttpErrorHandler errorHandler) { + Assert.notNull(errorHandler, "'errorHandler' must not be null"); + this.errorHandler = errorHandler; + } + + // GET + + public T getForObject(String url, Class responseType, String... urlVariables) { + checkForSupportedEntityConverter(responseType); + return execute(url, HttpMethod.GET, new GetCallback(responseType), + new HttpMessageConverterExtractor(responseType), urlVariables); + } + + public T getForObject(String url, Class responseType, Map urlVariables) { + checkForSupportedEntityConverter(responseType); + return execute(url, HttpMethod.GET, new GetCallback(responseType), + new HttpMessageConverterExtractor(responseType), urlVariables); + } + + // POST + + public URI postForLocation(String url, Object request, String... urlVariables) { + checkForSupportedEntityConverter(request.getClass()); + HttpHeaders headers = + execute(url, HttpMethod.POST, new PostPutCallback(request), headersExtractor, urlVariables); + return headers.getLocation(); + } + + public URI postForLocation(String url, Object request, Map urlVariables) { + checkForSupportedEntityConverter(request.getClass()); + HttpHeaders headers = + execute(url, HttpMethod.POST, new PostPutCallback(request), headersExtractor, urlVariables); + return headers.getLocation(); + } + + // PUT + + public void put(String url, Object request, String... urlVariables) { + checkForSupportedEntityConverter(request.getClass()); + execute(url, HttpMethod.PUT, new PostPutCallback(request), null, urlVariables); + } + + public void put(String url, Object request, Map urlVariables) { + checkForSupportedEntityConverter(request.getClass()); + execute(url, HttpMethod.PUT, new PostPutCallback(request), null, urlVariables); + } + + // HEAD + + public HttpHeaders headForHeaders(String url, String... urlVariables) { + return execute(url, HttpMethod.HEAD, null, headersExtractor, urlVariables); + } + + public HttpHeaders headForHeaders(String url, Map urlVariables) { + return execute(url, HttpMethod.HEAD, null, headersExtractor, urlVariables); + } + + // DELETE + + public void delete(String url, String... urlVariables) { + execute(url, HttpMethod.DELETE, null, null, urlVariables); + } + + public void delete(String url, Map urlVariables) { + execute(url, HttpMethod.DELETE, null, null, urlVariables); + } + + // OPTIONS + + public EnumSet optionsForAllow(String url, String... urlVariables) { + HttpHeaders headers = execute(url, HttpMethod.OPTIONS, null, headersExtractor, urlVariables); + return headers.getAllow(); + } + + public EnumSet optionsForAllow(String url, Map urlVariables) { + HttpHeaders headers = execute(url, HttpMethod.OPTIONS, null, headersExtractor, urlVariables); + return headers.getAllow(); + } + + // execute + + public T execute(String url, + HttpMethod method, + HttpRequestCallback requestCallback, + HttpResponseExtractor responseExtractor, + String... urlVariables) { + UriTemplate uriTemplate = new UriTemplate(url); + URI expanded = uriTemplate.expand(urlVariables); + return doExecute(expanded, method, requestCallback, responseExtractor); + } + + public T execute(String url, + HttpMethod method, + HttpRequestCallback requestCallback, + HttpResponseExtractor responseExtractor, + Map urlVariables) { + UriTemplate uriTemplate = new UriTemplate(url); + URI expanded = uriTemplate.expand(urlVariables); + return doExecute(expanded, method, requestCallback, responseExtractor); + } + + /** + * Execute the given method on the provided URI. The {@link ClientHttpRequest} is processed using the {@link + * HttpRequestCallback}; the response with the {@link HttpResponseExtractor}. + * + * @param url the fully-expanded URL to connect to + * @param method the HTTP method to execute (GET, POST, etc.) + * @param requestCallback object that prepares the request. Can be null. + * @param responseExtractor object that extracts the return value from the response. Can be null. + * @return an arbitrary object, as returned by the {@link HttpResponseExtractor} + */ + protected T doExecute(URI url, + HttpMethod method, + HttpRequestCallback requestCallback, + HttpResponseExtractor responseExtractor) { + Assert.notNull(url, "'url' must not be null"); + Assert.notNull(method, "'method' must not be null"); + ClientHttpResponse response = null; + try { + ClientHttpRequest request = createRequest(url, method); + if (requestCallback != null) { + requestCallback.doWithRequest(request); + } + response = request.execute(); + if (getErrorHandler().hasError(response)) { + getErrorHandler().handleError(response); + } + if (responseExtractor != null) { + return responseExtractor.extractData(response); + } + else { + return null; + } + } + catch (IOException ex) { + throw new HttpIOException("I/O error: " + ex.getMessage(), ex); + } + finally { + if (response != null) { + response.close(); + } + } + } + + /** + * Checks 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 checkForSupportedEntityConverter(Class type) { + for (HttpMessageConverter entityConverter : getMessageConverters()) { + if (entityConverter.supports(type)) { + return; + } + } + throw new IllegalArgumentException("Could not resolve HttpMessageConverter for [" + type.getName() + "]"); + } + + /** + * Request callback implementation that sets the Accept header based on the registered {@linkplain + * HttpMessageConverter entity converters}. + */ + private class AcceptHeaderCallback implements HttpRequestCallback { + + public void doWithRequest(ClientHttpRequest request) throws IOException { + List allSupportedMediaTypes = new ArrayList(); + for (HttpMessageConverter entityConverter : getMessageConverters()) { + List supportedMediaTypes = entityConverter.getSupportedMediaTypes(); + for (MediaType supportedMediaType : supportedMediaTypes) { + if (supportedMediaType.getCharSet() != null) { + supportedMediaType = + new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype()); + } + allSupportedMediaTypes.add(supportedMediaType); + } + } + Collections.sort(allSupportedMediaTypes); + request.getHeaders().setAccept(allSupportedMediaTypes); + } + } + + private class GetCallback implements HttpRequestCallback { + + private final Class responseType; + + private GetCallback(Class responseType) { + this.responseType = responseType; + } + + public void doWithRequest(ClientHttpRequest request) throws IOException { + List allSupportedMediaTypes = new ArrayList(); + for (HttpMessageConverter entityConverter : getSupportedMessageConverters(responseType)) { + List supportedMediaTypes = entityConverter.getSupportedMediaTypes(); + for (MediaType supportedMediaType : supportedMediaTypes) { + if (supportedMediaType.getCharSet() != null) { + supportedMediaType = + new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype()); + } + allSupportedMediaTypes.add(supportedMediaType); + } + } + Collections.sort(allSupportedMediaTypes); + request.getHeaders().setAccept(allSupportedMediaTypes); + } + } + + /** + * Extension of {@link AcceptHeaderCallback} that writes the given object to the request stream. + */ + private class PostPutCallback implements HttpRequestCallback { + + private final Object request; + + private PostPutCallback(Object request) { + this.request = request; + } + + @SuppressWarnings("unchecked") + public void doWithRequest(ClientHttpRequest httpRequest) throws IOException { + for (HttpMessageConverter entityConverter : getSupportedMessageConverters(request.getClass())) { + entityConverter.write(request, httpRequest); + break; + } + } + + } + + /** + * Response extractor that uses the registered {@linkplain HttpMessageConverter entity converters} to convert the + * response into a type T. + */ + private class HttpMessageConverterExtractor implements HttpResponseExtractor { + + private final Class responseType; + + private HttpMessageConverterExtractor(Class responseType) { + this.responseType = responseType; + } + + public T extractData(ClientHttpResponse response) throws IOException { + MediaType contentType = response.getHeaders().getContentType(); + if (contentType == null) { + throw new HttpClientException("Cannot extract response: no Content-Type found"); + } + for (HttpMessageConverter messageConverter : getSupportedMessageConverters(responseType)) { + for (MediaType supportedMediaType : messageConverter.getSupportedMediaTypes()) { + if (supportedMediaType.includes(contentType)) { + return messageConverter.read(responseType, response); + } + } + } + throw new HttpClientException( + "Could not extract response: no suitable HttpMessageConverter found for " + "response type [" + + responseType.getName() + "] and content type [" + contentType + "]"); + } + + } + + /** + * Response extractor that extracts the response {@link HttpHeaders}. + */ + private static class HeadersExtractor implements HttpResponseExtractor { + + public HttpHeaders extractData(ClientHttpResponse response) throws IOException { + return response.getHeaders(); + } + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/core/SimpleHttpErrorHandler.java b/org.springframework.web/src/main/java/org/springframework/web/client/core/SimpleHttpErrorHandler.java new file mode 100644 index 0000000000..5b69f97b37 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/core/SimpleHttpErrorHandler.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2009 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.client.core; + +import java.io.IOException; + +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpClientException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.http.HttpStatus; +import org.springframework.web.http.client.ClientHttpResponse; + +/** + * Default implementation of the {@link HttpErrorHandler} interface. + * + *

This error handler checks for the status code on the {@link ClientHttpResponse}: any code with series + * {@link HttpStatus.Series#CLIENT_ERROR} or {@link HttpStatus.Series#SERVER_ERROR} is considered to be an error. + * This behavior can be changed by overriding the {@link #hasError(HttpStatus)} method. + * + * @author Arjen Poutsma + * @see RestTemplate#setErrorHandler(HttpErrorHandler) + * @since 3.0 + */ +public class SimpleHttpErrorHandler implements HttpErrorHandler { + + /** + * Delegates to {@link #hasError(HttpStatus)} with the response status code. + */ + public boolean hasError(ClientHttpResponse response) throws IOException { + return hasError(response.getStatusCode()); + } + + /** + * Template method called from {@link #hasError(ClientHttpResponse)}. + * + *

Default implementation checks if the given status code is {@link HttpStatus.Series#CLIENT_ERROR} or {@link + * HttpStatus.Series#SERVER_ERROR}. Can be overridden in subclasses. + * + * @param statusCode the HTTP status code + * @return true if the response has an error; false otherwise + * @see HttpStatus.Series#CLIENT_ERROR + * @see HttpStatus.Series#SERVER_ERROR + */ + protected boolean hasError(HttpStatus statusCode) { + return statusCode.series() == HttpStatus.Series.CLIENT_ERROR || + statusCode.series() == HttpStatus.Series.SERVER_ERROR; + } + + public void handleError(ClientHttpResponse response) throws IOException { + HttpStatus statusCode = response.getStatusCode(); + switch (statusCode.series()) { + case CLIENT_ERROR: + throw new HttpClientErrorException(statusCode, response.getStatusText()); + case SERVER_ERROR: + throw new HttpServerErrorException(statusCode, response.getStatusText()); + default: + throw new HttpClientException("Unknown status code [" + statusCode + "]"); + } + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/core/package.html b/org.springframework.web/src/main/java/org/springframework/web/client/core/package.html new file mode 100644 index 0000000000..fe274ba0f5 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/core/package.html @@ -0,0 +1,8 @@ + + + +Core package of the client-side HTTP support. +Provides a RestTemplate class and various callback interfaces. + + + diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/core/support/RestGatewaySupport.java b/org.springframework.web/src/main/java/org/springframework/web/client/core/support/RestGatewaySupport.java new file mode 100644 index 0000000000..0812970f1d --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/core/support/RestGatewaySupport.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2009 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.client.core.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; +import org.springframework.web.client.core.RestTemplate; +import org.springframework.web.http.client.ClientHttpRequestFactory; + +/** + * Convenient super class for application classes that need REST access. + * + *

Requires a {@link ClientHttpRequestFactory} or a {@link RestTemplate} instance to be set. It will create its own + * JmsTemplate if a ConnectionFactory is passed in. A custom JmsTemplate instance can be created for a given + * ConnectionFactory through overriding the createJmsTemplate method. + * + * @author Arjen Poutsma + * @see #setRestTemplate(RestTemplate) + * @see RestTemplate + * @since 3.0 + */ +public class RestGatewaySupport { + + /** + * Logger available to subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + private RestTemplate restTemplate; + + /** + * Constructs a new instance of the {@link RestGatewaySupport}, with default parameters. + * + * @see RestTemplate#RestTemplate() + */ + public RestGatewaySupport() { + restTemplate = new RestTemplate(); + } + + /** + * Constructs a new instance of the {@link RestGatewaySupport}, with the given {@link ClientHttpRequestFactory}. + * + * @see RestTemplate#RestTemplate(ClientHttpRequestFactory + */ + public RestGatewaySupport(ClientHttpRequestFactory requestFactory) { + Assert.notNull(requestFactory, "'requestFactory' must not be null"); + this.restTemplate = new RestTemplate(requestFactory); + } + + /** + * Returns the {@link RestTemplate} for the gateway. + */ + public RestTemplate getRestTemplate() { + return restTemplate; + } + + /** + * Sets the {@link RestTemplate} for the gateway. + */ + public void setRestTemplate(RestTemplate restTemplate) { + Assert.notNull(restTemplate, "'restTemplate' must not be null"); + this.restTemplate = restTemplate; + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/core/support/package.html b/org.springframework.web/src/main/java/org/springframework/web/client/core/support/package.html new file mode 100644 index 0000000000..9a319aa7d6 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/core/support/package.html @@ -0,0 +1,8 @@ + + + +Classes supporting the org.springframework.web.client.core package. +Contains a base class for RestTemplate usage. + + + diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/package.html b/org.springframework.web/src/main/java/org/springframework/web/client/package.html new file mode 100644 index 0000000000..094b557c35 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/package.html @@ -0,0 +1,7 @@ + + + +This package contains integration classes for client-side access of HTTP services. + + + diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/support/HttpAccessor.java b/org.springframework.web/src/main/java/org/springframework/web/client/support/HttpAccessor.java new file mode 100644 index 0000000000..e3e868d352 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/support/HttpAccessor.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2009 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.client.support; + +import java.io.IOException; +import java.net.URI; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; +import org.springframework.web.http.HttpMethod; +import org.springframework.web.http.client.ClientHttpRequest; +import org.springframework.web.http.client.ClientHttpRequestFactory; +import org.springframework.web.http.client.SimpleClientHttpRequestFactory; + +/** + * Base class for {@link org.springframework.web.client.core.RestTemplate} and other HTTP accessing gateway helpers, defining + * common properties such as the {@link ClientHttpRequestFactory} to operate on. + *

+ * Not intended to be used directly. See {@link org.springframework.web.client.core.RestTemplate}. + * + * @author Arjen Poutsma + * @see org.springframework.web.client.core.RestTemplate + */ +public abstract class HttpAccessor { + + /** + * Logger available to subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + + /** + * Returns the request factory that this accessor uses for obtaining {@link ClientHttpRequest HttpRequests} + */ + public ClientHttpRequestFactory getRequestFactory() { + return requestFactory; + } + + /** + * Sets the request factory that this accessor uses for obtaining {@link ClientHttpRequest HttpRequests} + */ + public void setRequestFactory(ClientHttpRequestFactory requestFactory) { + Assert.notNull(requestFactory, "'requestFactory' must not be null"); + this.requestFactory = requestFactory; + } + + /** + * Creates a new {@link ClientHttpRequest} via this template's {@link ClientHttpRequestFactory}. + * + * @param url the URL to connect to + * @param method the HTTP method to exectute (GET, POST, etc.) + * @return the created request + * @throws IOException in case of I/O errors + */ + protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException { + ClientHttpRequest request = getRequestFactory().createRequest(url, method); + if (logger.isDebugEnabled()) { + logger.debug("Created " + method.name() + " request for \"" + url + "\""); + } + return request; + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/support/package.html b/org.springframework.web/src/main/java/org/springframework/web/client/support/package.html new file mode 100644 index 0000000000..b6dbef257a --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/web/client/support/package.html @@ -0,0 +1,8 @@ + + + +This package provides generic HTTP support classes, +to be used by higher-level classes like RestTemplate. + + + diff --git a/org.springframework.web/src/test/java/org/springframework/web/client/core/RestTemplateIntegrationTests.java b/org.springframework.web/src/test/java/org/springframework/web/client/core/RestTemplateIntegrationTests.java new file mode 100644 index 0000000000..52fb2816e1 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/client/core/RestTemplateIntegrationTests.java @@ -0,0 +1,171 @@ +/* + * Copyright 2002-2009 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.client.core; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.EnumSet; +import javax.servlet.GenericServlet; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.AfterClass; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mortbay.jetty.Server; +import org.mortbay.jetty.servlet.Context; +import org.mortbay.jetty.servlet.ServletHolder; + +import org.springframework.util.FileCopyUtils; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.http.HttpMethod; +import org.springframework.web.http.client.commons.CommonsClientHttpRequestFactory; + +/** + * @author Arjen Poutsma + */ +public class RestTemplateIntegrationTests { + + private RestTemplate template; + + private static Server jettyServer; + + @BeforeClass + public static void startJettyServer() throws Exception { + jettyServer = new Server(8889); + Context jettyContext = new Context(jettyServer, "/"); + String s = "H\u00e9llo W\u00f6rld"; + byte[] bytes = s.getBytes("UTF-8"); + jettyContext.addServlet(new ServletHolder(new GetServlet(bytes, "text/plain;charset=utf-8")), "/get"); + jettyContext + .addServlet(new ServletHolder(new PostServlet(s, new URI("http://localhost:8889/post/1"))), "/post"); + jettyContext.addServlet(new ServletHolder(new ErrorServlet(404)), "/errors/notfound"); + jettyContext.addServlet(new ServletHolder(new ErrorServlet(500)), "/errors/server"); + jettyServer.start(); + } + + @Before + public void createTemplate() { +// template = new RestTemplate(); + template = new RestTemplate(new CommonsClientHttpRequestFactory()); + } + + @AfterClass + public static void stopJettyServer() throws Exception { + if (jettyServer != null) { + jettyServer.stop(); + } + } + + @Test + public void getString() { + String s = template.getForObject("http://localhost:8889/{method}", String.class, "get"); + assertEquals("Invalid content", "H\u00e9llo W\u00f6rld", s); + } + + @Test + public void postString() throws URISyntaxException { + URI location = template.postForLocation("http://localhost:8889/{method}", "H\u00e9llo W\u00f6rld", "post"); + assertEquals("Invalid location", new URI("http://localhost:8889/post/1"), location); + } + + @Test(expected = HttpClientErrorException.class) + public void notFound() { + template.execute("http://localhost:8889/errors/notfound", HttpMethod.GET, null, null); + } + + @Test(expected = HttpServerErrorException.class) + public void serverError() { + template.execute("http://localhost:8889/errors/server", HttpMethod.GET, null, null); + } + + @Test + public void optionsForAllow() { + EnumSet allowed = template.optionsForAllow("http://localhost:8889/get"); + assertEquals("Invalid response", + EnumSet.of(HttpMethod.GET, HttpMethod.OPTIONS, HttpMethod.HEAD, HttpMethod.TRACE), allowed); + } + + /** + * Servlet that returns and error message for a given status code. + */ + private static class ErrorServlet extends GenericServlet { + + private final int sc; + + private ErrorServlet(int sc) { + this.sc = sc; + } + + @Override + public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { + ((HttpServletResponse) response).sendError(sc); + } + } + + private static class GetServlet extends HttpServlet { + + private final byte[] buf; + + private final String contentType; + + private GetServlet(byte[] buf, String contentType) { + this.buf = buf; + this.contentType = contentType; + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentLength(buf.length); + response.setContentType(contentType); + FileCopyUtils.copy(buf, response.getOutputStream()); + } + } + + private static class PostServlet extends HttpServlet { + + private final String s; + + private final URI location; + + private PostServlet(String s, URI location) { + this.s = s; + this.location = location; + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + assertTrue("Invalid request content-length", request.getContentLength() > 0); + assertNotNull("No content-type", request.getContentType()); + String body = FileCopyUtils.copyToString(request.getReader()); + assertEquals("Invalid request body", s, body); + response.setStatus(HttpServletResponse.SC_CREATED); + response.setHeader("Location", location.toASCIIString()); + } + } + +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/client/core/RestTemplateTest.java b/org.springframework.web/src/test/java/org/springframework/web/client/core/RestTemplateTest.java new file mode 100644 index 0000000000..a6a321bdfd --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/web/client/core/RestTemplateTest.java @@ -0,0 +1,349 @@ +/* + * Copyright 2002-2009 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.client.core; + +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; + +import static org.easymock.EasyMock.*; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.util.MediaType; +import org.springframework.web.client.HttpClientException; +import org.springframework.web.client.HttpIOException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.converter.ByteArrayHttpMessageConverter; +import org.springframework.web.converter.HttpMessageConverter; +import org.springframework.web.converter.StringHttpMessageConverter; +import org.springframework.web.http.HttpHeaders; +import org.springframework.web.http.HttpMethod; +import org.springframework.web.http.HttpStatus; +import org.springframework.web.http.client.ClientHttpRequest; +import org.springframework.web.http.client.ClientHttpRequestFactory; +import org.springframework.web.http.client.ClientHttpResponse; + +/** + * @author Arjen Poutsma + */ +@SuppressWarnings("unchecked") +public class RestTemplateTest { + + private RestTemplate template; + + private ClientHttpRequestFactory requestFactory; + + private ClientHttpRequest request; + + private ClientHttpResponse response; + + private HttpErrorHandler errorHandler; + + private HttpMessageConverter converter; + + @Before + public void setUp() { + requestFactory = createMock(ClientHttpRequestFactory.class); + request = createMock(ClientHttpRequest.class); + response = createMock(ClientHttpResponse.class); + errorHandler = createMock(HttpErrorHandler.class); + converter = createMock(HttpMessageConverter.class); + template = new RestTemplate(requestFactory); + template.setErrorHandler(errorHandler); + template.setMessageConverters(new HttpMessageConverter[]{converter}); + } + + @Test + public void getSupportedMessageBodyConverters() { + ByteArrayHttpMessageConverter byteArrayConverter = new ByteArrayHttpMessageConverter(); + StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(); + template.setMessageConverters(new HttpMessageConverter[]{byteArrayConverter, stringConverter}); + + List> result = template.getSupportedMessageConverters(String.class); + assertEquals("Invalid amount of String converters", 1, result.size()); + assertEquals("Invalid String converters", stringConverter, result.get(0)); + } + + @Test + public void varArgsTemplateVariables() throws Exception { + expect(requestFactory.createRequest(new URI("http://example.com/hotels/42/bookings/21"), HttpMethod.GET)) + .andReturn(request); + expect(request.execute()).andReturn(response); + expect(errorHandler.hasError(response)).andReturn(false); + response.close(); + + replayMocks(); + + template.execute("http://example.com/hotels/{hotel}/bookings/{booking}", HttpMethod.GET, null, null, "42", + "21"); + + verifyMocks(); + } + + @Test + public void mapTemplateVariables() throws Exception { + expect(requestFactory.createRequest(new URI("http://example.com/hotels/42/bookings/42"), HttpMethod.GET)) + .andReturn(request); + expect(request.execute()).andReturn(response); + expect(errorHandler.hasError(response)).andReturn(false); + response.close(); + + replayMocks(); + + Map vars = Collections.singletonMap("hotel", "42"); + template.execute("http://example.com/hotels/{hotel}/bookings/{hotel}", HttpMethod.GET, null, null, vars); + + verifyMocks(); + } + + @Test + public void errorHandling() throws Exception { + expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.GET)).andReturn(request); + expect(request.execute()).andReturn(response); + expect(errorHandler.hasError(response)).andReturn(true); + errorHandler.handleError(response); + expectLastCall().andThrow(new HttpServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR)); + response.close(); + + replayMocks(); + + try { + template.execute("http://example.com", HttpMethod.GET, null, null); + fail("HttpServerErrorException expected"); + } + catch (HttpServerErrorException ex) { + // expected + } + verifyMocks(); + } + + @Test + public void getForObject() throws Exception { + expect(converter.supports(String.class)).andReturn(true).times(3); + MediaType textPlain = new MediaType("text", "plain"); + expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(textPlain)).times(2); + expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.GET)).andReturn(request); + HttpHeaders requestHeaders = new HttpHeaders(); + expect(request.getHeaders()).andReturn(requestHeaders); + expect(request.execute()).andReturn(response); + expect(errorHandler.hasError(response)).andReturn(false); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentType(textPlain); + expect(response.getHeaders()).andReturn(responseHeaders); + String expected = "Hello World"; + expect(converter.read(String.class, response)).andReturn(expected); + response.close(); + + replayMocks(); + + String result = template.getForObject("http://example.com", String.class); + assertEquals("Invalid GET result", expected, result); + + 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 + public void getUnsupportedMediaType() throws Exception { + expect(converter.supports(String.class)).andReturn(true).times(3); + MediaType supportedMediaType = new MediaType("foo", "bar"); + expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(supportedMediaType)).times(2); + expect(requestFactory.createRequest(new URI("http://example.com/resource"), HttpMethod.GET)).andReturn(request); + HttpHeaders requestHeaders = new HttpHeaders(); + expect(request.getHeaders()).andReturn(requestHeaders); + expect(request.execute()).andReturn(response); + expect(errorHandler.hasError(response)).andReturn(false); + HttpHeaders responseHeaders = new HttpHeaders(); + MediaType contentType = new MediaType("bar", "baz"); + responseHeaders.setContentType(contentType); + expect(response.getHeaders()).andReturn(responseHeaders); + response.close(); + + replayMocks(); + + try { + template.getForObject("http://example.com/{p}", String.class, "resource"); + fail("UnsupportedMediaTypeException expected"); + } + catch (HttpClientException ex) { + // expected + } + verifyMocks(); + } + + @Test + public void headForHeaders() throws Exception { + expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.HEAD)).andReturn(request); + expect(request.execute()).andReturn(response); + expect(errorHandler.hasError(response)).andReturn(false); + HttpHeaders responseHeaders = new HttpHeaders(); + expect(response.getHeaders()).andReturn(responseHeaders); + response.close(); + + replayMocks(); + HttpHeaders result = template.headForHeaders("http://example.com"); + + assertSame("Invalid headers returned", responseHeaders, result); + + verifyMocks(); + } + + @Test + 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); + String helloWorld = "Hello World"; + converter.write(helloWorld, request); + expect(request.execute()).andReturn(response); + expect(errorHandler.hasError(response)).andReturn(false); + HttpHeaders responseHeaders = new HttpHeaders(); + URI expected = new URI("http://example.com/hotels"); + responseHeaders.setLocation(expected); + expect(response.getHeaders()).andReturn(responseHeaders); + response.close(); + + replayMocks(); + + URI result = template.postForLocation("http://example.com", helloWorld); + assertEquals("Invalid POST result", expected, result); + + verifyMocks(); + } + + @Test + 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); + String helloWorld = "Hello World"; + converter.write(helloWorld, request); + expect(request.execute()).andReturn(response); + expect(errorHandler.hasError(response)).andReturn(false); + HttpHeaders responseHeaders = new HttpHeaders(); + expect(response.getHeaders()).andReturn(responseHeaders); + response.close(); + + replayMocks(); + + URI result = template.postForLocation("http://example.com", helloWorld); + assertNull("Invalid POST result", result); + + verifyMocks(); + } + + @Test + public void put() throws Exception { + expect(converter.supports(String.class)).andReturn(true).times(2); + expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.PUT)).andReturn(request); + String helloWorld = "Hello World"; + converter.write(helloWorld, request); + expect(request.execute()).andReturn(response); + expect(errorHandler.hasError(response)).andReturn(false); + response.close(); + + replayMocks(); + + template.put("http://example.com", helloWorld); + + verifyMocks(); + } + + @Test + public void delete() throws Exception { + expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.DELETE)).andReturn(request); + expect(request.execute()).andReturn(response); + expect(errorHandler.hasError(response)).andReturn(false); + response.close(); + + replayMocks(); + + template.delete("http://example.com"); + + verifyMocks(); + } + + @Test + public void optionsForAllow() throws Exception { + expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.OPTIONS)).andReturn(request); + expect(request.execute()).andReturn(response); + expect(errorHandler.hasError(response)).andReturn(false); + HttpHeaders responseHeaders = new HttpHeaders(); + EnumSet expected = EnumSet.of(HttpMethod.GET, HttpMethod.POST); + responseHeaders.setAllow(expected); + expect(response.getHeaders()).andReturn(responseHeaders); + response.close(); + + replayMocks(); + + EnumSet result = template.optionsForAllow("http://example.com"); + assertEquals("Invalid OPTIONS result", expected, result); + + verifyMocks(); + } + + @Test + public void ioException() throws Exception { + expect(converter.supports(String.class)).andReturn(true).times(2); + MediaType mediaType = new MediaType("foo", "bar"); + expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(mediaType)); + expect(requestFactory.createRequest(new URI("http://example.com/resource"), HttpMethod.GET)).andReturn(request); + expect(request.getHeaders()).andReturn(new HttpHeaders()); + expect(request.execute()).andThrow(new IOException()); + + replayMocks(); + + try { + template.getForObject("http://example.com/resource", String.class); + fail("RestClientException expected"); + } + catch (HttpIOException ex) { + // expected + } + + verifyMocks(); + } + + private void replayMocks() { + replay(requestFactory, request, response, errorHandler, converter); + } + + private void verifyMocks() { + verify(requestFactory, request, response, errorHandler, converter); + } + + +}