SWS-563 - Provide support for Apache HttpClient 4.0

This commit is contained in:
Arjen Poutsma
2012-05-08 10:01:56 +00:00
parent 8df20eaf33
commit a19c411374
7 changed files with 549 additions and 4 deletions

View File

@@ -136,6 +136,11 @@
<artifactId>mail</artifactId>
</dependency>
<!-- Transport dependencies -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2005-2010 the original author or authors.
* Copyright 2005-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
* 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,
@@ -42,7 +42,9 @@ import org.apache.commons.httpclient.methods.PostMethod;
*
* @author Arjen Poutsma
* @since 1.0.0
* @deprecated In favor of {@link HttpComponentsConnection}
*/
@Deprecated
public class CommonsHttpConnection extends AbstractHttpSenderConnection {
private final HttpClient httpClient;

View File

@@ -1,11 +1,11 @@
/*
* Copyright 2005-2010 the original author or authors.
* Copyright 2005-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
* 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,
@@ -51,7 +51,9 @@ import org.apache.commons.httpclient.methods.PostMethod;
* @see HttpClient
* @see #setCredentials(Credentials)
* @since 1.0.0
* @deprecated In favor of {@link HttpComponentsMessageSender}
*/
@Deprecated
public class CommonsHttpMessageSender extends AbstractHttpWebServiceMessageSender
implements InitializingBean, DisposableBean {

View File

@@ -0,0 +1,165 @@
/*
* Copyright 2005-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.ws.transport.http;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Iterator;
import org.springframework.util.Assert;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.transport.WebServiceConnection;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.util.EntityUtils;
/**
* Implementation of {@link WebServiceConnection} that is based on Apache HttpClient. Exposes a {@link HttpPost} and
* {@link HttpResponse}.
*
* @author Alan Stewart
* @author Barry Pitman
* @author Arjen Poutsma
* @since 2.1.0
*/
public class HttpComponentsConnection extends AbstractHttpSenderConnection {
private final HttpClient httpClient;
private final HttpPost httpPost;
private HttpResponse httpResponse;
private ByteArrayOutputStream requestBuffer;
protected HttpComponentsConnection(HttpClient httpClient, HttpPost httpPost) {
Assert.notNull(httpClient, "httpClient must not be null");
Assert.notNull(httpPost, "httpPost must not be null");
this.httpClient = httpClient;
this.httpPost = httpPost;
}
public HttpPost getHttpPost() {
return httpPost;
}
public HttpResponse getHttpResponse() {
return httpResponse;
}
@Override
public void onClose() throws IOException {
if (httpResponse != null && httpResponse.getEntity() != null) {
EntityUtils.consume(httpResponse.getEntity());
}
}
/*
* URI
*/
public URI getUri() throws URISyntaxException {
return new URI(httpPost.getURI().toString());
}
/*
* Sending request
*/
@Override
protected void onSendBeforeWrite(WebServiceMessage message) throws IOException {
requestBuffer = new ByteArrayOutputStream();
}
@Override
protected void addRequestHeader(String name, String value) throws IOException {
httpPost.addHeader(name, value);
}
@Override
protected OutputStream getRequestOutputStream() throws IOException {
return requestBuffer;
}
@Override
protected void onSendAfterWrite(WebServiceMessage message) throws IOException {
httpPost.setEntity(new ByteArrayEntity(requestBuffer.toByteArray()));
requestBuffer = null;
httpResponse = httpClient.execute(httpPost);
}
/*
* Receiving response
*/
@Override
protected int getResponseCode() throws IOException {
return httpResponse.getStatusLine().getStatusCode();
}
@Override
protected String getResponseMessage() throws IOException {
return httpResponse.getStatusLine().getReasonPhrase();
}
@Override
protected long getResponseContentLength() throws IOException {
HttpEntity entity = httpResponse.getEntity();
if (entity != null) {
return entity.getContentLength();
}
return 0;
}
@Override
protected InputStream getRawResponseInputStream() throws IOException {
HttpEntity entity = httpResponse.getEntity();
if (entity != null) {
return entity.getContent();
}
throw new IllegalStateException("Response has no enclosing response entity, cannot create input stream");
}
@Override
protected Iterator<String> getResponseHeaderNames() throws IOException {
Header[] headers = httpResponse.getAllHeaders();
String[] names = new String[headers.length];
for (int i = 0; i < headers.length; i++) {
names[i] = headers[i].getName();
}
return Arrays.asList(names).iterator();
}
@Override
protected Iterator<String> getResponseHeaders(String name) throws IOException {
Header[] headers = httpResponse.getHeaders(name);
String[] values = new String[headers.length];
for (int i = 0; i < headers.length; i++) {
values[i] = headers[i].getValue();
}
return Arrays.asList(values).iterator();
}
}

View File

@@ -0,0 +1,254 @@
/*
* Copyright 2005-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.ws.transport.http;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.ws.transport.WebServiceConnection;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.SingleClientConnManager;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.protocol.BasicHttpProcessor;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
/**
* {@code WebServiceMessageSender} implementation that uses <a href="http://hc.apache.org/httpcomponents-client">Apache
* HttpClient</a> to execute POST requests.
* <p/>
* Allows to use a pre-configured HttpClient instance, potentially with authentication, HTTP connection pooling, etc.
* Authentication can also be set by injecting a {@link Credentials} instance (such as the {@link
* UsernamePasswordCredentials}).
*
* @author Alan Stewart
* @author Barry Pitman
* @author Arjen Poutsma
* @see HttpClient
* @since 2.1.0
*/
public class HttpComponentsMessageSender extends AbstractHttpWebServiceMessageSender
implements InitializingBean, DisposableBean {
private static final int DEFAULT_CONNECTION_TIMEOUT_MILLISECONDS = (60 * 1000);
private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000);
private HttpClient httpClient;
private Credentials credentials;
private AuthScope authScope = AuthScope.ANY;
/**
* Create a new instance of the {@code HttpClientMessageSender} with a default {@link HttpClient} that uses a
* default {@link SingleClientConnManager}.
*/
public HttpComponentsMessageSender() {
httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager()) {
@Override
protected BasicHttpProcessor createHttpProcessor() {
BasicHttpProcessor processor = super.createHttpProcessor();
processor.addInterceptor(new ProtocolExceptionOverrideInterceptor(), 0);
return processor;
}
};
setConnectionTimeout(DEFAULT_CONNECTION_TIMEOUT_MILLISECONDS);
setReadTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS);
}
/**
* Create a new instance of the <code>HttpClientMessageSender</code> with the given {@link HttpClient} instance.
*
* @param httpClient the HttpClient instance to use for this sender
*/
public HttpComponentsMessageSender(HttpClient httpClient) {
Assert.notNull(httpClient, "httpClient must not be null");
this.httpClient = httpClient;
}
/**
* Sets the credentials to be used. If not set, no authentication is done.
*
* @see UsernamePasswordCredentials
* @see org.apache.http.auth.NTCredentials
*/
public void setCredentials(Credentials credentials) {
this.credentials = credentials;
}
/**
* Returns the <code>HttpClient</code> used by this message sender.
*/
public HttpClient getHttpClient() {
return httpClient;
}
/**
* Set the {@code HttpClient} used by this message sender.
*/
public void setHttpClient(HttpClient httpClient) {
this.httpClient = httpClient;
}
/**
* Sets the timeout until a connection is established. A value of 0 means <em>never</em> timeout.
*
* @param timeout the timeout value in milliseconds
* @see org.apache.http.params.HttpConnectionParams#setConnectionTimeout(org.apache.http.params.HttpParams, int)
*/
public void setConnectionTimeout(int timeout) {
if (timeout < 0) {
throw new IllegalArgumentException("timeout must be a non-negative value");
}
HttpConnectionParams.setConnectionTimeout(getHttpClient().getParams(), timeout);
}
/**
* Set the socket read timeout for the underlying HttpClient. A value of 0 means <em>never</em> timeout.
*
* @param timeout the timeout value in milliseconds
* @see org.apache.http.params.HttpConnectionParams#setSoTimeout(org.apache.http.params.HttpParams, int)
*/
public void setReadTimeout(int timeout) {
if (timeout < 0) {
throw new IllegalArgumentException("timeout must be a non-negative value");
}
HttpConnectionParams.setSoTimeout(getHttpClient().getParams(), timeout);
}
/**
* Sets the maximum number of connections allowed for the underlying HttpClient.
*
* @param maxTotalConnections the maximum number of connections allowed
* @see ThreadSafeClientConnManager#setMaxTotal(int)
*/
public void setMaxTotalConnections(int maxTotalConnections) {
if (maxTotalConnections <= 0) {
throw new IllegalArgumentException("maxTotalConnections must be a positive value");
}
ClientConnectionManager connectionManager = getHttpClient().getConnectionManager();
if (!(connectionManager instanceof ThreadSafeClientConnManager)) {
throw new IllegalArgumentException("maxTotalConnections is not supported on " +
connectionManager.getClass().getName() + ". Use " + ThreadSafeClientConnManager.class.getName() +
" instead");
}
((ThreadSafeClientConnManager) connectionManager).setMaxTotal(maxTotalConnections);
}
/**
* Sets the maximum number of connections per host for the underlying HttpClient. The maximum number of connections
* per host can be set in a form accepted by the {@code java.util.Properties} class, like as follows:
* <p/>
* <pre>
* https://www.example.com=1
* http://www.example.com:8080=7
* http://www.springframework.org=10
* </pre>
* <p/>
* The host can be specified as a URI (with scheme and port).
*
* @param maxConnectionsPerHost a properties object specifying the maximum number of connection
* @see org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager#setMaxForRoute(org.apache.http.conn.routing.HttpRoute,
* int)
*/
public void setMaxConnectionsPerHost(Map<String, String> maxConnectionsPerHost) throws URISyntaxException {
ClientConnectionManager connectionManager = getHttpClient().getConnectionManager();
if (!(connectionManager instanceof ThreadSafeClientConnManager)) {
throw new IllegalArgumentException("maxConnectionsPerHost is not supported on " +
connectionManager.getClass().getName() + ". Use " + ThreadSafeClientConnManager.class.getName() +
" instead");
}
for (Object o : maxConnectionsPerHost.keySet()) {
String host = (String) o;
URI uri = new URI(host);
HttpHost httpHost = new HttpHost(uri.getHost(), uri.getPort(), uri.getScheme());
int maxHostConnections = Integer.parseInt(maxConnectionsPerHost.get(host));
((ThreadSafeClientConnManager) connectionManager)
.setMaxForRoute(new HttpRoute(httpHost), maxHostConnections);
}
}
/**
* Sets the authentication scope to be used. Only used when the <code>credentials</code> property has been set.
* <p/>
* By default, the {@link AuthScope#ANY} is used.
*
* @see #setCredentials(Credentials)
*/
public void setAuthScope(AuthScope authScope) {
this.authScope = authScope;
}
public void afterPropertiesSet() throws Exception {
if (credentials != null && getHttpClient() instanceof DefaultHttpClient) {
((DefaultHttpClient) getHttpClient()).getCredentialsProvider().setCredentials(authScope, credentials);
}
}
public WebServiceConnection createConnection(URI uri) throws IOException {
HttpPost httpPost = new HttpPost(uri);
if (isAcceptGzipEncoding()) {
httpPost.addHeader(HttpTransportConstants.HEADER_ACCEPT_ENCODING,
HttpTransportConstants.CONTENT_ENCODING_GZIP);
}
return new HttpComponentsConnection(getHttpClient(), httpPost);
}
public void destroy() throws Exception {
getHttpClient().getConnectionManager().shutdown();
}
/**
* HttpClient {@link org.apache.http.HttpRequestInterceptor} implementation that removes {@code Content-Length} and
* {@code Transfer-Encoding} headers from the request. Necessary, because SAAJ and other SOAP implementations set these
* headers themselves, and HttpClient throws an exception if they have been set.
*/
private static class ProtocolExceptionOverrideInterceptor implements HttpRequestInterceptor {
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
if (request instanceof HttpEntityEnclosingRequest) {
if (request.containsHeader(HTTP.TRANSFER_ENCODING)) {
request.removeHeaders(HTTP.TRANSFER_ENCODING);
}
if (request.containsHeader(HTTP.CONTENT_LEN)) {
request.removeHeaders(HTTP.CONTENT_LEN);
}
}
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2005-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.ws.transport.http;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.soap.MessageFactory;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.util.FileCopyUtils;
import org.springframework.ws.soap.saaj.SaajSoapMessage;
import org.springframework.ws.soap.saaj.SaajSoapMessageFactory;
import org.springframework.ws.transport.WebServiceConnection;
import org.springframework.ws.transport.support.FreePortScanner;
import org.apache.commons.httpclient.URIException;
import org.junit.Test;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.servlet.Context;
import org.mortbay.jetty.servlet.ServletHolder;
public class HttpComponentsMessageSenderIntegrationTest extends AbstractHttpWebServiceMessageSenderIntegrationTestCase {
@Override
protected AbstractHttpWebServiceMessageSender createMessageSender() {
return new HttpComponentsMessageSender();
}
@Test
public void testMaxConnections() throws URISyntaxException, URIException {
HttpComponentsMessageSender messageSender = new HttpComponentsMessageSender();
messageSender.setMaxTotalConnections(2);
Map<String, String> maxConnectionsPerHost = new HashMap<String, String>();
maxConnectionsPerHost.put("https://www.example.com", "1");
maxConnectionsPerHost.put("http://www.example.com:8080", "7");
maxConnectionsPerHost.put("http://www.springframework.org", "10");
messageSender.setMaxConnectionsPerHost(maxConnectionsPerHost);
}
@Test
public void testContextClose() throws Exception {
MessageFactory messageFactory = MessageFactory.newInstance();
int port = FreePortScanner.getFreePort();
Server jettyServer = new Server(port);
Context jettyContext = new Context(jettyServer, "/");
jettyContext.addServlet(new ServletHolder(new EchoServlet()), "/");
jettyServer.start();
WebServiceConnection connection = null;
try {
StaticApplicationContext appContext = new StaticApplicationContext();
appContext.registerSingleton("messageSender", HttpComponentsMessageSender.class);
appContext.refresh();
HttpComponentsMessageSender messageSender = appContext
.getBean("messageSender", HttpComponentsMessageSender.class);
connection = messageSender.createConnection(new URI("http://localhost:" + port));
connection.send(new SaajSoapMessage(messageFactory.createMessage()));
connection.receive(new SaajSoapMessageFactory(messageFactory));
appContext.close();
}
finally {
if (connection != null) {
try {
connection.close();
} catch (IOException ex) {
// ignore
}
}
if (jettyServer.isRunning()) {
jettyServer.stop();
}
}
}
private class EchoServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/xml");
FileCopyUtils.copy(request.getInputStream(), response.getOutputStream());
}
}
}

View File

@@ -630,6 +630,11 @@
<scope>provided</scope>
</dependency>
<!-- Transport dependencies -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.1.3</version>
</dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>