/* Copyright 2012 Cloudseal Ltd
*
* 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 com.cloudseal.rest.client;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.entity.GzipDecompressingEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.protocol.HttpContext;
import com.cloudseal.rest.exception.RestException;
import org.apache.log4j.Logger;
/**
* Apache <a href="http://hc.apache.org/">Http Components</a> based
* implementation
*
* <p>
* This class supports connection pooling and GZIP compression. This
* implementation can reuse an existing HttpClient or create and manage it's own
* connection pool. <strong>Note:</strong> This class wraps all exceptions as
* unchecked RestExceptions so there is no need to explicitly catch them unless
* required.
* </p>
*
* <p>
* Logging is supported via <a href="http://logging.apache.org/log4j/1.2/">Log4J</a> at Error,
* Debug and Trace levels. This class is thread safe.
* </p>
*
* <p>
* Calling code should invoke the destroy method when finished to free up
* resources
* </p>
*
* @author Toby Hobson <a
* href="mailto:toby.hobson@cloudseal.com">toby.hobson@cloudseal.com</a>
* @since 1.0
*
*
*/
public class RESTClientImpl implements RESTClient {
private static final Logger LOG = Logger.getLogger(RESTClientImpl.class);
private PoolingClientConnectionManager cm;
private HttpClient httpClient;
private String hostname;
private String accessKey;
private String secret;
private JAXBContext jaxbContext;
/**
* Construct a new instance with a configurable thread/connection pool.
*
* @param hostname
* Cloudseal hostname e.g. acme
* @param accessKey
* Access key (you can find this in your Cloudseal admin console)
* @param secret
* Secret (you can find this in your Cloudseal admin cons
* @param threadPool
* Thread/connection pool size
* @param useGzip
* Whether the client should use GZIP compression.
*
*/
public RESTClientImpl(String hostname, String accessKey, String secret,
int threadPool, boolean useGzip) {
this.hostname = hostname;
this.accessKey = accessKey;
this.secret = secret;
LOG.debug("Creating HTTP connection pool");
cm = new PoolingClientConnectionManager();
cm.setMaxTotal(threadPool);
cm.setDefaultMaxPerRoute(threadPool);
LOG.debug("Creating HTTP client");
httpClient = new DefaultHttpClient(cm);
if (useGzip) {
LOG.debug("Adding GZIP support");
addGzipInterceptors((DefaultHttpClient) httpClient);
}
try {
LOG.debug("Creating JAXB marshaller/unmarshaller");
jaxbContext = JAXBContext.newInstance("com.cloudseal.rest.jaxb");
} catch (JAXBException ex) {
throw new RestException(ex);
}
}
/**
* Construct a new instance using an existing HttpClient
*
* @param hostname
* Cloudseal hostname e.g. acme
* @param accessKey
* Access key (you can find this in your Cloudseal admin console)
* @param secret
* Secret (you can find this in your Cloudseal admin console)
* @param httpClient
* Existing {@link org.apache.http.client.HttpClient} instance
*
*/
public RESTClientImpl(String hostname, String accessKey, String secret,
HttpClient httpClient) {
this.hostname = hostname;
this.accessKey = accessKey;
this.secret = secret;
this.httpClient = httpClient;
LOG.debug("Using existing HttpClient instance");
try {
LOG.debug("Creating JAXB marshaller/unmarshaller");
jaxbContext = JAXBContext.newInstance("com.cloudseal.rest.jaxb");
} catch (JAXBException ex) {
throw new RestException(ex);
}
}
/**
* Construct a new instance using the defaults: Connection pool size of 10,
* GZIP compression enabled
*
* @param hostname
* Cloudseal hostname e.g. acme
* @param accessKey
* Access key (you can find this in your Cloudseal admin console)
* @param secret
* Secret (you can find this in your Cloudseal admin console)
*/
public RESTClientImpl(String hostname, String accessKey, String secret) {
this(hostname, accessKey, secret, 10, true);
}
/**
* Make a GET request for an object. This method will return a null object
* in the event of a 404 error on the server end.
*
* @throws java.io.IOException
* In the event of an underlying connection problem
* @throws org.apache.http.client.ClientProtocolException
* In the event of a 500 error on the server
* @throws javax.xml.bind.JAXBException
* In the event that the HTTP response cannot be unmarshalled
*
* @see RESTClient#get(String)
*/
@Override
@SuppressWarnings("unchecked")
public <T> T get(String path) {
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Invoking GET " + buildUrl(path));
}
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
ResponseHandler<String> responseHandler = new LoggingResponseHandlerDecorator();
HttpGet get = new HttpGet(buildUrl(path));
addHeaders(get);
HttpResponse response = httpClient.execute(get);
logResponse(response);
String responseBody = responseHandler.handleResponse(response);
if (response.getStatusLine().getStatusCode() == 404) {
return null;
}
T object = (T) unmarshaller
.unmarshal(new StringReader(responseBody));
return object;
} catch (ClientProtocolException ex) {
if (ex.getMessage().equals("USER_NOT_FOUND")) {
return null;
} else {
throw new RestException(ex);
}
} catch (IOException ex) {
throw new RestException(ex);
} catch (JAXBException ex) {
throw new RestException(ex);
}
finally {
}
}
/**
* Make a POST request
*
* @throws java.io.IOException
* In the event of an underlying connection problem
* @throws org.apache.http.client.ClientProtocolException
* In the event of a 500 error on the server
* @throws javax.xml.bind.JAXBException
* In the event that the HTTP request or response cannot be
* marshalled/unmarshalled
*
* @see RESTClient#post
*/
@Override
@SuppressWarnings("unchecked")
public <T> T post(String path, T body) {
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Invoking POST " + buildUrl(path));
}
Marshaller marshaller = jaxbContext.createMarshaller();
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
StringWriter writer = new StringWriter();
marshaller.marshal(body, writer);
String xmlRequest = writer.toString();
ResponseHandler<String> responseHandler = new LoggingResponseHandlerDecorator();
HttpPost post = new HttpPost(buildUrl(path));
addHeaders(post);
post.setEntity(new StringEntity(xmlRequest));
HttpResponse response = httpClient.execute(post);
logResponse(response);
String responseBody = responseHandler.handleResponse(response);
T object = (T) unmarshaller
.unmarshal(new StringReader(responseBody));
return object;
} catch (ClientProtocolException ex) {
throw new RestException(ex);
} catch (IOException ex) {
throw new RestException(ex);
} catch (JAXBException ex) {
throw new RestException(ex);
}
}
/**
* Make a PUT request. This method will throw a wrapped
* ClientProtocolException if the server returns a 404 error.
*
* @throws java.io.IOException
* In the event of an underlying connection problem
* @throws org.apache.http.client.ClientProtocolException
* In the event of a 500 error on the server
* @throws javax.xml.bind.JAXBException
* In the event that the HTTP response cannot be unmarshalled
*
* @see RESTClient#put
*/
@Override
@SuppressWarnings("unchecked")
public <T> T put(String path, T body) {
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Invoking PUT " + buildUrl(path));
}
Marshaller marshaller = jaxbContext.createMarshaller();
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
StringWriter writer = new StringWriter();
marshaller.marshal(body, writer);
String xmlRequest = writer.toString();
ResponseHandler<String> responseHandler = new LoggingResponseHandlerDecorator();
HttpPut put = new HttpPut(buildUrl(path));
addHeaders(put);
put.setEntity(new StringEntity(xmlRequest));
HttpResponse response = httpClient.execute(put);
logResponse(response);
String responseBody = responseHandler.handleResponse(response);
T object = (T) unmarshaller
.unmarshal(new StringReader(responseBody));
return object;
} catch (ClientProtocolException ex) {
throw new RestException(ex);
} catch (IOException ex) {
throw new RestException(ex);
} catch (JAXBException ex) {
throw new RestException(ex);
}
}
/**
* Make a DELETE request. This object will throw a wrapped
* ClientProtocolException if the server returns a 404 error.
*
* @throws java.io.IOException
* In the event of an underlying connection problem
* @throws org.apache.http.client.ClientProtocolException
* In the event of a 500 error on the server
* @throws javax.xml.bind.JAXBException
* In the event that the HTTP response cannot be unmarshalled
*
* @see RESTClient#delete(String)
*/
@Override
@SuppressWarnings("unchecked")
public <T> T delete(String path) {
try {
if (LOG.isDebugEnabled()) {
LOG.debug("Invoking DELETE " + buildUrl(path));
}
Marshaller marshaller = jaxbContext.createMarshaller();
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
ResponseHandler<String> responseHandler = new LoggingResponseHandlerDecorator();
HttpDelete delete = new HttpDelete(buildUrl(path));
addHeaders(delete);
HttpResponse response = httpClient.execute(delete);
logResponse(response);
String responseBody = responseHandler.handleResponse(response);
T object = (T) unmarshaller
.unmarshal(new StringReader(responseBody));
return object;
} catch (ClientProtocolException ex) {
throw new RestException(ex);
} catch (IOException ex) {
throw new RestException(ex);
} catch (JAXBException ex) {
throw new RestException(ex);
}
}
/**
* Shutdown the connection manager. This method must be called to ensure the
* underlying resources are released.
*
*/
public void destroy() {
if (cm != null) {
cm.shutdown();
}
}
private String buildUrl(String path) {
StringBuilder builder = new StringBuilder();
builder.append("https://");
builder.append(hostname);
builder.append(".cloudseal.com");
builder.append(path);
return builder.toString();
}
private void addHeaders(HttpRequestBase httpRequest) {
httpRequest.setHeader("X-Access-Key", this.accessKey);
httpRequest.setHeader("X-Secret", this.secret);
httpRequest.setHeader("Host", hostname + ".cloudseal.com");
httpRequest.setHeader("Accept", "application/xml");
httpRequest.setHeader("Content-Type", "application/xml");
}
private void logResponse(HttpResponse response)
throws HttpResponseException, IOException {
if (LOG.isDebugEnabled()) {
LOG.debug("Received response " + response.getStatusLine());
}
}
private void addGzipInterceptors(DefaultHttpClient httpClient) {
httpClient.addRequestInterceptor(new GzipRequestInterceptor());
httpClient.addResponseInterceptor(new GzipResponseInterceptor());
}
private class LoggingResponseHandlerDecorator implements
ResponseHandler<String> {
private BasicResponseHandler delegate = new BasicResponseHandler();
@Override
public String handleResponse(HttpResponse response)
throws ClientProtocolException, IOException {
if (LOG.isTraceEnabled()) {
for (Header header : response.getAllHeaders()) {
LOG.trace("HTTP Response " + header.toString());
}
}
String responseString = delegate.handleResponse(response);
if (LOG.isTraceEnabled()) {
LOG.debug("HTTP Response: \n" + responseString);
}
return responseString;
}
}
private class GzipRequestInterceptor implements HttpRequestInterceptor {
@Override
public void process(final HttpRequest request, final HttpContext context)
throws HttpException, IOException {
if (!request.containsHeader("Accept-Encoding")) {
request.addHeader("Accept-Encoding", "gzip");
}
}
}
private class GzipResponseInterceptor implements HttpResponseInterceptor {
@Override
public void process(HttpResponse response, HttpContext context)
throws HttpException, IOException {
HttpEntity entity = response.getEntity();
Header ceheader = entity.getContentEncoding();
if (ceheader != null) {
HeaderElement[] codecs = ceheader.getElements();
for (int i = 0; i < codecs.length; i++) {
if (codecs[i].getName().equalsIgnoreCase("gzip")) {
response.setEntity(new GzipDecompressingEntity(response
.getEntity()));
return;
}
}
}
}
}
}