/*
* Copyright (C) 2011 lightcouch.org
*
* 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.lightcouch;
import java.io.Closeable;
import java.io.IOException;
import java.net.URLDecoder;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import org.apache.http.Consts;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.RequestLine;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.HttpClientUtils;
import org.apache.http.config.ConnectionConfig;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
/**
* Presents a <i>client</i> to CouchDB database server.
* <p>This class is the main object to use to gain access to the APIs.
* <h3>Usage Example:</h3>
* <p>Create a new client instance:
* <pre>
* CouchDbClient dbClient = new CouchDbClient();
* </pre>
*
* <p>Start using the API by the client:
*
* <p>Documents <code>CRUD</code> APIs is accessed by the client directly, eg.: {@link CouchDbClientBase#find(Class, String) dbClient.find(Foo.class, "doc-id")}
* <p>View APIs {@link View dbClient.view()}
* <p>Change Notifications {@link Changes dbClient.changes()}
* <p>Replication {@link Replication dbClient.replication()} and {@link Replicator dbClient.replicator()}
* <p>DB server {@link CouchDbContext dbClient.context()}
* <p>Design documents {@link CouchDbDesign dbClient.design()}
*
* <p>At the end of a client usage; it's useful to call: {@link #shutdown()} to ensure proper release of resources.
*
* @see CouchDbClientAndroid
* @since 0.0.2
* @author Ahmed Yehia
*
*/
public class CouchDbClient extends CouchDbClientBase implements Closeable {
/**
* Constructs a new instance of this class, expects a configuration file named
* <code>couchdb.properties</code> to be available in your application default classpath.
*/
public CouchDbClient() {
super();
}
/**
* Constructs a new instance of this class.
* @param configFileName The configuration file name.
*/
public CouchDbClient(String configFileName) {
super(new CouchDbConfig(configFileName));
}
/**
* Constructs a new instance of this class.
* @param dbName The database name.
* @param createDbIfNotExist To create a new database if it does not already exist.
* @param protocol The protocol to use (i.e http or https)
* @param host The database host address
* @param port The database listening port
* @param username The Username credential
* @param password The Password credential
*/
public CouchDbClient(String dbName, boolean createDbIfNotExist,
String protocol, String host, int port, String username, String password) {
super(new CouchDbConfig(new CouchDbProperties(dbName, createDbIfNotExist, protocol, host, port, username, password)));
}
/**
* Constructs a new instance of this class.
* @param properties An object containing configuration properties.
* @see {@link CouchDbProperties}
*/
public CouchDbClient(CouchDbProperties properties) {
super(new CouchDbConfig(properties));
}
/**
* @return {@link CloseableHttpClient} instance.
*/
@Override
HttpClient createHttpClient(CouchDbProperties props) {
try {
Registry<ConnectionSocketFactory> registry = createRegistry(props);
PoolingHttpClientConnectionManager ccm = createConnectionManager(props, registry);
HttpClientBuilder clientBuilder = HttpClients.custom()
.setConnectionManager(ccm)
.setDefaultConnectionConfig(ConnectionConfig.custom()
.setCharset(Consts.UTF_8).build())
.setDefaultRequestConfig(RequestConfig.custom()
.setSocketTimeout(props.getSocketTimeout())
.setConnectTimeout(props.getConnectionTimeout()).build());
if (props.getProxyHost() != null)
clientBuilder.setProxy(new HttpHost(props.getProxyHost(), props.getProxyPort()));
if (props.getUsername() != null) {
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(new AuthScope(props.getHost(),
props.getPort()),
new UsernamePasswordCredentials(props.getUsername(),
props.getPassword()));
clientBuilder.setDefaultCredentialsProvider(credsProvider);
props.clearPassword();
}
registerInterceptors(clientBuilder);
return clientBuilder.build();
} catch (Exception e) {
throw new IllegalStateException("Error Creating HTTPClient: ", e);
}
}
@Override
HttpContext createContext() {
AuthCache authCache = new BasicAuthCache();
authCache.put(host, new BasicScheme());
HttpContext context = new BasicHttpContext();
context.setAttribute(HttpClientContext.AUTH_CACHE, authCache);
return context;
}
private PoolingHttpClientConnectionManager createConnectionManager(
CouchDbProperties props, Registry<ConnectionSocketFactory> registry) {
PoolingHttpClientConnectionManager ccm = new PoolingHttpClientConnectionManager(registry);
if (props.getMaxConnections() != 0) {
ccm.setMaxTotal(props.getMaxConnections());
ccm.setDefaultMaxPerRoute(props.getMaxConnections());
}
return ccm;
}
private Registry<ConnectionSocketFactory> createRegistry(CouchDbProperties props) throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {
RegistryBuilder<ConnectionSocketFactory> registry = RegistryBuilder
.<ConnectionSocketFactory> create();
if("https".equals(props.getProtocol())) {
SSLContext sslcontext = SSLContexts.custom()
.loadTrustMaterial(null, new TrustStrategy(){
public boolean isTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
return true;
}
}).build();
return registry.register("https", new SSLConnectionSocketFactory(sslcontext,
new NoopHostnameVerifier())).build();
} else {
return registry.register("http", PlainConnectionSocketFactory.INSTANCE).build();
}
}
/**
* Adds request/response interceptors for logging and validation.
* @param clientBuilder
*/
private void registerInterceptors(HttpClientBuilder clientBuilder) {
clientBuilder.addInterceptorFirst(new HttpRequestInterceptor() {
public void process(final HttpRequest request,
final HttpContext context) throws IOException {
if (log.isInfoEnabled()) {
RequestLine req = request.getRequestLine();
log.info("> " + req.getMethod() + " " + URLDecoder.decode(req.getUri(), "UTF-8"));
}
}
});
clientBuilder.addInterceptorFirst(new HttpResponseInterceptor() {
public void process(final HttpResponse response,
final HttpContext context) throws IOException {
if (log.isInfoEnabled()) {
log.info("< Status: " + response.getStatusLine().getStatusCode());
}
validate(response);
}
});
}
public void shutdown() {
HttpClientUtils.closeQuietly(this.httpClient);
}
@Override
public void close() throws IOException {
shutdown();
}
}