/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.wink.client.internal.handlers.httpclient;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MultivaluedMap;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.conn.params.ConnRoutePNames;
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.conn.ssl.X509HostnameVerifier;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.SchemeRegistryFactory;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.util.EntityUtils;
import org.apache.wink.client.ClientRequest;
import org.apache.wink.client.ClientResponse;
import org.apache.wink.client.handlers.HandlerContext;
import org.apache.wink.client.httpclient.ApacheHttpClientConfig;
import org.apache.wink.client.internal.handlers.AbstractConnectionHandler;
import org.apache.wink.client.internal.handlers.ClientResponseImpl;
import org.apache.wink.common.internal.WinkConfiguration;
/**
* Extends AbstractConnectionHandler and uses Apache HttpClient to perform HTTP
* request execution. Each outgoing Http request is wrapped by EntityWriter.
*/
public class ApacheHttpClientConnectionHandler extends AbstractConnectionHandler {
private HttpClient httpclient;
public ApacheHttpClientConnectionHandler() {
httpclient = null;
}
public ApacheHttpClientConnectionHandler(HttpClient httpclient) {
this.httpclient = httpclient;
}
public ClientResponse handle(ClientRequest request, HandlerContext context) throws Exception {
HttpResponse response = null;
try {
response = processRequest(request, context);
return processResponse(request, context, response);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private HttpResponse processRequest(ClientRequest request, HandlerContext context)
throws IOException, KeyManagementException, NoSuchAlgorithmException {
HttpClient client = openConnection(request);
// TODO: move this functionality to the base class
NonCloseableOutputStream ncos = new NonCloseableOutputStream();
OutputStream os = ncos;
EntityWriter entityWriter = null;
if (request.getEntity() != null) {
os = adaptOutputStream(ncos, request, context.getOutputStreamAdapters());
// cast is safe because we're on the client
ApacheHttpClientConfig config = (ApacheHttpClientConfig)request.getAttribute(WinkConfiguration.class);
// prepare the entity that will write our entity
entityWriter = new EntityWriter(this, request, os, ncos, config.isChunked());
}
HttpRequestBase entityRequest = setupHttpRequest(request, client, entityWriter);
try {
return client.execute(entityRequest);
} catch (Exception ex) {
entityRequest.abort();
throw new RuntimeException(ex);
}
}
private HttpRequestBase setupHttpRequest(ClientRequest request,
HttpClient client,
EntityWriter entityWriter) {
URI uri = request.getURI();
String method = request.getMethod();
HttpRequestBase httpRequest = null;
if (entityWriter == null) {
GenericHttpRequestBase entityRequest = new GenericHttpRequestBase(method);
httpRequest = entityRequest;
} else {
// create a new request with the specified method
HttpEntityEnclosingRequestBase entityRequest =
new GenericHttpEntityEnclosingRequestBase(method);
entityRequest.setEntity(entityWriter);
httpRequest = entityRequest;
}
// set the uri
httpRequest.setURI(uri);
// add all headers
MultivaluedMap<String, String> headers = request.getHeaders();
for (String header : headers.keySet()) {
List<String> values = headers.get(header);
for (String value : values) {
if (value != null) {
httpRequest.addHeader(header, value);
}
}
}
return httpRequest;
}
private synchronized HttpClient openConnection(ClientRequest request) throws NoSuchAlgorithmException, KeyManagementException {
if (this.httpclient != null) {
return this.httpclient;
}
// cast is safe because we're on the client
ApacheHttpClientConfig config = (ApacheHttpClientConfig)request.getAttribute(WinkConfiguration.class);
BasicHttpParams params = new BasicHttpParams();
params.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, Integer.valueOf(config
.getConnectTimeout()));
params.setParameter(CoreConnectionPNames.SO_TIMEOUT, Integer.valueOf(config
.getReadTimeout()));
params.setParameter(ClientPNames.HANDLE_REDIRECTS, Boolean.valueOf(config
.isFollowRedirects()));
if (config.isFollowRedirects()) {
params.setParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, Boolean.TRUE);
}
// setup proxy
if (config.getProxyHost() != null) {
params.setParameter(ConnRoutePNames.DEFAULT_PROXY, new HttpHost(config.getProxyHost(),
config.getProxyPort()));
}
if (config.getMaxPooledConnections() > 0) {
SchemeRegistry schemeRegistry = SchemeRegistryFactory.createDefault();
ThreadSafeClientConnManager httpConnectionManager = new ThreadSafeClientConnManager(schemeRegistry);
httpConnectionManager.setMaxTotal(config.getMaxPooledConnections());
httpConnectionManager.setDefaultMaxPerRoute(config.getMaxPooledConnections());
this.httpclient = new DefaultHttpClient(httpConnectionManager, params);
} else {
this.httpclient = new DefaultHttpClient(params);
}
if (config.getBypassHostnameVerification()) {
SSLContext sslcontext = SSLContext.getInstance("TLS");
sslcontext.init(null, null, null);
SSLSocketFactory sf = new SSLSocketFactory(sslcontext, new X509HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
public void verify(String host, String[] cns, String[] subjectAlts)
throws SSLException {
}
public void verify(String host, X509Certificate cert) throws SSLException {
}
public void verify(String host, SSLSocket ssl) throws IOException {
}
});
httpclient.getConnectionManager().getSchemeRegistry().register(new Scheme("https", 443, sf));
}
return this.httpclient;
}
/**
* An empty input stream to simulate an empty message body.
*/
private static class EmptyInputStream extends InputStream {
@Override
public int read() throws IOException {
return -1;
}
}
private ClientResponse processResponse(ClientRequest request,
HandlerContext context,
HttpResponse httpResponse) throws IllegalStateException,
IOException {
ClientResponse response = createResponse(request, httpResponse);
HttpEntity entity = httpResponse.getEntity();
InputStream is = null;
if (entity == null) {
is = new EmptyInputStream();
} else {
is = entity.getContent();
}
is = adaptInputStream(is, response, context.getInputStreamAdapters());
response.setEntity(is);
return response;
}
private ClientResponse createResponse(ClientRequest request, final HttpResponse httpResponse) {
final ClientResponseImpl response = new ClientResponseImpl();
StatusLine statusLine = httpResponse.getStatusLine();
response.setStatusCode(statusLine.getStatusCode());
response.setMessage(statusLine.getReasonPhrase());
response.getAttributes().putAll(request.getAttributes());
response.setContentConsumer(new Runnable() {
public void run() {
HttpEntity entity = httpResponse.getEntity();
if (entity != null) {
try {
EntityUtils.consume(entity);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
});
processResponseHeaders(response, httpResponse);
return response;
}
private void processResponseHeaders(ClientResponse response, HttpResponse httpResponse) {
Header[] allHeaders = httpResponse.getAllHeaders();
for (Header header : allHeaders) {
response.getHeaders().add(header.getName(), header.getValue());
}
}
private static class GenericHttpRequestBase extends HttpRequestBase {
private String method;
public GenericHttpRequestBase(String method) {
this.method = method;
}
@Override
public String getMethod() {
return method;
}
}
private static class GenericHttpEntityEnclosingRequestBase extends
HttpEntityEnclosingRequestBase {
private String method;
public GenericHttpEntityEnclosingRequestBase(String method) {
this.method = method;
}
@Override
public String getMethod() {
return method;
}
}
// TODO: move this class to the base class
private static class NonCloseableOutputStream extends OutputStream {
OutputStream os;
public NonCloseableOutputStream() {
}
public void setOutputStream(OutputStream os) {
this.os = os;
}
@Override
public void close() throws IOException {
// do nothing
}
@Override
public void flush() throws IOException {
os.flush();
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
os.write(b, off, len);
}
@Override
public void write(byte[] b) throws IOException {
os.write(b);
}
@Override
public void write(int b) throws IOException {
os.write(b);
}
}
private static class EntityWriter implements HttpEntity {
private ApacheHttpClientConnectionHandler apacheHttpClientHandler;
private ClientRequest request;
private OutputStream adaptedOutputStream;
private NonCloseableOutputStream ncos;
private boolean chunked;
private long length = -1l;
private byte[] content;
public EntityWriter(ApacheHttpClientConnectionHandler apacheHttpClientHandler,
ClientRequest request,
OutputStream adaptedOutputStream,
NonCloseableOutputStream ncos,
boolean chunked) {
this.apacheHttpClientHandler = apacheHttpClientHandler;
this.request = request;
this.adaptedOutputStream = adaptedOutputStream;
this.ncos = ncos;
this.chunked = chunked;
if (!chunked) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
apacheHttpClientHandler.writeEntity(request, bos);
content = bos.toByteArray();
length = content.length;
} catch (IOException e) {
throw new WebApplicationException(e);
}
}
}
@Deprecated
public void consumeContent() throws IOException {
}
public InputStream getContent() throws IOException, IllegalStateException {
return null;
}
public Header getContentEncoding() {
return null;
}
public long getContentLength() {
return length;
}
public Header getContentType() {
return null;
}
public boolean isChunked() {
return chunked;
}
public boolean isRepeatable() {
return true;
}
public boolean isStreaming() {
return content == null;
}
public void writeTo(OutputStream os) throws IOException {
if (!chunked && length > 0 && content != null) {
os.write(content);
os.flush();
} else {
ncos.setOutputStream(os);
apacheHttpClientHandler.writeEntity(request, adaptedOutputStream);
}
}
}
}