package org.arangodb.objectmapper.http;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPatch;
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.client.methods.HttpUriRequest;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.params.ConnRoutePNames;
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.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DecompressingHttpClient;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.RequestExpectContinue;
import org.arangodb.objectmapper.ArangoDb4JException;
import org.apache.log4j.Logger;
/**
* Original file from "Java API for CouchDB http://www.ektorp.org"
*
* @author henrik lundgren
*
*/
public class ArangoDbHttpClient {
/**
* http default character set
*/
private static final String DEFAULT_CHARSET = "UTF-8";
/**
* http mime type
*/
private static final String MIME_TYPE_JSON = "application/json";
/**
* apache http client
*/
private final org.apache.http.client.HttpClient client;
/**
* the logger
*/
private final static Logger LOG = Logger.getLogger(ArangoDbHttpClient.class);
private String apiCompatibility;
/**
* constructor
*/
public ArangoDbHttpClient(org.apache.http.client.HttpClient hc) {
this.client = hc;
}
public void setApiCompatibility(String s) {
apiCompatibility = s;
}
public void setApiCompatibility(int s) {
apiCompatibility = String.valueOf(s);
}
public ArangoDbHttpResponse post(String uri, String content) throws ArangoDb4JException {
return executeWithBody(new HttpPost(uri), content);
}
public ArangoDbHttpResponse post(String uri, InputStream content) throws ArangoDb4JException {
InputStreamEntity e = new InputStreamEntity(content, -1);
e.setContentType(MIME_TYPE_JSON);
HttpPost post = new HttpPost(uri);
post.setEntity(e);
return executeRequest(post);
}
public ArangoDbHttpResponse patch(String uri, String content) throws ArangoDb4JException {
return executeWithBody(new HttpPatch(uri), content);
}
public ArangoDbHttpResponse patch(String uri, InputStream content) throws ArangoDb4JException {
InputStreamEntity e = new InputStreamEntity(content, -1);
e.setContentType(MIME_TYPE_JSON);
HttpPatch patch = new HttpPatch(uri);
patch.setEntity(e);
return executeRequest(patch);
}
public ArangoDbHttpResponse get(String uri) throws ArangoDb4JException {
return executeRequest(new HttpGet(uri));
}
public ArangoDbHttpResponse get(String uri, Map<String, String> headers) throws ArangoDb4JException {
return executeRequest(new HttpGet(uri), headers);
}
public ArangoDbHttpResponse delete(String uri) throws ArangoDb4JException {
return executeRequest(new HttpDelete(uri));
}
public ArangoDbHttpResponse put(String uri, String content) throws ArangoDb4JException {
return executeWithBody(new HttpPut(uri), content);
}
public ArangoDbHttpResponse put(String uri) throws ArangoDb4JException {
return executeRequest(new HttpPut(uri));
}
public ArangoDbHttpResponse put(String uri, InputStream data, String contentType,
long contentLength) throws ArangoDb4JException {
InputStreamEntity e = new InputStreamEntity(data, contentLength);
e.setContentType(contentType);
HttpPut hp = new HttpPut(uri);
hp.setEntity(e);
return executeRequest(hp);
}
public ArangoDbHttpResponse head(String uri) throws ArangoDb4JException {
return executeRequest(new HttpHead(uri));
}
private ArangoDbHttpResponse executeWithBody(
HttpEntityEnclosingRequestBase request, String content) throws ArangoDb4JException {
LOG.debug("Request-body: " + content);
try {
request.setEntity(new StringEntity(content, DEFAULT_CHARSET));
} catch (UnsupportedEncodingException e) {
LOG.error(DEFAULT_CHARSET + " is not supported");
}
request.setHeader(new BasicHeader("Content-Type", MIME_TYPE_JSON));
return executeRequest(request);
}
private ArangoDbHttpResponse executeRequest(HttpRequestBase request, Map<String, String> headers) throws ArangoDb4JException {
for(Map.Entry<String, String> header : headers.entrySet()) {
request.setHeader(header.getKey(), header.getValue());
}
return executeRequest(request);
}
private ArangoDbHttpResponse executeRequest(HttpUriRequest request) throws ArangoDb4JException {
org.apache.http.HttpResponse rsp;
try {
if (apiCompatibility != null) {
request.setHeader("X-Arango-Version", apiCompatibility);
}
rsp = client.execute((HttpHost)client.getParams().getParameter(ClientPNames.DEFAULT_HOST), request);
} catch (ClientProtocolException e) {
e.printStackTrace();
throw new ArangoDb4JException(e.getMessage());
} catch (IOException e) {
e.printStackTrace();
throw new ArangoDb4JException(e.getMessage());
}
LOG.debug("Request:" + request.getMethod() + " " + request.getURI() + " -> " +
rsp.getStatusLine().getStatusCode() + " " + rsp.getStatusLine().getReasonPhrase());
return ArangoDbHttpResponse.of(rsp, request);
}
public void shutdown() {
client.getConnectionManager().shutdown();
}
/**
* Helper class
*/
public static class Builder {
String host = "localhost";
int port = 8529;
int maxConnections = 20;
int connectionTimeout = 3000;
int socketTimeout = 10000;
ClientConnectionManager conman;
int proxyPort = -1;
String proxy = null;
boolean enableSSL = false;
boolean relaxedSSLSettings = false;
SSLSocketFactory sslSocketFactory;
String username;
String password;
boolean cleanupIdleConnections = true;
boolean useExpectContinue = false;
boolean staleConnectionCheck = false;
boolean caching = true;
boolean compression; // Default is false;
long keepAliveTimeout = 90;
int maxObjectSizeBytes = 8192;
int maxCacheEntries = 1000;
public Builder url(String s) throws MalformedURLException {
if (s == null) return this;
return this.url(new URL(s));
}
/**
* Will set host, port and possible enables SSL based on the properties if the supplied URL.
* This method overrides the properties: host, port and enableSSL.
* @param url
* @return the builder
*/
public Builder url(URL url){
this.host = url.getHost();
this.port = url.getPort();
if (url.getUserInfo() != null) {
String[] userInfoParts = url.getUserInfo().split(":");
if (userInfoParts.length == 2) {
this.username = userInfoParts[0];
this.password = userInfoParts[1];
}
}
enableSSL("https".equals(url.getProtocol()));
if (this.port == -1) {
if (this.enableSSL) {
this.port = 443;
} else {
this.port = 80;
}
}
return this;
}
public Builder host(String s) {
host = s;
return this;
}
public Builder proxyPort(int p) {
proxyPort = p;
return this;
}
public Builder proxy(String s) {
proxy = s;
return this;
}
/**
* Controls if the http client should send Accept-Encoding: gzip,deflate
* header and handle Content-Encoding responses. This enable compression
* on the server; although not supported natively by CouchDB, you can
* use a reverse proxy, such as nginx, in front of CouchDB to achieve
* this.
* <p>
* Disabled by default (for backward compatibility).
*
* @param b
* @return This builder
*/
public Builder compression(boolean b){
compression = b;
return this;
}
/**
* Controls if the http client should cache response entities.
* Default is true.
* @param b
* @return the builder
*/
public Builder caching(boolean b) {
caching = b;
return this;
}
public Builder maxCacheEntries(int m) {
maxCacheEntries = m;
return this;
}
public Builder maxObjectSizeBytes(int m) {
maxObjectSizeBytes = m;
return this;
}
public ClientConnectionManager configureConnectionManager(
HttpParams params) throws ArangoDb4JException {
if (conman == null) {
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(configureScheme());
PoolingClientConnectionManager cm = new PoolingClientConnectionManager(schemeRegistry);
cm.setMaxTotal(maxConnections);
cm.setDefaultMaxPerRoute(maxConnections);
conman = cm;
}
if (cleanupIdleConnections) {
IdleConnectionMonitor.monitor(conman);
}
return conman;
}
private Scheme configureScheme() throws ArangoDb4JException {
if (enableSSL) {
try {
if (sslSocketFactory == null) {
SSLContext context = SSLContext.getInstance("TLS");
if (relaxedSSLSettings) {
context.init(
null,
new TrustManager[] { new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(
java.security.cert.X509Certificate[] certs,
String authType) {
}
public void checkServerTrusted(
java.security.cert.X509Certificate[] certs,
String authType) {
}
} }, null);
} else {
context.init(null, null, null);
}
sslSocketFactory = relaxedSSLSettings ? new SSLSocketFactory(context, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) : new SSLSocketFactory(context);
}
return new Scheme("https", port, sslSocketFactory);
} catch (Exception e) {
throw new ArangoDb4JException(e.getMessage());
}
} else {
return new Scheme("http", port, PlainSocketFactory.getSocketFactory());
}
}
public org.apache.http.client.HttpClient configureClient() throws ArangoDb4JException {
HttpParams params = new BasicHttpParams();
params.setParameter(HttpConnectionParams.STALE_CONNECTION_CHECK, staleConnectionCheck);
params.setParameter(HttpProtocolParams.USE_EXPECT_CONTINUE, useExpectContinue);
params.setParameter(HttpConnectionParams.CONNECTION_TIMEOUT, connectionTimeout);
params.setParameter(HttpConnectionParams.SO_TIMEOUT, socketTimeout);
params.setParameter(HttpConnectionParams.TCP_NODELAY, true);
params.setParameter(HttpConnectionParams.SO_KEEPALIVE, false); // keep-alive on TCP level
ConnectionKeepAliveStrategy customKeepAliveStrategy = new ConnectionKeepAliveStrategy() {
public long getKeepAliveDuration(org.apache.http.HttpResponse response, org.apache.http.protocol.HttpContext context) {
return keepAliveTimeout * 1000;
}
};
String protocol = "http";
if (enableSSL) {
protocol = "https";
}
params.setParameter(ClientPNames.DEFAULT_HOST, new HttpHost(host, port, protocol));
if (proxy != null) {
params.setParameter(ConnRoutePNames.DEFAULT_PROXY,
new HttpHost(proxy, proxyPort, protocol));
}
ClientConnectionManager connectionManager = configureConnectionManager(params);
DefaultHttpClient client = new DefaultHttpClient(connectionManager, params);
if (username != null && password != null) {
client.getCredentialsProvider().setCredentials(
new AuthScope(host, port, AuthScope.ANY_REALM),
new UsernamePasswordCredentials(username, password));
client.addRequestInterceptor(
new PreemptiveAuthRequestInterceptor(), 0);
}
client.setKeepAliveStrategy(customKeepAliveStrategy);
if (compression) {
return new DecompressingHttpClient(client);
}
return client;
}
public Builder port(int i) {
port = i;
return this;
}
public Builder username(String s) {
username = s;
return this;
}
public Builder password(String s) {
password = s;
return this;
}
public Builder maxConnections(int i) {
maxConnections = i;
return this;
}
public Builder connectionTimeout(int i) {
connectionTimeout = i;
return this;
}
public Builder socketTimeout(int i) {
socketTimeout = i;
return this;
}
/**
* If set to true, a monitor thread will be started that cleans up idle
* connections every 30 seconds.
*
* @param b
* @return the builder
*/
public Builder cleanupIdleConnections(boolean b) {
cleanupIdleConnections = b;
return this;
}
/**
* Bring your own Connection Manager. If this parameters is set, the
* parameters port, maxConnections, connectionTimeout and socketTimeout
* are ignored.
*
* @param cm
* @return the builder
*/
public Builder connectionManager(ClientConnectionManager cm) {
conman = cm;
return this;
}
/**
* Set to true in order to enable SSL sockets. Note that the CouchDB
* host must be accessible through a https:// path Default is false.
*
* @param b
* @return the builder
*/
public Builder enableSSL(boolean b) {
enableSSL = b;
return this;
}
/**
* Bring your own SSLSocketFactory. Note that schemeName must be also be
* configured to "https". Will override any setting of
* relaxedSSLSettings.
*
* @param f
* @return the builder
*/
public Builder sslSocketFactory(SSLSocketFactory f) {
sslSocketFactory = f;
return this;
}
/**
* If set to true all SSL certificates and hosts will be trusted. This
* might be handy during development. default is false.
*
* @param b
* @return the builder
*/
public Builder relaxedSSLSettings(boolean b) {
relaxedSSLSettings = b;
return this;
}
public Builder useExpectContinue(boolean b) {
useExpectContinue = b;
return this;
}
public Builder staleConnectionCheck(boolean b) {
staleConnectionCheck = b;
return this;
}
public Builder keepAliveTimeout(long t) {
keepAliveTimeout = t;
return this;
}
public ArangoDbHttpClient build() throws ArangoDb4JException {
org.apache.http.client.HttpClient client = configureClient();
return new ArangoDbHttpClient(client);
}
}
}