diff --git a/build.gradle b/build.gradle index af8914b2236..ad6b22101df 100644 --- a/build.gradle +++ b/build.gradle @@ -478,7 +478,8 @@ project("spring-web") { optional("com.caucho:hessian:4.0.7") optional("rome:rome:1.0") optional("commons-fileupload:commons-fileupload:1.3") - optional("org.apache.httpcomponents:httpclient:4.2") + optional("org.apache.httpcomponents:httpclient:4.3-beta2") + optional("org.apache.httpcomponents:httpasyncclient:4.0-beta4") optional("org.codehaus.jackson:jackson-mapper-asl:1.9.12") optional("com.fasterxml.jackson.core:jackson-databind:2.2.0") optional("taglibs:standard:1.1.2") @@ -619,7 +620,7 @@ project("spring-webmvc") { testCompile("commons-fileupload:commons-fileupload:1.2") testCompile("commons-io:commons-io:1.3") testCompile("org.hibernate:hibernate-validator:4.3.0.Final") - testCompile("org.apache.httpcomponents:httpclient:4.2") + testCompile("org.apache.httpcomponents:httpclient:4.3-beta2") } // pick up DispatcherServlet.properties in src/main diff --git a/spring-web/src/main/java/org/springframework/http/client/AbstractAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/AbstractAsyncClientHttpRequest.java new file mode 100644 index 00000000000..9292bcf2804 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/AbstractAsyncClientHttpRequest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2013 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.http.client; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.Future; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.Assert; + +/** + * Abstract base for {@link AsyncClientHttpRequest} that makes sure that headers and body + * are not written multiple times. + * + * @author Arjen Poutsma + * @since 4.0 + */ +abstract class AbstractAsyncClientHttpRequest implements AsyncClientHttpRequest { + + private final HttpHeaders headers = new HttpHeaders(); + + private boolean executed = false; + + + @Override + public final HttpHeaders getHeaders() { + return (this.executed ? HttpHeaders.readOnlyHttpHeaders(this.headers) : this.headers); + } + + @Override + public final OutputStream getBody() throws IOException { + assertNotExecuted(); + return getBodyInternal(this.headers); + } + + @Override + public Future executeAsync() throws IOException { + assertNotExecuted(); + Future result = executeInternal(this.headers); + this.executed = true; + return result; + } + + /** + * Asserts that this request has not been {@linkplain #execute() executed} yet. + * + * @throws IllegalStateException if this request has been executed + */ + protected void assertNotExecuted() { + Assert.state(!this.executed, "ClientHttpRequest already executed"); + } + + + /** + * Abstract template method that returns the body. + * @param headers the HTTP headers + * @return the body output stream + */ + protected abstract OutputStream getBodyInternal(HttpHeaders headers) throws IOException; + + /** + * Abstract template method that writes the given headers and content to the HTTP request. + * @param headers the HTTP headers + * @return the response object for the executed request + */ + protected abstract Future executeInternal(HttpHeaders headers) throws IOException; + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/AbstractBufferingAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/AbstractBufferingAsyncClientHttpRequest.java new file mode 100644 index 00000000000..efa290f5360 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/AbstractBufferingAsyncClientHttpRequest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2013 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.http.client; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.concurrent.Future; + +import org.springframework.http.HttpHeaders; + +/** + * Abstract base for {@link org.springframework.http.client.ClientHttpRequest} that buffers output in a byte array before sending it over the wire. + * + * @author Arjen Poutsma + * @since 3.0.6 + */ +abstract class AbstractBufferingAsyncClientHttpRequest + extends AbstractAsyncClientHttpRequest { + + private ByteArrayOutputStream bufferedOutput = new ByteArrayOutputStream(); + + @Override + protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException { + return this.bufferedOutput; + } + + @Override + protected Future executeInternal(HttpHeaders headers) throws IOException { + byte[] bytes = this.bufferedOutput.toByteArray(); + if (headers.getContentLength() == -1) { + headers.setContentLength(bytes.length); + } + Future result = executeInternal(headers, bytes); + this.bufferedOutput = null; + return result; + } + + /** + * Abstract template method that writes the given headers and content to the HTTP + * request. + * + * @param headers the HTTP headers + * @param bufferedOutput the body content + * @return the response object for the executed request + */ + protected abstract Future executeInternal(HttpHeaders headers, + byte[] bufferedOutput) throws IOException; + + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequest.java new file mode 100644 index 00000000000..55ec53a39f9 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2013 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.http.client; + +import java.io.IOException; +import java.util.concurrent.Future; + +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.HttpRequest; + +/** + * Represents a client-side asynchronous HTTP request. Created via an implementation of + * the {@link AsyncClientHttpRequestFactory}. + *

A {@code AsyncHttpRequest} can be {@linkplain #executeAsync() executed}, getting a + * future {@link ClientHttpResponse} which can be read from. + * + * @author Arjen Poutsma + * @since 4.0 + * @see AsyncClientHttpRequestFactory#createAsyncRequest(java.net.URI, org.springframework.http.HttpMethod) + */ +public interface AsyncClientHttpRequest extends HttpRequest, HttpOutputMessage { + + /** + * Execute this request asynchronously, resulting in a future + * {@link ClientHttpResponse} that can be read. + * + * @return the future response result of the execution + * @throws java.io.IOException in case of I/O errors + */ + Future executeAsync() throws IOException; + + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestFactory.java new file mode 100644 index 00000000000..89cdd9fd0d9 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/AsyncClientHttpRequestFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2013 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.http.client; + +import java.io.IOException; +import java.net.URI; + +import org.springframework.http.HttpMethod; + +/** + * Factory for {@link AsyncClientHttpRequest} objects. Requests are created by the + * {@link #createAsyncRequest(URI, HttpMethod)} method. + * + * @author Arjen Poutsma + * @since 4.0 + */ +public interface AsyncClientHttpRequestFactory { + + /** + * Create a new asynchronous {@link AsyncClientHttpRequest} for the specified URI and + * HTTP method. + *

The returned request can be written to, and then executed by calling + * {@link AsyncClientHttpRequest#executeAsync()}. + * + * @param uri the URI to create a request for + * @param httpMethod the HTTP method to execute + * @return the created request + * @throws IOException in case of I/O errors + */ + AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) + throws IOException; + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequest.java index 7137365507d..5532359bbc6 100644 --- a/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/ClientHttpRequest.java @@ -25,8 +25,8 @@ import org.springframework.http.HttpRequest; /** * Represents a client-side HTTP request. Created via an implementation of the {@link ClientHttpRequestFactory}. * - *

A {@code HttpRequest} can be {@linkplain #execute() executed}, getting a {@link ClientHttpResponse} - * which can be read from. + *

A {@code ClientHttpRequest} can be {@linkplain #execute() executed}, getting a + * {@link ClientHttpResponse} which can be read from. * * @author Arjen Poutsma * @since 3.0 diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequest.java new file mode 100644 index 00000000000..7616f686577 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequest.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2013 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.http.client; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.nio.client.HttpAsyncClient; +import org.apache.http.protocol.HttpContext; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * {@link ClientHttpRequest} implementation that uses Apache HttpComponents HttpClient to + * execute requests. + * + *

Created via the {@link org.springframework.http.client.HttpComponentsClientHttpRequestFactory}. + * + * @author Oleg Kalnichevski + * @author Arjen Poutsma + * @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory#createRequest(java.net.URI, + * org.springframework.http.HttpMethod) + * @since 3.1 + */ +final class HttpComponentsAsyncClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest { + + private final HttpAsyncClient httpClient; + + private final HttpUriRequest httpRequest; + + private final HttpContext httpContext; + + public HttpComponentsAsyncClientHttpRequest(HttpAsyncClient httpClient, + HttpUriRequest httpRequest, HttpContext httpContext) { + this.httpClient = httpClient; + this.httpRequest = httpRequest; + this.httpContext = httpContext; + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(this.httpRequest.getMethod()); + } + + @Override + public URI getURI() { + return this.httpRequest.getURI(); + } + + @Override + protected Future executeInternal(HttpHeaders headers, + byte[] bufferedOutput) throws IOException { + HttpComponentsClientHttpRequest.addHeaders(this.httpRequest, headers); + + if (this.httpRequest instanceof HttpEntityEnclosingRequest) { + HttpEntityEnclosingRequest entityEnclosingRequest = + (HttpEntityEnclosingRequest) this.httpRequest; + HttpEntity requestEntity = new ByteArrayEntity(bufferedOutput); + entityEnclosingRequest.setEntity(requestEntity); + } + + final Future futureResponse = + this.httpClient.execute(this.httpRequest, this.httpContext, null); + return new ClientHttpResponseFuture(futureResponse); + } + + + private static class ClientHttpResponseFuture implements Future { + + private final Future futureResponse; + + + public ClientHttpResponseFuture(Future futureResponse) { + this.futureResponse = futureResponse; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return futureResponse.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return futureResponse.isCancelled(); + } + + @Override + public boolean isDone() { + return futureResponse.isDone(); + } + + @Override + public ClientHttpResponse get() + throws InterruptedException, ExecutionException { + HttpResponse response = futureResponse.get(); + return new HttpComponentsClientHttpResponse(response); + } + + @Override + public ClientHttpResponse get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + HttpResponse response = futureResponse.get(timeout, unit); + return new HttpComponentsClientHttpResponse(response); + } + + } + + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactory.java new file mode 100644 index 00000000000..9b07b7d9e54 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactory.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2013 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.http.client; + +import java.io.IOException; +import java.net.URI; + +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.nio.client.HttpAsyncClients; +import org.apache.http.nio.client.HttpAsyncClient; +import org.apache.http.nio.reactor.IOReactorStatus; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; + +/** + * Asynchronous extension of the {@link HttpComponentsClientHttpRequestFactory}. Uses + * Apache HttpComponents + * HttpAsyncClient to create requests. + * + * @author Arjen Poutsma + * @since 4.0 + * @see HttpAsyncClient + */ +public class HttpComponentsAsyncClientHttpRequestFactory + extends HttpComponentsClientHttpRequestFactory + implements AsyncClientHttpRequestFactory, InitializingBean { + + private HttpAsyncClient httpAsyncClient; + + + /** + * Create a new instance of the {@code HttpComponentsAsyncClientHttpRequestFactory} + * with a default {@link HttpAsyncClient} and {@link HttpClient}. + */ + public HttpComponentsAsyncClientHttpRequestFactory() { + this(HttpAsyncClients.createDefault()); + } + + /** + * Create a new instance of the {@code HttpComponentsAsyncClientHttpRequestFactory} + * with the given {@link HttpAsyncClient} instance and a default {@link HttpClient}. + * @param httpAsyncClient the HttpAsyncClient instance to use for this request factory + */ + public HttpComponentsAsyncClientHttpRequestFactory(HttpAsyncClient httpAsyncClient) { + super(); + Assert.notNull(httpAsyncClient, "'httpAsyncClient' must not be null"); + this.httpAsyncClient = httpAsyncClient; + } + + /** + * Create a new instance of the {@code HttpComponentsAsyncClientHttpRequestFactory} + * with the given {@link HttpClient} and {@link HttpAsyncClient} instances. + * @param httpClient the HttpClient instance to use for this request factory + * @param httpAsyncClient the HttpAsyncClient instance to use for this request factory + */ + public HttpComponentsAsyncClientHttpRequestFactory(HttpClient httpClient, + HttpAsyncClient httpAsyncClient) { + super(httpClient); + Assert.notNull(httpAsyncClient, "'httpAsyncClient' must not be null"); + this.httpAsyncClient = httpAsyncClient; + } + + /** + * Set the {@code HttpClient} used for + * {@linkplain #createAsyncRequest(java.net.URI, org.springframework.http.HttpMethod) asynchronous execution}. + */ + public void setHttpAsyncClient(HttpAsyncClient httpAsyncClient) { + this.httpAsyncClient = httpAsyncClient; + } + + /** + * Return the {@code HttpClient} used for + * {@linkplain #createAsyncRequest(URI, HttpMethod) asynchronous execution}. + */ + public HttpAsyncClient getHttpAsyncClient() { + return httpAsyncClient; + } + + @Override + public void afterPropertiesSet() { + startAsyncClient(); + } + + private void startAsyncClient() { + HttpAsyncClient asyncClient = getHttpAsyncClient(); + if (asyncClient.getStatus() != IOReactorStatus.ACTIVE) { + asyncClient.start(); + } + } + + @Override + public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) + throws IOException { + HttpAsyncClient asyncClient = getHttpAsyncClient(); + startAsyncClient(); + HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri); + postProcessHttpRequest(httpRequest); + return new HttpComponentsAsyncClientHttpRequest(asyncClient, httpRequest, + createHttpContext(httpMethod, uri)); + } + + @Override + public void destroy() throws Exception { + try { + super.destroy(); + } + finally { + getHttpAsyncClient().shutdown(); + } + } +} diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java index 8d789382e7b..7b6e178d6c1 100644 --- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java @@ -29,12 +29,7 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpTrace; import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.conn.scheme.PlainSocketFactory; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.conn.ssl.SSLSocketFactory; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.PoolingClientConnectionManager; +import org.apache.http.impl.client.HttpClients; import org.apache.http.params.CoreConnectionPNames; import org.apache.http.protocol.HttpContext; @@ -43,9 +38,9 @@ import org.springframework.http.HttpMethod; import org.springframework.util.Assert; /** - * {@link org.springframework.http.client.ClientHttpRequestFactory} implementation that uses - * Apache HttpComponents HttpClient - * to create requests. + * {@link org.springframework.http.client.ClientHttpRequestFactory} implementation that + * uses Apache HttpComponents + * HttpClient to create requests. * *

Allows to use a pre-configured {@link HttpClient} instance - * potentially with authentication, HTTP connection pooling, etc. @@ -54,56 +49,43 @@ import org.springframework.util.Assert; * @author Arjen Poutsma * @since 3.1 */ -public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory, DisposableBean { - - private static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 100; - - private static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 5; - - private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000); +public class HttpComponentsClientHttpRequestFactory + implements ClientHttpRequestFactory, DisposableBean { private HttpClient httpClient; private boolean bufferRequestBody = true; + /** - * Create a new instance of the HttpComponentsClientHttpRequestFactory with a default - * {@link HttpClient} that uses a default {@link org.apache.http.impl.conn.PoolingClientConnectionManager}. + * Create a new instance of the {@code HttpComponentsClientHttpRequestFactory} with + * a default {@link HttpClient}. */ public HttpComponentsClientHttpRequestFactory() { - SchemeRegistry schemeRegistry = new SchemeRegistry(); - schemeRegistry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory())); - schemeRegistry.register(new Scheme("https", 443, SSLSocketFactory.getSocketFactory())); - - PoolingClientConnectionManager connectionManager = new PoolingClientConnectionManager(schemeRegistry); - connectionManager.setMaxTotal(DEFAULT_MAX_TOTAL_CONNECTIONS); - connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_CONNECTIONS_PER_ROUTE); - - this.httpClient = new DefaultHttpClient(connectionManager); - setReadTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS); + this(HttpClients.createDefault()); } - /** - * Create a new instance of the HttpComponentsClientHttpRequestFactory + * Create a new instance of the {@code HttpComponentsClientHttpRequestFactory} * with the given {@link HttpClient} instance. * @param httpClient the HttpClient instance to use for this request factory */ public HttpComponentsClientHttpRequestFactory(HttpClient httpClient) { - Assert.notNull(httpClient, "HttpClient must not be null"); + Assert.notNull(httpClient, "'httpClient' must not be null"); this.httpClient = httpClient; } - /** - * Set the {@code HttpClient} used by this factory. + * Set the {@code HttpClient} used for + * {@linkplain #createRequest(URI, HttpMethod) synchronous execution}. */ public void setHttpClient(HttpClient httpClient) { this.httpClient = httpClient; } /** - * Return the {@code HttpClient} used by this factory. + * Return the {@code HttpClient} used for + * {@linkplain #createRequest(URI, HttpMethod) synchronous execution}. */ public HttpClient getHttpClient() { return this.httpClient; @@ -113,7 +95,9 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest * Set the connection timeout for the underlying HttpClient. * A timeout value of 0 specifies an infinite timeout. * @param timeout the timeout value in milliseconds + * @deprecated With no direct replacement */ + @Deprecated public void setConnectTimeout(int timeout) { Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value"); getHttpClient().getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, timeout); @@ -123,7 +107,9 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest * Set the socket read timeout for the underlying HttpClient. * A timeout value of 0 specifies an infinite timeout. * @param timeout the timeout value in milliseconds + * @deprecated With no direct replacement */ + @Deprecated public void setReadTimeout(int timeout) { Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value"); getHttpClient().getParams().setIntParameter(CoreConnectionPNames.SO_TIMEOUT, timeout); @@ -139,16 +125,28 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest this.bufferRequestBody = bufferRequestBody; } + /** + * Indicates whether this request factory should buffer the request body internally. + */ + public boolean isBufferRequestBody() { + return bufferRequestBody; + } + + + @Override public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { + HttpClient client = getHttpClient(); + Assert.state(client != null, + "Synchronous execution requires an HttpClient to be set"); HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri); postProcessHttpRequest(httpRequest); if (bufferRequestBody) { - return new HttpComponentsClientHttpRequest(getHttpClient(), httpRequest, + return new HttpComponentsClientHttpRequest(client, httpRequest, createHttpContext(httpMethod, uri)); } else { - return new HttpComponentsStreamingClientHttpRequest(getHttpClient(), + return new HttpComponentsStreamingClientHttpRequest(client, httpRequest, createHttpContext(httpMethod, uri)); } } @@ -208,7 +206,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest * connection pool, if any. */ @Override - public void destroy() { + public void destroy() throws Exception { getHttpClient().getConnectionManager().shutdown(); } diff --git a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsStreamingClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsStreamingClientHttpRequest.java index ab670128ddf..d5736cb42dd 100644 --- a/spring-web/src/main/java/org/springframework/http/client/HttpComponentsStreamingClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/HttpComponentsStreamingClientHttpRequest.java @@ -108,6 +108,7 @@ final class HttpComponentsStreamingClientHttpRequest extends AbstractClientHttpR private final StreamingHttpOutputMessage.Body body; + private StreamingHttpEntity(HttpHeaders headers, StreamingHttpOutputMessage.Body body) { this.headers = headers; diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java new file mode 100644 index 00000000000..8ceff285cf6 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleBufferingAsyncClientHttpRequest.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2013 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.http.client; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; + +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.FileCopyUtils; + +/** + * {@link org.springframework.http.client.ClientHttpRequest} implementation that uses + * standard J2SE facilities to execute buffered requests. Created via the + * {@link org.springframework.http.client.SimpleClientHttpRequestFactory}. + * + * @author Arjen Poutsma + * @since 3.0 + * @see org.springframework.http.client.SimpleClientHttpRequestFactory#createRequest(java.net.URI, org.springframework.http.HttpMethod) + */ +final class SimpleBufferingAsyncClientHttpRequest extends AbstractBufferingAsyncClientHttpRequest { + + private final HttpURLConnection connection; + + private final boolean outputStreaming; + + private final AsyncTaskExecutor taskExecutor; + + SimpleBufferingAsyncClientHttpRequest(HttpURLConnection connection, + boolean outputStreaming, AsyncTaskExecutor taskExecutor) { + this.connection = connection; + this.outputStreaming = outputStreaming; + this.taskExecutor = taskExecutor; + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(this.connection.getRequestMethod()); + } + + @Override + public URI getURI() { + try { + return this.connection.getURL().toURI(); + } + catch (URISyntaxException ex) { + throw new IllegalStateException("Could not get HttpURLConnection URI: " + ex.getMessage(), ex); + } + } + + @Override + protected Future executeInternal(final HttpHeaders headers, + final byte[] bufferedOutput) throws IOException { + return taskExecutor.submit(new Callable() { + @Override + public ClientHttpResponse call() throws Exception { + for (Map.Entry> entry : headers.entrySet()) { + String headerName = entry.getKey(); + for (String headerValue : entry.getValue()) { + connection.addRequestProperty(headerName, headerValue); + } + } + + if (connection.getDoOutput() && outputStreaming) { + connection.setFixedLengthStreamingMode(bufferedOutput.length); + } + + connection.connect(); + if (connection.getDoOutput()) { + FileCopyUtils.copy(bufferedOutput, connection.getOutputStream()); + } + return new SimpleClientHttpResponse(connection); + } + }); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java index f6c69ec2158..006886a2f00 100644 --- a/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleClientHttpRequestFactory.java @@ -23,6 +23,7 @@ import java.net.URI; import java.net.URL; import java.net.URLConnection; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; @@ -33,9 +34,10 @@ import org.springframework.util.Assert; * @author Juergen Hoeller * @since 3.0 * @see java.net.HttpURLConnection - * @see CommonsClientHttpRequestFactory + * @see HttpComponentsClientHttpRequestFactory */ -public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory { +public class SimpleClientHttpRequestFactory + implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory { private static final int DEFAULT_CHUNK_SIZE = 4096; @@ -52,6 +54,8 @@ public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory private boolean outputStreaming = true; + private AsyncTaskExecutor taskExecutor; + /** * Set the {@link Proxy} to use for this request factory. @@ -121,6 +125,15 @@ public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory this.outputStreaming = outputStreaming; } + /** + * Sets the task executor for this request factory. Setting this property is required + * for {@linkplain #createAsyncRequest(URI, HttpMethod) creating asynchronous + * request}. + * @param taskExecutor the task executor + */ + public void setTaskExecutor(AsyncTaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + } @Override public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { @@ -130,8 +143,28 @@ public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming); } else { - return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, - this.outputStreaming); + return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming); + } + } + + /** + * {@inheritDoc} + *

Setting the {@link #setTaskExecutor(AsyncTaskExecutor) taskExecutor} property + * is required before calling this method. + */ + @Override + public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) + throws IOException { + Assert.state(this.taskExecutor != null, "Asynchronous execution requires an " + + "AsyncTaskExecutor to be set"); + HttpURLConnection connection = openConnection(uri.toURL(), this.proxy); + prepareConnection(connection, httpMethod.name()); + if (this.bufferRequestBody) { + return new SimpleBufferingAsyncClientHttpRequest(connection, this.outputStreaming, this.taskExecutor); + } + else { + return new SimpleStreamingAsyncClientHttpRequest(connection, this.chunkSize, + this.outputStreaming, this.taskExecutor); } } diff --git a/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java new file mode 100644 index 00000000000..dfb78502910 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/SimpleStreamingAsyncClientHttpRequest.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2013 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.http.client; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; + +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.StreamUtils; + +/** + * {@link org.springframework.http.client.ClientHttpRequest} implementation that uses + * standard J2SE facilities to execute streaming requests. Created via the {@link + * org.springframework.http.client.SimpleClientHttpRequestFactory}. + * + * @author Arjen Poutsma + * @see org.springframework.http.client.SimpleClientHttpRequestFactory#createRequest(java.net.URI, + * org.springframework.http.HttpMethod) + * @since 3.0 + */ +final class SimpleStreamingAsyncClientHttpRequest extends AbstractAsyncClientHttpRequest { + + private final HttpURLConnection connection; + + private final int chunkSize; + + private OutputStream body; + + private final boolean outputStreaming; + + private final AsyncTaskExecutor taskExecutor; + + SimpleStreamingAsyncClientHttpRequest(HttpURLConnection connection, int chunkSize, + boolean outputStreaming, AsyncTaskExecutor taskExecutor) { + this.connection = connection; + this.chunkSize = chunkSize; + this.outputStreaming = outputStreaming; + this.taskExecutor = taskExecutor; + } + + @Override + public HttpMethod getMethod() { + return HttpMethod.valueOf(this.connection.getRequestMethod()); + } + + @Override + public URI getURI() { + try { + return this.connection.getURL().toURI(); + } + catch (URISyntaxException ex) { + throw new IllegalStateException( + "Could not get HttpURLConnection URI: " + ex.getMessage(), ex); + } + } + + @Override + protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException { + if (this.body == null) { + if (this.outputStreaming) { + int contentLength = (int) headers.getContentLength(); + if (contentLength >= 0) { + this.connection.setFixedLengthStreamingMode(contentLength); + } + else { + this.connection.setChunkedStreamingMode(this.chunkSize); + } + } + writeHeaders(headers); + this.connection.connect(); + this.body = this.connection.getOutputStream(); + } + return StreamUtils.nonClosing(this.body); + } + + private void writeHeaders(HttpHeaders headers) { + for (Map.Entry> entry : headers.entrySet()) { + String headerName = entry.getKey(); + for (String headerValue : entry.getValue()) { + this.connection.addRequestProperty(headerName, headerValue); + } + } + } + + @Override + protected Future executeInternal(final HttpHeaders headers) + throws IOException { + return taskExecutor.submit(new Callable() { + @Override + public ClientHttpResponse call() throws Exception { + try { + if (body != null) { + body.close(); + } + else { + writeHeaders(headers); + connection.connect(); + } + } + catch (IOException ex) { + // ignore + } + return new SimpleClientHttpResponse(connection); + } + }); + + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/client/support/AsyncHttpAccessor.java b/spring-web/src/main/java/org/springframework/http/client/support/AsyncHttpAccessor.java new file mode 100644 index 00000000000..e2a95d9780d --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/client/support/AsyncHttpAccessor.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2013 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.http.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.http.HttpMethod; +import org.springframework.http.client.AsyncClientHttpRequest; +import org.springframework.http.client.AsyncClientHttpRequestFactory; +import org.springframework.util.Assert; + +/** + * Base class for {@link org.springframework.web.client.AsyncRestTemplate} + * and other HTTP accessing gateway helpers, defining common properties + * such as the {@link AsyncClientHttpRequestFactory} to operate on. + * + *

Not intended to be used directly. See + * {@link org.springframework.web.client.AsyncRestTemplate}. + * + * @author Arjen Poutsma + * @since 4.0 + * @see org.springframework.web.client.AsyncRestTemplate + */ +public class AsyncHttpAccessor { + + /** Logger available to subclasses. */ + protected final Log logger = LogFactory.getLog(getClass()); + + private AsyncClientHttpRequestFactory asyncRequestFactory; + + /** + * Set the request factory that this accessor uses for obtaining {@link + * org.springframework.http.client.ClientHttpRequest HttpRequests}. + */ + public void setAsyncRequestFactory(AsyncClientHttpRequestFactory asyncRequestFactory) { + Assert.notNull(asyncRequestFactory, "'asyncRequestFactory' must not be null"); + this.asyncRequestFactory = asyncRequestFactory; + } + + /** + * Return the request factory that this accessor uses for obtaining {@link + * org.springframework.http.client.ClientHttpRequest HttpRequests}. + */ + public AsyncClientHttpRequestFactory getAsyncRequestFactory() { + return this.asyncRequestFactory; + } + + /** + * Create a new {@link AsyncClientHttpRequest} via this template's {@link + * AsyncClientHttpRequestFactory}. + * + * @param url the URL to connect to + * @param method the HTTP method to execute (GET, POST, etc.) + * @return the created request + * @throws IOException in case of I/O errors + */ + protected AsyncClientHttpRequest createAsyncRequest(URI url, HttpMethod method) + throws IOException { + AsyncClientHttpRequest request = getAsyncRequestFactory().createAsyncRequest(url, method); + if (logger.isDebugEnabled()) { + logger.debug("Created asynchronous " + method.name() + " request for \"" + url + "\""); + } + return request; + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/client/AsyncRequestCallback.java b/spring-web/src/main/java/org/springframework/web/client/AsyncRequestCallback.java new file mode 100644 index 00000000000..4a9840ef596 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/client/AsyncRequestCallback.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2013 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; + +import org.springframework.http.client.AsyncClientHttpRequest; + +/** + * Callback interface for code that operates on an {@link AsyncClientHttpRequest}. Allows + * to manipulate the request headers, and write to the request body. + * + *

Used internally by the {@link AsyncRestTemplate}, but also useful for application code. + * + * @author Arjen Poutsma + * @see org.springframework.web.client.AsyncRestTemplate#execute + * @since 4.0 + */ +public interface AsyncRequestCallback { + + /** + * Gets called by {@link AsyncRestTemplate#execute} with an opened {@code ClientHttpRequest}. + * Does not need to care about closing the request or about handling errors: + * this will all be handled by the {@code RestTemplate}. + * @param request the active HTTP request + * @throws java.io.IOException in case of I/O errors + */ + void doWithRequest(AsyncClientHttpRequest request) throws IOException; + +} diff --git a/spring-web/src/main/java/org/springframework/web/client/AsyncRequestCallbackAdapter.java b/spring-web/src/main/java/org/springframework/web/client/AsyncRequestCallbackAdapter.java new file mode 100644 index 00000000000..fae22dfc551 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/client/AsyncRequestCallbackAdapter.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2013 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; +import java.io.OutputStream; +import java.net.URI; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.client.AsyncClientHttpRequest; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; + +/** + * Adapts a {@link RequestCallback} to the {@link AsyncRequestCallback} interface. + * + * @author Arjen Poutsma + * @since 4.0 + */ +public class AsyncRequestCallbackAdapter implements AsyncRequestCallback { + + private final RequestCallback adaptee; + + /** + * Creates a new {@code AsyncRequestCallbackAdapter} from the given + * {@link RequestCallback}. + * + * @param requestCallback the callback to base this adapter on + */ + public AsyncRequestCallbackAdapter(RequestCallback requestCallback) { + this.adaptee = requestCallback; + } + + @Override + public void doWithRequest(final AsyncClientHttpRequest request) throws IOException { + if (adaptee != null) { + adaptee.doWithRequest(new ClientHttpRequest() { + @Override + public ClientHttpResponse execute() throws IOException { + throw new UnsupportedOperationException("execute not supported"); + } + + @Override + public OutputStream getBody() throws IOException { + return request.getBody(); + } + + @Override + public HttpMethod getMethod() { + return request.getMethod(); + } + + @Override + public URI getURI() { + return request.getURI(); + } + + @Override + public HttpHeaders getHeaders() { + return request.getHeaders(); + } + + }); + } + } +} diff --git a/spring-web/src/main/java/org/springframework/web/client/AsyncRestOperations.java b/spring-web/src/main/java/org/springframework/web/client/AsyncRestOperations.java new file mode 100644 index 00000000000..c3a7861b6db --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/client/AsyncRestOperations.java @@ -0,0 +1,441 @@ +/* + * Copyright 2002-2013 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.net.URI; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Future; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; + +/** + * Interface specifying a basic set of asynchronous RESTful operations. Implemented by + * {@link AsyncRestTemplate}. Not often used directly, but a useful option to enhance + * testability, as it can easily be mocked or stubbed. + * + * @author Arjen Poutsma + * @since 4.0 + */ +public interface AsyncRestOperations { + + /** + * Expose the synchronous Spring RestTemplate to allow synchronous invocation. + */ + RestOperations getRestOperations(); + + // GET + + /** + * Asynchronously retrieve an entity by doing a GET on the specified URL. The response is + * converted and stored in an {@link ResponseEntity}. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param responseType the type of the return value + * @param uriVariables the variables to expand the template + * @return the entity wrapped in a {@link Future} + */ + Future> getForEntity(String url, Class responseType, + Object... uriVariables) throws RestClientException; + + /** + * Asynchronously retrieve a representation by doing a GET on the URI template. The + * response is converted and stored in an {@link ResponseEntity}. + *

URI Template variables are expanded using the given map. + * @param url the URL + * @param responseType the type of the return value + * @param uriVariables the map containing variables for the URI template + * @return the entity wrapped in a {@link Future} + */ + Future> getForEntity(String url, Class responseType, + Map uriVariables) throws RestClientException; + + /** + * Asynchronously retrieve a representation by doing a GET on the URL. + * The response is converted and stored in an {@link ResponseEntity}. + * @param url the URL + * @param responseType the type of the return value + * @return the entity wrapped in a {@link Future} + */ + Future> getForEntity(URI url, Class responseType) + throws RestClientException; + + // HEAD + + /** + * Asynchronously retrieve all headers of the resource specified by the URI template. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand the template + * @return all HTTP headers of that resource wrapped in a {@link Future} + */ + Future headForHeaders(String url, Object... uriVariables) + throws RestClientException; + + /** + * Asynchronously retrieve all headers of the resource specified by the URI template. + *

URI Template variables are expanded using the given map. + * @param url the URL + * @param uriVariables the map containing variables for the URI template + * @return all HTTP headers of that resource wrapped in a {@link Future} + */ + Future headForHeaders(String url, Map uriVariables) + throws RestClientException; + + /** + * Asynchronously retrieve all headers of the resource specified by the URL. + * @param url the URL + * @return all HTTP headers of that resource wrapped in a {@link Future} + */ + Future headForHeaders(URI url) throws RestClientException; + + // POST + + /** + * Create a new resource by POSTing the given object to the URI template, and + * asynchronously 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. + * @param url the URL + * @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 wrapped in a {@link Future} + * @see org.springframework.http.HttpEntity + */ + Future postForLocation(String url, HttpEntity request, Object... uriVariables) + throws RestClientException; + + /** + * Create a new resource by POSTing the given object to the URI template, and + * asynchronously 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. + * @param url the URL + * @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 wrapped in a {@link Future} + * @see org.springframework.http.HttpEntity + */ + Future postForLocation(String url, HttpEntity request, Map uriVariables) + throws RestClientException; + + /** + * Create a new resource by POSTing the given object to the URL, and asynchronously + * returns the value of the {@code Location} header. This header typically indicates + * where the new resource is stored. + * @param url the URL + * @param request the Object to be POSTed, may be {@code null} + * @return the value for the {@code Location} header wrapped in a {@link Future} + * @see org.springframework.http.HttpEntity + */ + Future postForLocation(URI url, HttpEntity request) throws RestClientException; + + /** + * Create a new resource by POSTing the given object to the URI template, + * and asynchronously returns the response as {@link ResponseEntity}. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param request the Object to be POSTed, may be {@code null} + * @param uriVariables the variables to expand the template + * @return the entity wrapped in a {@link Future} + * @see org.springframework.http.HttpEntity + */ + Future> postForEntity(String url, HttpEntity request, + Class responseType, Object... uriVariables) throws RestClientException; + + /** + * Create a new resource by POSTing the given object to the URI template, + * and asynchronously returns the response as {@link ResponseEntity}. + *

URI Template variables are expanded using the given map. + * @param url the URL + * @param request the Object to be POSTed, may be {@code null} + * @param uriVariables the variables to expand the template + * @return the entity wrapped in a {@link Future} + * @see org.springframework.http.HttpEntity + */ + Future> postForEntity(String url, HttpEntity request, + Class responseType, Map uriVariables) + throws RestClientException; + + /** + * Create a new resource by POSTing the given object to the URL, + * and asynchronously returns the response as {@link ResponseEntity}. + * @param url the URL + * @param request the Object to be POSTed, may be {@code null} + * @return the entity wrapped in a {@link Future} + * @see org.springframework.http.HttpEntity + */ + Future> postForEntity(URI url, HttpEntity request, + Class responseType) throws RestClientException; + + // PUT + + /** + * Create or update a resource by PUTting the given object to the URI. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param request the Object to be PUT, may be {@code null} + * @param uriVariables the variables to expand the template + * @see HttpEntity + */ + Future put(String url, HttpEntity request, Object... uriVariables) + throws RestClientException; + + /** + * Creates a new resource by PUTting the given object to URI template. + *

URI Template variables are expanded using the given map. + * @param url the URL + * @param request the Object to be PUT, may be {@code null} + * @param uriVariables the variables to expand the template + * @see HttpEntity + */ + Future put(String url, HttpEntity request, Map uriVariables) + throws RestClientException; + + /** + * Creates a new resource by PUTting the given object to URL. + * @param url the URL + * @param request the Object to be PUT, may be {@code null} + * @see HttpEntity + */ + Future put(URI url, HttpEntity request) throws RestClientException; + + // DELETE + + /** + * Asynchronously delete the resources at the specified URI. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + */ + Future delete(String url, Object... uriVariables) throws RestClientException; + + /** + * Asynchronously delete the resources at the specified URI. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + */ + Future delete(String url, Map uriVariables) throws RestClientException; + + /** + * Asynchronously delete the resources at the specified URI. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + */ + Future delete(URI url) throws RestClientException; + + // OPTIONS + + /** + * Asynchronously return the value of the Allow header for the given URI. + *

URI Template variables are expanded using the given URI variables, if any. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the value of the allow header wrapped in a {@link Future} + */ + Future> optionsForAllow(String url, Object... uriVariables) + throws RestClientException; + + /** + * Asynchronously return the value of the Allow header for the given URI. + *

URI Template variables are expanded using the given map. + * @param url the URL + * @param uriVariables the variables to expand in the template + * @return the value of the allow header wrapped in a {@link Future} + */ + Future> optionsForAllow(String url, Map uriVariables) + throws RestClientException; + + /** + * Asynchronously return the value of the Allow header for the given URL. + * @param url the URL + * @return the value of the allow header wrapped in a {@link Future} + */ + Future> optionsForAllow(URI url) throws RestClientException; + + // exchange + + /** + * Asynchronously execute the HTTP method to the given URI template, writing the + * given request entity to the request, and returns the response as + * {@link ResponseEntity}. + *

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 responseType the type of the return value + * @param uriVariables the variables to expand in the template + * @return the response as entity wrapped in a {@link Future} + */ + Future> exchange(String url, HttpMethod method, + HttpEntity requestEntity, Class responseType, Object... uriVariables) + throws RestClientException; + + /** + * Asynchronously execute the HTTP method to the given URI template, writing the + * given request entity to the request, and returns the response as + * {@link ResponseEntity}. + *

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 responseType the type of the return value + * @param uriVariables the variables to expand in the template + * @return the response as entity wrapped in a {@link Future} + */ + Future> exchange(String url, HttpMethod method, + HttpEntity requestEntity, Class responseType, + Map uriVariables) throws RestClientException; + + /** + * Asynchronously execute the HTTP method to the given URI template, writing the + * given request entity to the request, and 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 responseType the type of the return value + * @return the response as entity wrapped in a {@link Future} + */ + Future> exchange(URI url, HttpMethod method, + HttpEntity requestEntity, Class responseType) + throws RestClientException; + + /** + * Asynchronously execute the HTTP method to the given URI template, writing the given + * request entity to the request, and returns the response as {@link ResponseEntity}. + * The given {@link ParameterizedTypeReference} is used to pass generic type + * information: + * + *

+	 * ParameterizedTypeReference<List<MyBean>> myBean = new ParameterizedTypeReference<List<MyBean>>() {};
+	 * ResponseEntity<List<MyBean>> response = template.exchange("http://example.com",HttpMethod.GET, null, myBean);
+	 * 
+ * + * @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 responseType the type of the return value + * @param uriVariables the variables to expand in the template + * @return the response as entity wrapped in a {@link Future} + */ + Future> exchange(String url, HttpMethod method, + HttpEntity requestEntity, ParameterizedTypeReference responseType, + Object... uriVariables) throws RestClientException; + + /** + * Asynchronously execute the HTTP method to the given URI template, writing the given + * request entity to the request, and returns the response as {@link ResponseEntity}. + * The given {@link ParameterizedTypeReference} is used to pass generic type + * information: + * + *
+	 * ParameterizedTypeReference<List<MyBean>> myBean = new ParameterizedTypeReference<List<MyBean>>() {};
+	 * ResponseEntity<List<MyBean>> response = template.exchange("http://example.com",HttpMethod.GET, null, myBean);
+	 * 
+ * + * @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 responseType the type of the return value + * @param uriVariables the variables to expand in the template + * @return the response as entity wrapped in a {@link Future} + */ + Future> exchange(String url, HttpMethod method, + HttpEntity requestEntity, ParameterizedTypeReference responseType, + Map uriVariables) throws RestClientException; + + /** + * Asynchronously execute the HTTP method to the given URI template, writing the given + * request entity to the request, and returns the response as {@link ResponseEntity}. + * The given {@link ParameterizedTypeReference} is used to pass generic type + * information: + * + *
+	 * ParameterizedTypeReference<List<MyBean>> myBean = new ParameterizedTypeReference<List<MyBean>>() {};
+	 * ResponseEntity<List<MyBean>> response = template.exchange("http://example.com",HttpMethod.GET, null, myBean);
+	 * 
+ * + * @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 responseType the type of the return value + * @return the response as entity wrapped in a {@link Future} + */ + Future> exchange(URI url, HttpMethod method, + HttpEntity requestEntity, ParameterizedTypeReference responseType) + throws RestClientException; + + + // general execution + + /** + * Asynchronously execute the HTTP method to the given URI template, preparing the + * request with the {@link AsyncRequestCallback}, and reading the response with a + * {@link ResponseExtractor}. + *

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 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 ResponseExtractor} + */ + Future execute(String url, HttpMethod method, + AsyncRequestCallback requestCallback, ResponseExtractor responseExtractor, + Object... uriVariables) throws RestClientException; + + /** + * Asynchronously execute the HTTP method to the given URI template, preparing the + * request with the {@link AsyncRequestCallback}, and reading the response with a + * {@link ResponseExtractor}. + *

URI Template variables are expanded using the given URI variables map. + * @param url the URL + * @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 ResponseExtractor} + */ + Future execute(String url, HttpMethod method, + AsyncRequestCallback requestCallback, ResponseExtractor responseExtractor, + Map uriVariables) throws RestClientException; + + /** + * Asynchronously execute the HTTP method to the given URL, preparing the request + * with the {@link AsyncRequestCallback}, and reading the response with a + * {@link ResponseExtractor}. + * @param url the URL + * @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 + * @return an arbitrary object, as returned by the {@link ResponseExtractor} + */ + Future execute(URI url, HttpMethod method, + AsyncRequestCallback requestCallback, ResponseExtractor responseExtractor) + throws RestClientException; + +} 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 new file mode 100644 index 00000000000..e3a97668159 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/client/AsyncRestTemplate.java @@ -0,0 +1,715 @@ +/* + * Copyright 2002-2013 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; +import java.lang.reflect.Type; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.AsyncClientHttpRequest; +import org.springframework.http.client.AsyncClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.http.client.support.AsyncHttpAccessor; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.util.Assert; +import org.springframework.web.util.UriTemplate; + +/** + * Spring's central class for asynchronous client-side HTTP access. + * Exposes similar methods as {@link RestTemplate}, but returns {@link Future} wrappers + * as opposed to concrete results. + * + *

The {@code AsyncRestTemplate} exposes a synchronous {@link RestTemplate} via the + * {@link #getRestOperations()} method, and it shares its + * {@linkplain #setErrorHandler(ResponseErrorHandler) error handler} and + * {@linkplain #setMessageConverters(List) message converters} with this + * {@code RestTemplate}. + * + *

For more information, please refer to the {@link RestTemplate} API documentation

+ * + * @author Arjen Poutsma + * @since 4.0 + * @see RestTemplate + */ +public class AsyncRestTemplate extends AsyncHttpAccessor implements AsyncRestOperations { + + private final RestTemplate syncTemplate; + + + /** + * Create a new instance of the {@link AsyncRestTemplate} using default settings. + *

This constructor uses a {@link SimpleClientHttpRequestFactory} in combination + * with a {@link SimpleAsyncTaskExecutor} for asynchronous execution. + */ + public AsyncRestTemplate() { + this(new SimpleAsyncTaskExecutor()); + } + + /** + * Create a new instance of the {@link AsyncRestTemplate} using the given + * {@link AsyncTaskExecutor}. + *

This constructor uses a {@link SimpleClientHttpRequestFactory} in combination + * with the given {@code AsyncTaskExecutor} for asynchronous execution. + */ + public AsyncRestTemplate(AsyncTaskExecutor taskExecutor) { + Assert.notNull(taskExecutor, "'taskExecutor' must not be null"); + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setTaskExecutor(taskExecutor); + this.syncTemplate = new RestTemplate(requestFactory); + setAsyncRequestFactory(requestFactory); + } + + /** + * Create a new instance of the {@link AsyncRestTemplate} using the given + * {@link AsyncClientHttpRequestFactory}. + *

This constructor will cast the given asynchronous + * {@code AsyncClientHttpRequestFactory} to a {@link ClientHttpRequestFactory}. Since + * all implementations of {@code ClientHttpRequestFactory} provided in Spring also + * implement {@code AsyncClientHttpRequestFactory}, this should not result in a + * {@code ClassCastException}. + */ + public AsyncRestTemplate(AsyncClientHttpRequestFactory asyncRequestFactory) { + this(asyncRequestFactory, (ClientHttpRequestFactory) asyncRequestFactory); + } + + /** + * Creates a new instance of the {@link AsyncRestTemplate} using the given + * asynchronous and synchronous request factories. + * @param asyncRequestFactory the asynchronous request factory + * @param syncRequestFactory the synchronous request factory + */ + public AsyncRestTemplate(AsyncClientHttpRequestFactory asyncRequestFactory, + ClientHttpRequestFactory syncRequestFactory) { + this(asyncRequestFactory, new RestTemplate(syncRequestFactory)); + } + + /** + * Create a new instance of the {@link AsyncRestTemplate} using the given + * {@link AsyncClientHttpRequestFactory} and synchronous {@link RestTemplate}. + * @param requestFactory the asynchronous request factory to use + * @param restTemplate the synchronous template to use + */ + public AsyncRestTemplate(AsyncClientHttpRequestFactory requestFactory, + RestTemplate restTemplate) { + Assert.notNull(restTemplate, "'restTemplate' must not be null"); + this.syncTemplate = restTemplate; + setAsyncRequestFactory(requestFactory); + } + + /** + * Set the error handler. + *

By default, AsyncRestTemplate uses a + * {@link org.springframework.web.client.DefaultResponseErrorHandler}. + */ + public void setErrorHandler(ResponseErrorHandler errorHandler) { + this.syncTemplate.setErrorHandler(errorHandler); + } + + /** Return the error handler. */ + public ResponseErrorHandler getErrorHandler() { + return this.syncTemplate.getErrorHandler(); + } + + @Override + public RestOperations getRestOperations() { + return this.syncTemplate; + } + + /** + * Set the message body converters to use. + *

These converters are used to convert from and to HTTP requests and responses. + */ + public void setMessageConverters(List> messageConverters) { + this.syncTemplate.setMessageConverters(messageConverters); + } + + /** + * Return the message body converters. + */ + public List> getMessageConverters() { + return syncTemplate.getMessageConverters(); + } + + // GET + + @Override + public Future> getForEntity(String url, Class responseType, + Object... uriVariables) throws RestClientException { + AsyncRequestCallback requestCallback = acceptHeaderRequestCallback(responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); + return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables); + } + + @Override + public Future> getForEntity(String url, Class responseType, + Map urlVariables) throws RestClientException { + AsyncRequestCallback requestCallback = acceptHeaderRequestCallback(responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); + return execute(url, HttpMethod.GET, requestCallback, responseExtractor, urlVariables); + } + + @Override + public Future> getForEntity(URI url, Class responseType) + throws RestClientException { + AsyncRequestCallback requestCallback = acceptHeaderRequestCallback(responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); + return execute(url, HttpMethod.GET, requestCallback, responseExtractor); + } + + // HEAD + + @Override + public Future headForHeaders(String url, Object... uriVariables) + throws RestClientException { + ResponseExtractor headersExtractor = headersExtractor(); + return execute(url, HttpMethod.HEAD, null, headersExtractor, uriVariables); + } + + @Override + public Future headForHeaders(String url, Map uriVariables) + throws RestClientException { + ResponseExtractor headersExtractor = headersExtractor(); + return execute(url, HttpMethod.HEAD, null, headersExtractor, uriVariables); + } + + @Override + public Future headForHeaders(URI url) throws RestClientException { + ResponseExtractor headersExtractor = headersExtractor(); + return execute(url, HttpMethod.HEAD, null, headersExtractor); + } + + // POST + + @Override + public Future postForLocation(String url, HttpEntity request, + Object... uriVariables) throws RestClientException { + AsyncRequestCallback requestCallback = httpEntityCallback(request); + ResponseExtractor headersExtractor = headersExtractor(); + Future headersFuture = + execute(url, HttpMethod.POST, requestCallback, headersExtractor, + uriVariables); + return extractLocationHeader(headersFuture); + } + + @Override + public Future postForLocation(String url, HttpEntity request, + Map uriVariables) throws RestClientException { + AsyncRequestCallback requestCallback = httpEntityCallback(request); + ResponseExtractor headersExtractor = headersExtractor(); + Future headersFuture = + execute(url, HttpMethod.POST, requestCallback, headersExtractor, + uriVariables); + return extractLocationHeader(headersFuture); + } + + @Override + public Future postForLocation(URI url, HttpEntity request) + throws RestClientException { + AsyncRequestCallback requestCallback = httpEntityCallback(request); + ResponseExtractor headersExtractor = headersExtractor(); + Future headersFuture = + execute(url, HttpMethod.POST, requestCallback, headersExtractor); + return extractLocationHeader(headersFuture); + } + + private static Future extractLocationHeader(final Future headersFuture) { + return new Future() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return headersFuture.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return headersFuture.isCancelled(); + } + + @Override + public boolean isDone() { + return headersFuture.isDone(); + } + + @Override + public URI get() throws InterruptedException, ExecutionException { + HttpHeaders headers = headersFuture.get(); + return headers.getLocation(); + } + + @Override + public URI get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + HttpHeaders headers = headersFuture.get(timeout, unit); + return headers.getLocation(); + } + }; + } + + @Override + public Future> postForEntity(String url, HttpEntity request, + Class responseType, Object... uriVariables) throws RestClientException { + AsyncRequestCallback requestCallback = httpEntityCallback(request, responseType); + ResponseExtractor> responseExtractor = + responseEntityExtractor(responseType); + return execute(url, HttpMethod.POST, requestCallback, responseExtractor, + uriVariables); + } + + @Override + public Future> postForEntity(String url, HttpEntity request, + Class responseType, Map uriVariables) + throws RestClientException { + AsyncRequestCallback requestCallback = httpEntityCallback(request, responseType); + ResponseExtractor> responseExtractor = + responseEntityExtractor(responseType); + return execute(url, HttpMethod.POST, requestCallback, responseExtractor, + uriVariables); + } + + @Override + public Future> postForEntity(URI url, HttpEntity request, + Class responseType) throws RestClientException { + AsyncRequestCallback requestCallback = httpEntityCallback(request, responseType); + ResponseExtractor> responseExtractor = + responseEntityExtractor(responseType); + return execute(url, HttpMethod.POST, requestCallback, responseExtractor); + } + + // PUT + + @Override + public Future put(String url, HttpEntity request, Object... uriVariables) + throws RestClientException { + AsyncRequestCallback requestCallback = httpEntityCallback(request); + return execute(url, HttpMethod.PUT, requestCallback, null, uriVariables); + } + + @Override + public Future put(String url, HttpEntity request, + Map uriVariables) throws RestClientException { + AsyncRequestCallback requestCallback = httpEntityCallback(request); + return execute(url, HttpMethod.PUT, requestCallback, null, uriVariables); + } + + @Override + public Future put(URI url, HttpEntity request) throws RestClientException { + AsyncRequestCallback requestCallback = httpEntityCallback(request); + return execute(url, HttpMethod.PUT, requestCallback, null); + } + + // DELETE + + @Override + public Future delete(String url, Object... urlVariables) + throws RestClientException { + return execute(url, HttpMethod.DELETE, null, null, urlVariables); + } + + @Override + public Future delete(String url, Map urlVariables) + throws RestClientException { + return execute(url, HttpMethod.DELETE, null, null, urlVariables); + } + + @Override + public Future delete(URI url) throws RestClientException { + return execute(url, HttpMethod.DELETE, null, null); + } + + // OPTIONS + + @Override + public Future> optionsForAllow(String url, Object... uriVariables) + throws RestClientException { + ResponseExtractor headersExtractor = headersExtractor(); + Future + headersFuture = execute(url, HttpMethod.OPTIONS, null, headersExtractor, + uriVariables); + return extractAllowHeader(headersFuture); + } + + @Override + public Future> optionsForAllow(String url, + Map uriVariables) throws RestClientException { + ResponseExtractor headersExtractor = headersExtractor(); + Future + headersFuture = execute(url, HttpMethod.OPTIONS, null, headersExtractor, + uriVariables); + return extractAllowHeader(headersFuture); + } + + @Override + public Future> optionsForAllow(URI url) throws RestClientException { + ResponseExtractor headersExtractor = headersExtractor(); + Future + headersFuture = execute(url, HttpMethod.OPTIONS, null, headersExtractor); + return extractAllowHeader(headersFuture); + } + + private static Future> extractAllowHeader(final Future headersFuture) { + return new Future>() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return headersFuture.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return headersFuture.isCancelled(); + } + + @Override + public boolean isDone() { + return headersFuture.isDone(); + } + + @Override + public Set get() throws InterruptedException, ExecutionException { + HttpHeaders headers = headersFuture.get(); + return headers.getAllow(); + } + + @Override + public Set get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + HttpHeaders headers = headersFuture.get(timeout, unit); + return headers.getAllow(); + } + }; + } + + // exchange + + @Override + public Future> exchange(String url, HttpMethod method, + HttpEntity requestEntity, Class responseType, Object... uriVariables) + throws RestClientException { + AsyncRequestCallback requestCallback = + httpEntityCallback(requestEntity, responseType); + ResponseExtractor> responseExtractor = + responseEntityExtractor(responseType); + return execute(url, method, requestCallback, responseExtractor, uriVariables); + } + + @Override + public Future> exchange(String url, HttpMethod method, + HttpEntity requestEntity, Class responseType, + Map uriVariables) throws RestClientException { + AsyncRequestCallback requestCallback = + httpEntityCallback(requestEntity, responseType); + ResponseExtractor> responseExtractor = + responseEntityExtractor(responseType); + return execute(url, method, requestCallback, responseExtractor, uriVariables); + } + + @Override + public Future> exchange(URI url, HttpMethod method, + HttpEntity requestEntity, Class responseType) + throws RestClientException { + AsyncRequestCallback requestCallback = + httpEntityCallback(requestEntity, responseType); + ResponseExtractor> responseExtractor = + responseEntityExtractor(responseType); + return execute(url, method, requestCallback, responseExtractor); + } + + @Override + public Future> exchange(String url, HttpMethod method, + HttpEntity requestEntity, ParameterizedTypeReference responseType, + Object... uriVariables) throws RestClientException { + Type type = responseType.getType(); + AsyncRequestCallback requestCallback = httpEntityCallback(requestEntity, type); + ResponseExtractor> responseExtractor = + responseEntityExtractor(type); + return execute(url, method, requestCallback, responseExtractor, uriVariables); + } + + @Override + public Future> exchange(String url, HttpMethod method, + HttpEntity requestEntity, ParameterizedTypeReference responseType, + Map uriVariables) throws RestClientException { + Type type = responseType.getType(); + AsyncRequestCallback requestCallback = httpEntityCallback(requestEntity, type); + ResponseExtractor> responseExtractor = + responseEntityExtractor(type); + return execute(url, method, requestCallback, responseExtractor, uriVariables); + } + + @Override + public Future> exchange(URI url, HttpMethod method, + HttpEntity requestEntity, ParameterizedTypeReference responseType) + throws RestClientException { + Type type = responseType.getType(); + AsyncRequestCallback requestCallback = httpEntityCallback(requestEntity, type); + ResponseExtractor> responseExtractor = + responseEntityExtractor(type); + return execute(url, method, requestCallback, responseExtractor); + } + + // general execution + + @Override + public Future execute(String url, HttpMethod method, + AsyncRequestCallback requestCallback, ResponseExtractor responseExtractor, + Object... urlVariables) throws RestClientException { + + URI expanded = new UriTemplate(url).expand(urlVariables); + return doExecute(expanded, method, requestCallback, responseExtractor); + } + + @Override + public Future execute(String url, HttpMethod method, + AsyncRequestCallback requestCallback, ResponseExtractor responseExtractor, + Map urlVariables) throws RestClientException { + + URI expanded = new UriTemplate(url).expand(urlVariables); + return doExecute(expanded, method, requestCallback, responseExtractor); + } + + @Override + public Future execute(URI url, HttpMethod method, AsyncRequestCallback requestCallback, + ResponseExtractor responseExtractor) throws RestClientException { + + return doExecute(url, method, requestCallback, responseExtractor); + } + + /** + * Execute the given method on the provided URI. The + * {@link org.springframework.http.client.ClientHttpRequest} + * is processed using the {@link RequestCallback}; the response with + * the {@link ResponseExtractor}. + * + * @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 {@code null}) + * @param responseExtractor object that extracts the return value from the response (can + * be {@code null}) + * @return an arbitrary object, as returned by the {@link ResponseExtractor} + */ + @SuppressWarnings("unchecked") + protected Future doExecute(URI url, HttpMethod method, + AsyncRequestCallback requestCallback, ResponseExtractor responseExtractor) + throws RestClientException { + + Assert.notNull(url, "'url' must not be null"); + Assert.notNull(method, "'method' must not be null"); + try { + AsyncClientHttpRequest request = createAsyncRequest(url, method); + if (requestCallback != null) { + requestCallback.doWithRequest(request); + } + Future responseFuture = request.executeAsync(); + if (responseExtractor != null) { + return new ResponseExtractorFuture(method, url, responseFuture, responseExtractor); + } + else { + return (Future) new VoidResponseFuture(method, url, responseFuture); + } + } + catch (IOException ex) { + throw new ResourceAccessException("I/O error on " + method.name() + + " request for \"" + url + "\":" + ex.getMessage(), ex); + } + } + + private void logResponseStatus(HttpMethod method, URI url, + ClientHttpResponse response) { + if (logger.isDebugEnabled()) { + try { + logger.debug("Async " + method.name() + " request for \"" + url + + "\" resulted in " + response.getStatusCode() + " (" + + response.getStatusText() + ")"); + } + catch (IOException ex) { + // ignore + } + } + } + + private void handleResponseError(HttpMethod method, URI url, + ClientHttpResponse response) throws IOException { + if (logger.isWarnEnabled()) { + try { + logger.warn("Async " + method.name() + " request for \"" + url + + "\" resulted in " + response.getStatusCode() + " (" + + response.getStatusText() + "); invoking error handler"); + } + catch (IOException ex) { + // ignore + } + } + getErrorHandler().handleError(response); + } + + /** + * Returns a request callback implementation that prepares the request {@code Accept} + * headers based on the given response type and configured {@linkplain + * #getMessageConverters() message converters}. + */ + protected AsyncRequestCallbackAdapter acceptHeaderRequestCallback( + Class responseType) { + return new AsyncRequestCallbackAdapter( + this.syncTemplate.acceptHeaderRequestCallback(responseType)); + } + + /** + * Returns a request callback implementation that writes the given object to the + * request stream. + */ + protected AsyncRequestCallback httpEntityCallback(HttpEntity requestBody) { + return new AsyncRequestCallbackAdapter( + this.syncTemplate.httpEntityCallback(requestBody)); + } + + /** + * Returns a request callback implementation that writes the given object to the + * request stream. + */ + protected AsyncRequestCallback httpEntityCallback(HttpEntity request, + Type responseType) { + return new AsyncRequestCallbackAdapter( + this.syncTemplate.httpEntityCallback(request, responseType)); + } + + /** + * Returns a response extractor for {@link ResponseEntity}. + */ + protected ResponseExtractor> responseEntityExtractor( + Type responseType) { + return this.syncTemplate.responseEntityExtractor(responseType); + } + + /** + * Returns a response extractor for {@link HttpHeaders}. + */ + protected ResponseExtractor headersExtractor() { + return this.syncTemplate.headersExtractor(); + } + + private abstract class ResponseFuture implements Future { + + private final HttpMethod method; + + private final URI url; + + private final Future responseFuture; + + protected ResponseFuture(HttpMethod method, URI url, Future responseFuture) { + this.method = method; + this.url = url; + this.responseFuture = responseFuture; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return this.responseFuture.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() { + return this.responseFuture.isCancelled(); + } + + @Override + public boolean isDone() { + return this.responseFuture.isDone(); + } + + @Override + public T get() throws InterruptedException, ExecutionException { + return getInternal(this.responseFuture.get()); + } + + @Override + public T get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return getInternal(this.responseFuture.get(timeout, unit)); + } + + private T getInternal(ClientHttpResponse response) throws ExecutionException { + try { + if (!getErrorHandler().hasError(response)) { + logResponseStatus(this.method, this.url, response); + } + else { + handleResponseError(this.method, this.url, response); + } + return extractData(response); + } + catch (IOException ex) { + throw new ExecutionException(ex); + } + finally { + if (response != null) { + response.close(); + } + } + } + + protected abstract T extractData(ClientHttpResponse response) + throws IOException; + + } + + + private class ResponseExtractorFuture extends ResponseFuture { + + private final ResponseExtractor responseExtractor; + + private ResponseExtractorFuture(HttpMethod method, URI url, + Future responseFuture, + ResponseExtractor responseExtractor) { + super(method, url, responseFuture); + this.responseExtractor = responseExtractor; + } + + @Override + protected T extractData(ClientHttpResponse response) throws IOException { + return responseExtractor.extractData(response); + } + } + + private class VoidResponseFuture extends ResponseFuture { + + private VoidResponseFuture(HttpMethod method, URI url, + Future responseFuture) { + super(method, url, responseFuture); + } + + @Override + protected Void extractData(ClientHttpResponse response) throws IOException { + return null; + } + } + + +} 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 62ea452dd37..1f758ff2125 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 @@ -35,6 +35,7 @@ import org.springframework.http.ResponseEntity; * @author Juergen Hoeller * @since 3.0 * @see RestTemplate + * @see AsyncRestOperations */ public interface RestOperations { 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 dcb7ae9b1cb..767e4fe20b9 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 @@ -51,7 +51,7 @@ import org.springframework.util.ClassUtils; import org.springframework.web.util.UriTemplate; /** - * Spring's central class for client-side HTTP access. + * Spring's central class for synchronous 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. @@ -119,6 +119,7 @@ import org.springframework.web.util.UriTemplate; * @see RequestCallback * @see ResponseExtractor * @see ResponseErrorHandler + * @see AsyncRestTemplate */ public class RestTemplate extends InterceptingHttpAccessor implements RestOperations { @@ -217,7 +218,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat @Override public T getForObject(String url, Class responseType, Object... urlVariables) throws RestClientException { - AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(responseType); + RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); HttpMessageConverterExtractor responseExtractor = new HttpMessageConverterExtractor(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.GET, requestCallback, responseExtractor, urlVariables); @@ -225,7 +226,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat @Override public T getForObject(String url, Class responseType, Map urlVariables) throws RestClientException { - AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(responseType); + RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); HttpMessageConverterExtractor responseExtractor = new HttpMessageConverterExtractor(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.GET, requestCallback, responseExtractor, urlVariables); @@ -233,7 +234,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat @Override public T getForObject(URI url, Class responseType) throws RestClientException { - AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(responseType); + RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); HttpMessageConverterExtractor responseExtractor = new HttpMessageConverterExtractor(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.GET, requestCallback, responseExtractor); @@ -242,26 +243,23 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat @Override public ResponseEntity getForEntity(String url, Class responseType, Object... urlVariables) throws RestClientException { - AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(responseType); - ResponseEntityResponseExtractor responseExtractor = - new ResponseEntityResponseExtractor(responseType); + RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); return execute(url, HttpMethod.GET, requestCallback, responseExtractor, urlVariables); } @Override public ResponseEntity getForEntity(String url, Class responseType, Map urlVariables) throws RestClientException { - AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(responseType); - ResponseEntityResponseExtractor responseExtractor = - new ResponseEntityResponseExtractor(responseType); + RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); return execute(url, HttpMethod.GET, requestCallback, responseExtractor, urlVariables); } @Override public ResponseEntity getForEntity(URI url, Class responseType) throws RestClientException { - AcceptHeaderRequestCallback requestCallback = new AcceptHeaderRequestCallback(responseType); - ResponseEntityResponseExtractor responseExtractor = - new ResponseEntityResponseExtractor(responseType); + RequestCallback requestCallback = acceptHeaderRequestCallback(responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); return execute(url, HttpMethod.GET, requestCallback, responseExtractor); } @@ -269,24 +267,27 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat @Override public HttpHeaders headForHeaders(String url, Object... urlVariables) throws RestClientException { - return execute(url, HttpMethod.HEAD, null, this.headersExtractor, urlVariables); + ResponseExtractor headersExtractor = headersExtractor(); + return execute(url, HttpMethod.HEAD, null, headersExtractor, urlVariables); } @Override public HttpHeaders headForHeaders(String url, Map urlVariables) throws RestClientException { - return execute(url, HttpMethod.HEAD, null, this.headersExtractor, urlVariables); + ResponseExtractor headersExtractor = headersExtractor(); + return execute(url, HttpMethod.HEAD, null, headersExtractor, urlVariables); } @Override public HttpHeaders headForHeaders(URI url) throws RestClientException { - return execute(url, HttpMethod.HEAD, null, this.headersExtractor); + ResponseExtractor headersExtractor = headersExtractor(); + return execute(url, HttpMethod.HEAD, null, headersExtractor); } // POST @Override public URI postForLocation(String url, Object request, Object... urlVariables) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(request); + RequestCallback requestCallback = httpEntityCallback(request); HttpHeaders headers = execute(url, HttpMethod.POST, requestCallback, this.headersExtractor, urlVariables); return headers.getLocation(); } @@ -294,14 +295,14 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat @Override public URI postForLocation(String url, Object request, Map urlVariables) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(request); + RequestCallback requestCallback = httpEntityCallback(request); HttpHeaders headers = execute(url, HttpMethod.POST, requestCallback, this.headersExtractor, urlVariables); return headers.getLocation(); } @Override public URI postForLocation(URI url, Object request) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(request); + RequestCallback requestCallback = httpEntityCallback(request); HttpHeaders headers = execute(url, HttpMethod.POST, requestCallback, this.headersExtractor); return headers.getLocation(); } @@ -309,7 +310,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat @Override public T postForObject(String url, Object request, Class responseType, Object... uriVariables) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(request, responseType); + RequestCallback requestCallback = httpEntityCallback(request, responseType); HttpMessageConverterExtractor responseExtractor = new HttpMessageConverterExtractor(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables); @@ -318,7 +319,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat @Override public T postForObject(String url, Object request, Class responseType, Map uriVariables) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(request, responseType); + RequestCallback requestCallback = httpEntityCallback(request, responseType); HttpMessageConverterExtractor responseExtractor = new HttpMessageConverterExtractor(responseType, getMessageConverters(), logger); return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables); @@ -326,7 +327,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat @Override public T postForObject(URI url, Object request, Class responseType) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(request, responseType); + RequestCallback requestCallback = httpEntityCallback(request, responseType); HttpMessageConverterExtractor responseExtractor = new HttpMessageConverterExtractor(responseType, getMessageConverters()); return execute(url, HttpMethod.POST, requestCallback, responseExtractor); @@ -336,9 +337,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat public ResponseEntity postForEntity(String url, Object request, Class responseType, Object... uriVariables) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(request, responseType); - ResponseEntityResponseExtractor responseExtractor = - new ResponseEntityResponseExtractor(responseType); + RequestCallback requestCallback = httpEntityCallback(request, responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables); } @@ -346,17 +346,15 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat public ResponseEntity postForEntity(String url, Object request, Class responseType, Map uriVariables) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(request, responseType); - ResponseEntityResponseExtractor responseExtractor = - new ResponseEntityResponseExtractor(responseType); + RequestCallback requestCallback = httpEntityCallback(request, responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables); } @Override public ResponseEntity postForEntity(URI url, Object request, Class responseType) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(request, responseType); - ResponseEntityResponseExtractor responseExtractor = - new ResponseEntityResponseExtractor(responseType); + RequestCallback requestCallback = httpEntityCallback(request, responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); return execute(url, HttpMethod.POST, requestCallback, responseExtractor); } @@ -364,19 +362,19 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat @Override public void put(String url, Object request, Object... urlVariables) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(request); + RequestCallback requestCallback = httpEntityCallback(request); execute(url, HttpMethod.PUT, requestCallback, null, urlVariables); } @Override public void put(String url, Object request, Map urlVariables) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(request); + RequestCallback requestCallback = httpEntityCallback(request); execute(url, HttpMethod.PUT, requestCallback, null, urlVariables); } @Override public void put(URI url, Object request) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(request); + RequestCallback requestCallback = httpEntityCallback(request); execute(url, HttpMethod.PUT, requestCallback, null); } @@ -401,19 +399,22 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat @Override public Set optionsForAllow(String url, Object... urlVariables) throws RestClientException { - HttpHeaders headers = execute(url, HttpMethod.OPTIONS, null, this.headersExtractor, urlVariables); + ResponseExtractor headersExtractor = headersExtractor(); + HttpHeaders headers = execute(url, HttpMethod.OPTIONS, null, headersExtractor, urlVariables); return headers.getAllow(); } @Override public Set optionsForAllow(String url, Map urlVariables) throws RestClientException { - HttpHeaders headers = execute(url, HttpMethod.OPTIONS, null, this.headersExtractor, urlVariables); + ResponseExtractor headersExtractor = headersExtractor(); + HttpHeaders headers = execute(url, HttpMethod.OPTIONS, null, headersExtractor, urlVariables); return headers.getAllow(); } @Override public Set optionsForAllow(URI url) throws RestClientException { - HttpHeaders headers = execute(url, HttpMethod.OPTIONS, null, this.headersExtractor); + ResponseExtractor headersExtractor = headersExtractor(); + HttpHeaders headers = execute(url, HttpMethod.OPTIONS, null, headersExtractor); return headers.getAllow(); } @@ -423,8 +424,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat public ResponseEntity exchange(String url, HttpMethod method, HttpEntity requestEntity, Class responseType, Object... uriVariables) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType); - ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(responseType); + RequestCallback requestCallback = httpEntityCallback(requestEntity, responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); return execute(url, method, requestCallback, responseExtractor, uriVariables); } @@ -432,8 +433,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat public ResponseEntity exchange(String url, HttpMethod method, HttpEntity requestEntity, Class responseType, Map uriVariables) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType); - ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(responseType); + RequestCallback requestCallback = httpEntityCallback(requestEntity, responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); return execute(url, method, requestCallback, responseExtractor, uriVariables); } @@ -441,8 +442,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat public ResponseEntity exchange(URI url, HttpMethod method, HttpEntity requestEntity, Class responseType) throws RestClientException { - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType); - ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(responseType); + RequestCallback requestCallback = httpEntityCallback(requestEntity, responseType); + ResponseExtractor> responseExtractor = responseEntityExtractor(responseType); return execute(url, method, requestCallback, responseExtractor); } @@ -451,8 +452,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat ParameterizedTypeReference responseType, Object... uriVariables) throws RestClientException { Type type = responseType.getType(); - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type); - ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(type); + RequestCallback requestCallback = httpEntityCallback(requestEntity, type); + ResponseExtractor> responseExtractor = responseEntityExtractor(type); return execute(url, method, requestCallback, responseExtractor, uriVariables); } @@ -461,8 +462,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat ParameterizedTypeReference responseType, Map uriVariables) throws RestClientException { Type type = responseType.getType(); - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type); - ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(type); + RequestCallback requestCallback = httpEntityCallback(requestEntity, type); + ResponseExtractor> responseExtractor = responseEntityExtractor(type); return execute(url, method, requestCallback, responseExtractor, uriVariables); } @@ -471,8 +472,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat ParameterizedTypeReference responseType) throws RestClientException { Type type = responseType.getType(); - HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type); - ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(type); + RequestCallback requestCallback = httpEntityCallback(requestEntity, type); + ResponseExtractor> responseExtractor = responseEntityExtractor(type); return execute(url, method, requestCallback, responseExtractor); } @@ -549,9 +550,9 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat private void logResponseStatus(HttpMethod method, URI url, ClientHttpResponse response) { if (logger.isDebugEnabled()) { try { - logger.debug( - method.name() + " request for \"" + url + "\" resulted in " + response.getStatusCode() + " (" + - response.getStatusText() + ")"); + logger.debug(method.name() + " request for \"" + url + "\" resulted in " + + response.getStatusCode() + " (" + + response.getStatusText() + ")"); } catch (IOException e) { // ignore @@ -573,6 +574,46 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat getErrorHandler().handleError(response); } + /** + * Returns a request callback implementation that prepares the request {@code Accept} + * headers based on the given response type and configured + * {@linkplain #getMessageConverters() message converters}. + */ + protected RequestCallback acceptHeaderRequestCallback(Class responseType) { + return new AcceptHeaderRequestCallback(responseType); + } + + /** + * Returns a request callback implementation that writes the given object to the + * request stream. + */ + protected RequestCallback httpEntityCallback(Object requestBody) { + return new HttpEntityRequestCallback(requestBody); + } + + /** + * Returns a request callback implementation that writes the given object to the + * request stream. + */ + protected RequestCallback httpEntityCallback(Object requestBody, + Type responseType) { + return new HttpEntityRequestCallback(requestBody, responseType); + } + + /** + * Returns a response extractor for {@link ResponseEntity}. + */ + protected ResponseExtractor> responseEntityExtractor(Type responseType) { + return new ResponseEntityResponseExtractor(responseType); + } + + /** + * Returns a response extractor for {@link HttpHeaders}. + */ + protected ResponseExtractor headersExtractor() { + return this.headersExtractor; + } + /** * Request callback implementation that prepares the request's accept headers. diff --git a/spring-web/src/test/java/org/springframework/http/client/AbstractAsyncHttpRequestFactoryTestCase.java b/spring-web/src/test/java/org/springframework/http/client/AbstractAsyncHttpRequestFactoryTestCase.java new file mode 100644 index 00000000000..ba99d0c678a --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/client/AbstractAsyncHttpRequestFactoryTestCase.java @@ -0,0 +1,183 @@ +/* + * Copyright 2002-2013 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.http.client; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.util.Arrays; +import java.util.Locale; +import java.util.concurrent.Future; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.StreamingHttpOutputMessage; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; + +public abstract class AbstractAsyncHttpRequestFactoryTestCase extends + AbstractJettyServerTestCase { + + protected AsyncClientHttpRequestFactory factory; + + @Before + public final void createFactory() throws Exception { + factory = createRequestFactory(); + if (factory instanceof InitializingBean) { + ((InitializingBean) factory).afterPropertiesSet(); + } + } + + protected abstract AsyncClientHttpRequestFactory createRequestFactory(); + + @Test + public void status() throws Exception { + URI uri = new URI(baseUrl + "/status/notfound"); + AsyncClientHttpRequest request = factory.createAsyncRequest(uri, HttpMethod.GET); + assertEquals("Invalid HTTP method", HttpMethod.GET, request.getMethod()); + assertEquals("Invalid HTTP URI", uri, request.getURI()); + Future futureResponse = request.executeAsync(); + ClientHttpResponse response = futureResponse.get(); + assertEquals("Invalid status code", HttpStatus.NOT_FOUND, + response.getStatusCode()); + } + + @Test + public void echo() throws Exception { + AsyncClientHttpRequest + request = factory.createAsyncRequest(new URI(baseUrl + "/echo"), + HttpMethod.PUT); + assertEquals("Invalid HTTP method", HttpMethod.PUT, request.getMethod()); + String headerName = "MyHeader"; + String headerValue1 = "value1"; + request.getHeaders().add(headerName, headerValue1); + String headerValue2 = "value2"; + request.getHeaders().add(headerName, headerValue2); + final byte[] body = "Hello World".getBytes("UTF-8"); + request.getHeaders().setContentLength(body.length); + if (request instanceof StreamingHttpOutputMessage) { + StreamingHttpOutputMessage streamingRequest = + (StreamingHttpOutputMessage) request; + streamingRequest.setBody(new StreamingHttpOutputMessage.Body() { + @Override + public void writeTo(OutputStream outputStream) throws IOException { + StreamUtils.copy(body, outputStream); + } + }); + } + else { + StreamUtils.copy(body, request.getBody()); + } + Future futureResponse = request.executeAsync(); + ClientHttpResponse response = futureResponse.get(); + try { + assertEquals("Invalid status code", HttpStatus.OK, response.getStatusCode()); + assertTrue("Header not found", response.getHeaders().containsKey(headerName)); + assertEquals("Header value not found", Arrays.asList(headerValue1, headerValue2), + response.getHeaders().get(headerName)); + byte[] result = FileCopyUtils.copyToByteArray(response.getBody()); + assertTrue("Invalid body", Arrays.equals(body, result)); + } + finally { + response.close(); + } + } + + @Test(expected = IllegalStateException.class) + public void multipleWrites() throws Exception { + AsyncClientHttpRequest + request = factory.createAsyncRequest(new URI(baseUrl + "/echo"), + HttpMethod.POST); + final byte[] body = "Hello World".getBytes("UTF-8"); + if (request instanceof StreamingHttpOutputMessage) { + StreamingHttpOutputMessage streamingRequest = + (StreamingHttpOutputMessage) request; + streamingRequest.setBody(new StreamingHttpOutputMessage.Body() { + @Override + public void writeTo(OutputStream outputStream) throws IOException { + StreamUtils.copy(body, outputStream); + } + }); + } + else { + StreamUtils.copy(body, request.getBody()); + } + + Future futureResponse = request.executeAsync(); + ClientHttpResponse response = futureResponse.get(); + try { + FileCopyUtils.copy(body, request.getBody()); + } + finally { + response.close(); + } + } + + @Test(expected = UnsupportedOperationException.class) + public void headersAfterExecute() throws Exception { + AsyncClientHttpRequest + request = factory.createAsyncRequest(new URI(baseUrl + "/echo"), + HttpMethod.POST); + request.getHeaders().add("MyHeader", "value"); + byte[] body = "Hello World".getBytes("UTF-8"); + FileCopyUtils.copy(body, request.getBody()); + + Future futureResponse = request.executeAsync(); + ClientHttpResponse response = futureResponse.get(); + try { + request.getHeaders().add("MyHeader", "value"); + } + finally { + response.close(); + } + } + + @Test + public void httpMethods() throws Exception { + assertHttpMethod("get", HttpMethod.GET); + assertHttpMethod("head", HttpMethod.HEAD); + assertHttpMethod("post", HttpMethod.POST); + assertHttpMethod("put", HttpMethod.PUT); + assertHttpMethod("options", HttpMethod.OPTIONS); + assertHttpMethod("delete", HttpMethod.DELETE); + } + + protected void assertHttpMethod(String path, HttpMethod method) throws Exception { + ClientHttpResponse response = null; + try { + AsyncClientHttpRequest request = factory.createAsyncRequest( + new URI(baseUrl + "/methods/" + path), method); + + Future futureResponse = request.executeAsync(); + response = futureResponse.get(); + assertEquals("Invalid response status", HttpStatus.OK, response.getStatusCode()); + assertEquals("Invalid method", path.toUpperCase(Locale.ENGLISH), request.getMethod().name()); + } + finally { + if (response != null) { + response.close(); + } + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java b/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java index 48bd18c0a1c..95b0f7440fa 100644 --- a/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java +++ b/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java @@ -17,70 +17,27 @@ package org.springframework.http.client; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.util.Arrays; -import java.util.Enumeration; import java.util.Locale; -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.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.junit.AfterClass; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.StreamingHttpOutputMessage; import org.springframework.util.FileCopyUtils; -import org.springframework.util.SocketUtils; import org.springframework.util.StreamUtils; import static org.junit.Assert.*; -public abstract class AbstractHttpRequestFactoryTestCase { +/** @author Arjen Poutsma */ +public abstract class AbstractHttpRequestFactoryTestCase extends + AbstractJettyServerTestCase { protected ClientHttpRequestFactory factory; - protected static String baseUrl; - - private static Server jettyServer; - - @BeforeClass - public static void startJettyServer() throws Exception { - int port = SocketUtils.findAvailableTcpPort(); - jettyServer = new Server(port); - baseUrl = "http://localhost:" + port; - - ServletContextHandler handler = new ServletContextHandler(); - handler.setContextPath("/"); - - handler.addServlet(new ServletHolder(new EchoServlet()), "/echo"); - handler.addServlet(new ServletHolder(new EchoServlet()), "/echo"); - handler.addServlet(new ServletHolder(new StatusServlet(200)), "/status/ok"); - handler.addServlet(new ServletHolder(new StatusServlet(404)), "/status/notfound"); - handler.addServlet(new ServletHolder(new MethodServlet("DELETE")), "/methods/delete"); - handler.addServlet(new ServletHolder(new MethodServlet("GET")), "/methods/get"); - handler.addServlet(new ServletHolder(new MethodServlet("HEAD")), "/methods/head"); - handler.addServlet(new ServletHolder(new MethodServlet("OPTIONS")), "/methods/options"); - handler.addServlet(new ServletHolder(new PostServlet()), "/methods/post"); - handler.addServlet(new ServletHolder(new MethodServlet("PUT")), "/methods/put"); - handler.addServlet(new ServletHolder(new MethodServlet("PATCH")), "/methods/patch"); - - jettyServer.setHandler(handler); - jettyServer.start(); - } - @Before public final void createFactory() { factory = createRequestFactory(); @@ -88,13 +45,6 @@ public abstract class AbstractHttpRequestFactoryTestCase { protected abstract ClientHttpRequestFactory createRequestFactory(); - @AfterClass - public static void stopJettyServer() throws Exception { - if (jettyServer != null) { - jettyServer.stop(); - } - } - @Test public void status() throws Exception { URI uri = new URI(baseUrl + "/status/notfound"); @@ -210,85 +160,4 @@ public abstract class AbstractHttpRequestFactoryTestCase { } } - /** - * Servlet that sets a given status code. - */ - @SuppressWarnings("serial") - private static class StatusServlet extends GenericServlet { - - private final int sc; - - private StatusServlet(int sc) { - this.sc = sc; - } - - @Override - public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { - ((HttpServletResponse) response).setStatus(sc); - } - } - - @SuppressWarnings("serial") - private static class MethodServlet extends GenericServlet { - - private final String method; - - private MethodServlet(String method) { - this.method = method; - } - - @Override - public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { - HttpServletRequest httpReq = (HttpServletRequest) req; - assertEquals("Invalid HTTP method", method, httpReq.getMethod()); - res.setContentLength(0); - ((HttpServletResponse) res).setStatus(200); - } - } - - @SuppressWarnings("serial") - private static class PostServlet extends MethodServlet { - - private PostServlet() { - super("POST"); - } - - @Override - public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { - super.service(req, res); - long contentLength = req.getContentLength(); - if (contentLength != -1) { - InputStream in = req.getInputStream(); - long byteCount = 0; - byte[] buffer = new byte[4096]; - int bytesRead; - while ((bytesRead = in.read(buffer)) != -1) { - byteCount += bytesRead; - } - assertEquals("Invalid content-length", contentLength, byteCount); - } - } - } - - @SuppressWarnings("serial") - private static class EchoServlet extends HttpServlet { - - @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - echo(req, resp); - } - - private void echo(HttpServletRequest request, HttpServletResponse response) throws IOException { - response.setStatus(HttpServletResponse.SC_OK); - for (Enumeration e1 = request.getHeaderNames(); e1.hasMoreElements();) { - String headerName = (String) e1.nextElement(); - for (Enumeration e2 = request.getHeaders(headerName); e2.hasMoreElements();) { - String headerValue = (String) e2.nextElement(); - response.addHeader(headerName, headerValue); - } - } - FileCopyUtils.copy(request.getInputStream(), response.getOutputStream()); - } - } - } diff --git a/spring-web/src/test/java/org/springframework/http/client/AbstractJettyServerTestCase.java b/spring-web/src/test/java/org/springframework/http/client/AbstractJettyServerTestCase.java new file mode 100644 index 00000000000..6f7b70fd091 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/client/AbstractJettyServerTestCase.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2013 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.http.client; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +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.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.AfterClass; +import static org.junit.Assert.assertEquals; +import org.junit.BeforeClass; + +import org.springframework.util.FileCopyUtils; +import org.springframework.util.SocketUtils; + +/** @author Arjen Poutsma */ +public class AbstractJettyServerTestCase { + + protected static String baseUrl; + + private static Server jettyServer; + + @BeforeClass + public static void startJettyServer() throws Exception { + int port = SocketUtils.findAvailableTcpPort(); + jettyServer = new Server(port); + baseUrl = "http://localhost:" + port; + + ServletContextHandler handler = new ServletContextHandler(); + handler.setContextPath("/"); + + handler.addServlet(new ServletHolder(new EchoServlet()), "/echo"); + handler.addServlet(new ServletHolder(new EchoServlet()), "/echo"); + handler.addServlet(new ServletHolder(new StatusServlet(200)), "/status/ok"); + handler.addServlet(new ServletHolder(new StatusServlet(404)), "/status/notfound"); + handler.addServlet(new ServletHolder(new MethodServlet("DELETE")), "/methods/delete"); + handler.addServlet(new ServletHolder(new MethodServlet("GET")), "/methods/get"); + handler.addServlet(new ServletHolder(new MethodServlet("HEAD")), "/methods/head"); + handler.addServlet(new ServletHolder(new MethodServlet("OPTIONS")), "/methods/options"); + handler.addServlet(new ServletHolder(new PostServlet()), "/methods/post"); + handler.addServlet(new ServletHolder(new MethodServlet("PUT")), "/methods/put"); + handler.addServlet(new ServletHolder(new MethodServlet("PATCH")), "/methods/patch"); + + jettyServer.setHandler(handler); + jettyServer.start(); + } + + @AfterClass + public static void stopJettyServer() throws Exception { + if (jettyServer != null) { + jettyServer.stop(); + } + } + + /** + * Servlet that sets a given status code. + */ + @SuppressWarnings("serial") + private static class StatusServlet extends GenericServlet { + + private final int sc; + + private StatusServlet(int sc) { + this.sc = sc; + } + + @Override + public void service(ServletRequest request, ServletResponse response) throws + ServletException, IOException { + ((HttpServletResponse) response).setStatus(sc); + } + } + + @SuppressWarnings("serial") + private static class MethodServlet extends GenericServlet { + + private final String method; + + private MethodServlet(String method) { + this.method = method; + } + + @Override + public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { + HttpServletRequest httpReq = (HttpServletRequest) req; + assertEquals("Invalid HTTP method", method, httpReq.getMethod()); + res.setContentLength(0); + ((HttpServletResponse) res).setStatus(200); + } + } + + @SuppressWarnings("serial") + private static class PostServlet extends MethodServlet { + + private PostServlet() { + super("POST"); + } + + @Override + public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { + super.service(req, res); + long contentLength = req.getContentLength(); + if (contentLength != -1) { + InputStream in = req.getInputStream(); + long byteCount = 0; + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + byteCount += bytesRead; + } + assertEquals("Invalid content-length", contentLength, byteCount); + } + } + } + + @SuppressWarnings("serial") + private static class EchoServlet extends HttpServlet { + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + echo(req, resp); + } + + private void echo(HttpServletRequest request, HttpServletResponse response) throws IOException { + response.setStatus(HttpServletResponse.SC_OK); + for (Enumeration e1 = request.getHeaderNames(); e1.hasMoreElements();) { + String headerName = (String) e1.nextElement(); + for (Enumeration e2 = request.getHeaders(headerName); e2.hasMoreElements();) { + String headerValue = (String) e2.nextElement(); + response.addHeader(headerName, headerValue); + } + } + FileCopyUtils.copy(request.getInputStream(), response.getOutputStream()); + } + } +} diff --git a/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleAsyncHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleAsyncHttpRequestFactoryTests.java new file mode 100644 index 00000000000..519246b7b6e --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/client/BufferedSimpleAsyncHttpRequestFactoryTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2013 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.http.client; + +import java.net.ProtocolException; + +import org.junit.Test; + +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.http.HttpMethod; + +public class BufferedSimpleAsyncHttpRequestFactoryTests extends AbstractAsyncHttpRequestFactoryTestCase { + + @Override + protected AsyncClientHttpRequestFactory createRequestFactory() { + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + AsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + requestFactory.setTaskExecutor(taskExecutor); + return requestFactory; + } + + @Override + @Test + public void httpMethods() throws Exception { + try { + assertHttpMethod("patch", HttpMethod.PATCH); + } + catch (ProtocolException ex) { + // Currently HttpURLConnection does not support HTTP PATCH + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactoryTests.java new file mode 100644 index 00000000000..1e5c6fbe4d0 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/client/HttpComponentsAsyncClientHttpRequestFactoryTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * 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.http.client; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; + +/** + * @author Arjen Poutsma + */ +public class HttpComponentsAsyncClientHttpRequestFactoryTests extends AbstractAsyncHttpRequestFactoryTestCase { + + @Override + protected AsyncClientHttpRequestFactory createRequestFactory() { + return new HttpComponentsAsyncClientHttpRequestFactory(); + } + + + @Override + @Test + public void httpMethods() throws Exception { + assertHttpMethod("patch", HttpMethod.PATCH); + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/client/AbstractJettyServerTestCase.java b/spring-web/src/test/java/org/springframework/web/client/AbstractJettyServerTestCase.java new file mode 100644 index 00000000000..fcadcb6196a --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/client/AbstractJettyServerTestCase.java @@ -0,0 +1,270 @@ +/* + * Copyright 2002-2013 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; +import java.util.Collections; +import java.util.List; +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.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.AfterClass; +import static org.junit.Assert.*; +import org.junit.BeforeClass; + +import org.springframework.http.MediaType; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.SocketUtils; + +/** @author Arjen Poutsma */ +public class AbstractJettyServerTestCase { + + protected static String helloWorld = "H\u00e9llo W\u00f6rld"; + + protected static String baseUrl; + + protected static MediaType contentType; + + private static Server jettyServer; + + @BeforeClass + public static void startJettyServer() throws Exception { + int port = SocketUtils.findAvailableTcpPort(); + jettyServer = new Server(port); + baseUrl = "http://localhost:" + port; + ServletContextHandler handler = new ServletContextHandler(); + byte[] bytes = helloWorld.getBytes("UTF-8"); + contentType = new MediaType("text", "plain", Collections + .singletonMap("charset", "UTF-8")); + handler.addServlet(new ServletHolder(new GetServlet(bytes, contentType)), "/get"); + handler.addServlet(new ServletHolder(new GetServlet(new byte[0], contentType)), "/get/nothing"); + handler.addServlet(new ServletHolder(new GetServlet(bytes, null)), "/get/nocontenttype"); + handler.addServlet( + new ServletHolder(new PostServlet(helloWorld, baseUrl + "/post/1", bytes, contentType)), + "/post"); + handler.addServlet(new ServletHolder(new StatusCodeServlet(204)), "/status/nocontent"); + handler.addServlet(new ServletHolder(new StatusCodeServlet(304)), "/status/notmodified"); + handler.addServlet(new ServletHolder(new ErrorServlet(404)), "/status/notfound"); + handler.addServlet(new ServletHolder(new ErrorServlet(500)), "/status/server"); + handler.addServlet(new ServletHolder(new UriServlet()), "/uri/*"); + handler.addServlet(new ServletHolder(new MultipartServlet()), "/multipart"); + handler.addServlet(new ServletHolder(new DeleteServlet()), "/delete"); + handler.addServlet( + new ServletHolder(new PutServlet(helloWorld, bytes, contentType)), + "/put"); + jettyServer.setHandler(handler); + jettyServer.start(); + } + + @AfterClass + public static void stopJettyServer() throws Exception { + if (jettyServer != null) { + jettyServer.stop(); + } + } + + /** Servlet that sets the given status code. */ + @SuppressWarnings("serial") + private static class StatusCodeServlet extends GenericServlet { + + private final int sc; + + private StatusCodeServlet(int sc) { + this.sc = sc; + } + + @Override + public void service(ServletRequest request, ServletResponse response) throws + ServletException, IOException { + ((HttpServletResponse) response).setStatus(sc); + } + } + + /** Servlet that returns an error message for a given status code. */ + @SuppressWarnings("serial") + 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); + } + } + + @SuppressWarnings("serial") + private static class GetServlet extends HttpServlet { + + private final byte[] buf; + + private final MediaType contentType; + + private GetServlet(byte[] buf, MediaType contentType) { + this.buf = buf; + this.contentType = contentType; + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + if (contentType != null) { + response.setContentType(contentType.toString()); + } + response.setContentLength(buf.length); + FileCopyUtils.copy(buf, response.getOutputStream()); + } + } + + @SuppressWarnings("serial") + private static class PostServlet extends HttpServlet { + + private final String s; + + private final String location; + + private final byte[] buf; + + private final MediaType contentType; + + private PostServlet(String s, String location, byte[] buf, MediaType contentType) { + this.s = s; + this.location = location; + this.buf = buf; + this.contentType = contentType; + } + + @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); + response.setContentLength(buf.length); + response.setContentType(contentType.toString()); + FileCopyUtils.copy(buf, response.getOutputStream()); + } + } + + @SuppressWarnings("serial") + private static class PutServlet extends HttpServlet { + + private final String s; + + + private final byte[] buf; + + private final MediaType contentType; + + private PutServlet(String s, byte[] buf, MediaType contentType) { + this.s = s; + this.buf = buf; + this.contentType = contentType; + } + + @Override + protected void doPut(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_ACCEPTED); + } + } + + @SuppressWarnings("serial") + private static class UriServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setContentType("text/plain"); + resp.setCharacterEncoding("UTF-8"); + resp.getWriter().write(req.getRequestURI()); + } + } + + @SuppressWarnings("serial") + private static class MultipartServlet extends HttpServlet { + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + assertTrue(ServletFileUpload.isMultipartContent(req)); + FileItemFactory factory = new DiskFileItemFactory(); + ServletFileUpload upload = new ServletFileUpload(factory); + try { + List items = upload.parseRequest(req); + assertEquals(4, items.size()); + FileItem item = (FileItem) items.get(0); + assertTrue(item.isFormField()); + assertEquals("name 1", item.getFieldName()); + assertEquals("value 1", item.getString()); + + item = (FileItem) items.get(1); + assertTrue(item.isFormField()); + assertEquals("name 2", item.getFieldName()); + assertEquals("value 2+1", item.getString()); + + item = (FileItem) items.get(2); + assertTrue(item.isFormField()); + assertEquals("name 2", item.getFieldName()); + assertEquals("value 2+2", item.getString()); + + item = (FileItem) items.get(3); + assertFalse(item.isFormField()); + assertEquals("logo", item.getFieldName()); + assertEquals("logo.jpg", item.getName()); + assertEquals("image/jpeg", item.getContentType()); + } + catch (FileUploadException ex) { + throw new ServletException(ex); + } + + } + } + + @SuppressWarnings("serial") + private static class DeleteServlet extends HttpServlet { + + @Override + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + resp.setStatus(200); + } + + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java new file mode 100644 index 00000000000..87657398640 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java @@ -0,0 +1,247 @@ +/* + * Copyright 2002-2013 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.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.util.EnumSet; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.HttpComponentsAsyncClientHttpRequestFactory; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** @author Arjen Poutsma */ +public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCase { + + private AsyncRestTemplate template; + + @Before + public void createTemplate() { + template = new AsyncRestTemplate( + new HttpComponentsAsyncClientHttpRequestFactory()); + } + + @Test + public void getEntity() throws ExecutionException, InterruptedException { + Future> + futureEntity = template.getForEntity(baseUrl + "/{method}", String.class, "get"); + ResponseEntity entity = futureEntity.get(); + assertEquals("Invalid content", helloWorld, entity.getBody()); + assertFalse("No headers", entity.getHeaders().isEmpty()); + assertEquals("Invalid content-type", contentType, entity.getHeaders().getContentType()); + assertEquals("Invalid status code", HttpStatus.OK, entity.getStatusCode()); + } + + @Test + public void getNoResponse() throws ExecutionException, InterruptedException { + Future> + futureEntity = template.getForEntity(baseUrl + "/get/nothing", String.class); + ResponseEntity entity = futureEntity.get(); + assertNull("Invalid content", entity.getBody()); + } + + + @Test + public void getNoContentTypeHeader() + throws UnsupportedEncodingException, ExecutionException, + InterruptedException { + Future> + futureEntity = template.getForEntity(baseUrl + "/get/nocontenttype", + byte[].class); + ResponseEntity responseEntity = futureEntity.get(); + assertArrayEquals("Invalid content", helloWorld.getBytes("UTF-8"), + responseEntity.getBody()); + } + + + @Test + public void getNoContent() throws ExecutionException, InterruptedException { + Future> + responseFuture = template.getForEntity(baseUrl + "/status/nocontent", String.class); + ResponseEntity entity = responseFuture.get(); + assertEquals("Invalid response code", HttpStatus.NO_CONTENT, entity.getStatusCode()); + assertNull("Invalid content", entity.getBody()); + } + + @Test + public void getNotModified() throws ExecutionException, InterruptedException { + Future> + responseFuture = template.getForEntity(baseUrl + "/status/notmodified", + String.class); + ResponseEntity entity = responseFuture.get(); + assertEquals("Invalid response code", HttpStatus.NOT_MODIFIED, entity.getStatusCode()); + assertNull("Invalid content", entity.getBody()); + } + + @Test + public void headForHeaders() throws ExecutionException, InterruptedException { + Future headersFuture = template.headForHeaders(baseUrl + "/get"); + HttpHeaders headers = headersFuture.get(); + assertTrue("No Content-Type header", headers.containsKey("Content-Type")); + } + + @Test + public void postForLocation() + throws URISyntaxException, ExecutionException, InterruptedException { + HttpEntity requestEntity = new HttpEntity<>(helloWorld); + Future locationFuture = template.postForLocation(baseUrl + "/{method}", requestEntity, + "post"); + URI location = locationFuture.get(); + assertEquals("Invalid location", new URI(baseUrl + "/post/1"), location); + } + + @Test + public void postForLocationEntity() + throws URISyntaxException, ExecutionException, InterruptedException { + HttpHeaders entityHeaders = new HttpHeaders(); + entityHeaders.setContentType(new MediaType("text", "plain", Charset.forName("ISO-8859-15"))); + HttpEntity entity = new HttpEntity(helloWorld, entityHeaders); + Future + locationFuture = template.postForLocation(baseUrl + "/{method}", entity, + "post"); + URI location = locationFuture.get(); + assertEquals("Invalid location", new URI(baseUrl + "/post/1"), location); + } + + @Test + public void postForEntity() + throws URISyntaxException, ExecutionException, InterruptedException { + HttpEntity requestEntity = new HttpEntity<>(helloWorld); + Future> + responseEntityFuture = template.postForEntity(baseUrl + "/{method}", requestEntity, + String.class, "post"); + ResponseEntity responseEntity = responseEntityFuture.get(); + assertEquals("Invalid content", helloWorld, responseEntity.getBody()); + } + + @Test + public void put() + throws URISyntaxException, ExecutionException, InterruptedException { + HttpEntity requestEntity = new HttpEntity<>(helloWorld); + Future + responseEntityFuture = template.put(baseUrl + "/{method}", requestEntity, + "put"); + responseEntityFuture.get(); + } + + @Test + public void delete() + throws URISyntaxException, ExecutionException, InterruptedException { + Future deletedFuture = template.delete(new URI(baseUrl + "/delete")); + deletedFuture.get(); + } + + @Test + public void notFound() throws ExecutionException, InterruptedException { + try { + Future future = template.execute(baseUrl + "/status/notfound", HttpMethod.GET, null, null); + future.get(); + fail("HttpClientErrorException expected"); + } + catch (HttpClientErrorException ex) { + assertEquals(HttpStatus.NOT_FOUND, ex.getStatusCode()); + assertNotNull(ex.getStatusText()); + assertNotNull(ex.getResponseBodyAsString()); + } + } + + @Test + public void serverError() throws ExecutionException, InterruptedException { + try { + Future future = template.execute(baseUrl + "/status/server", HttpMethod.GET, null, null); + future.get(); + fail("HttpServerErrorException expected"); + } + catch (HttpServerErrorException ex) { + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, ex.getStatusCode()); + assertNotNull(ex.getStatusText()); + assertNotNull(ex.getResponseBodyAsString()); + } + } + + @Test + public void optionsForAllow() + throws URISyntaxException, ExecutionException, InterruptedException { + Future> + allowedFuture = template.optionsForAllow(new URI(baseUrl + "/get")); + Set allowed = allowedFuture.get(); + assertEquals("Invalid response", + EnumSet.of(HttpMethod.GET, HttpMethod.OPTIONS, HttpMethod.HEAD, HttpMethod.TRACE), allowed); + } + + @Test + @SuppressWarnings("unchecked") + public void exchangeGet() throws Exception { + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.set("MyHeader", "MyValue"); + HttpEntity requestEntity = new HttpEntity(requestHeaders); + Future> responseFuture = + template.exchange(baseUrl + "/{method}", HttpMethod.GET, requestEntity, + String.class, "get"); + ResponseEntity response = responseFuture.get(); + assertEquals("Invalid content", helloWorld, response.getBody()); + } + + @Test + public void exchangePost() throws Exception { + HttpHeaders requestHeaders = new HttpHeaders(); + requestHeaders.set("MyHeader", "MyValue"); + requestHeaders.setContentType(MediaType.TEXT_PLAIN); + HttpEntity requestEntity = new HttpEntity(helloWorld, requestHeaders); + Future> + resultFuture = template.exchange(baseUrl + "/{method}", HttpMethod.POST, + requestEntity, Void.class, "post"); + ResponseEntity result = resultFuture.get(); + assertEquals("Invalid location", new URI(baseUrl + "/post/1"), + result.getHeaders().getLocation()); + assertFalse(result.hasBody()); + } + + @Test + public void multipart() throws UnsupportedEncodingException, ExecutionException, + InterruptedException { + MultiValueMap parts = new LinkedMultiValueMap(); + parts.add("name 1", "value 1"); + parts.add("name 2", "value 2+1"); + parts.add("name 2", "value 2+2"); + Resource logo = new ClassPathResource("/org/springframework/http/converter/logo.jpg"); + parts.add("logo", logo); + + HttpEntity> requestBody = new HttpEntity<>(parts); + Future future = + template.postForLocation(baseUrl + "/multipart", requestBody); + future.get(); + } + +} 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 ed7438eb493..bacef9bde98 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 @@ -63,54 +63,15 @@ import org.springframework.util.SocketUtils; import static org.junit.Assert.*; /** @author Arjen Poutsma */ -public class RestTemplateIntegrationTests { +public class RestTemplateIntegrationTests extends AbstractJettyServerTestCase { private RestTemplate template; - private static Server jettyServer; - - private static String helloWorld = "H\u00e9llo W\u00f6rld"; - - private static String baseUrl; - - private static MediaType contentType; - - @BeforeClass - public static void startJettyServer() throws Exception { - int port = SocketUtils.findAvailableTcpPort(); - jettyServer = new Server(port); - baseUrl = "http://localhost:" + port; - ServletContextHandler handler = new ServletContextHandler(); - byte[] bytes = helloWorld.getBytes("UTF-8"); - contentType = new MediaType("text", "plain", Collections.singletonMap("charset", "UTF-8")); - handler.addServlet(new ServletHolder(new GetServlet(bytes, contentType)), "/get"); - handler.addServlet(new ServletHolder(new GetServlet(new byte[0], contentType)), "/get/nothing"); - handler.addServlet(new ServletHolder(new GetServlet(bytes, null)), "/get/nocontenttype"); - handler.addServlet( - new ServletHolder(new PostServlet(helloWorld, baseUrl + "/post/1", bytes, contentType)), - "/post"); - handler.addServlet(new ServletHolder(new StatusCodeServlet(204)), "/status/nocontent"); - handler.addServlet(new ServletHolder(new StatusCodeServlet(304)), "/status/notmodified"); - handler.addServlet(new ServletHolder(new ErrorServlet(404)), "/status/notfound"); - handler.addServlet(new ServletHolder(new ErrorServlet(500)), "/status/server"); - handler.addServlet(new ServletHolder(new UriServlet()), "/uri/*"); - handler.addServlet(new ServletHolder(new MultipartServlet()), "/multipart"); - jettyServer.setHandler(handler); - jettyServer.start(); - } - @Before public void createTemplate() { template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); } - @AfterClass - public static void stopJettyServer() throws Exception { - if (jettyServer != null) { - jettyServer.stop(); - } - } - @Test public void getString() { String s = template.getForObject(baseUrl + "/{method}", String.class, "get"); @@ -258,142 +219,4 @@ public class RestTemplateIntegrationTests { assertFalse(result.hasBody()); } - /** Servlet that sets the given status code. */ - @SuppressWarnings("serial") - private static class StatusCodeServlet extends GenericServlet { - - private final int sc; - - private StatusCodeServlet(int sc) { - this.sc = sc; - } - - @Override - public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { - ((HttpServletResponse) response).setStatus(sc); - } - } - - /** Servlet that returns an error message for a given status code. */ - @SuppressWarnings("serial") - 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); - } - } - - @SuppressWarnings("serial") - private static class GetServlet extends HttpServlet { - - private final byte[] buf; - - private final MediaType contentType; - - private GetServlet(byte[] buf, MediaType contentType) { - this.buf = buf; - this.contentType = contentType; - } - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - if (contentType != null) { - response.setContentType(contentType.toString()); - } - response.setContentLength(buf.length); - FileCopyUtils.copy(buf, response.getOutputStream()); - } - } - - @SuppressWarnings("serial") - private static class PostServlet extends HttpServlet { - - private final String s; - - private final String location; - - private final byte[] buf; - - private final MediaType contentType; - - private PostServlet(String s, String location, byte[] buf, MediaType contentType) { - this.s = s; - this.location = location; - this.buf = buf; - this.contentType = contentType; - } - - @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); - response.setContentLength(buf.length); - response.setContentType(contentType.toString()); - FileCopyUtils.copy(buf, response.getOutputStream()); - } - } - - @SuppressWarnings("serial") - private static class UriServlet extends HttpServlet { - - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - resp.setContentType("text/plain"); - resp.setCharacterEncoding("UTF-8"); - resp.getWriter().write(req.getRequestURI()); - } - } - - @SuppressWarnings("serial") - private static class MultipartServlet extends HttpServlet { - - @Override - protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - assertTrue(ServletFileUpload.isMultipartContent(req)); - FileItemFactory factory = new DiskFileItemFactory(); - ServletFileUpload upload = new ServletFileUpload(factory); - try { - List items = upload.parseRequest(req); - assertEquals(4, items.size()); - FileItem item = (FileItem) items.get(0); - assertTrue(item.isFormField()); - assertEquals("name 1", item.getFieldName()); - assertEquals("value 1", item.getString()); - - item = (FileItem) items.get(1); - assertTrue(item.isFormField()); - assertEquals("name 2", item.getFieldName()); - assertEquals("value 2+1", item.getString()); - - item = (FileItem) items.get(2); - assertTrue(item.isFormField()); - assertEquals("name 2", item.getFieldName()); - assertEquals("value 2+2", item.getString()); - - item = (FileItem) items.get(3); - assertFalse(item.isFormField()); - assertEquals("logo", item.getFieldName()); - assertEquals("logo.jpg", item.getName()); - assertEquals("image/jpeg", item.getContentType()); - } - catch (FileUploadException ex) { - throw new ServletException(ex); - } - - } - } - }