/*
* Copyright (C) 2011 Ahmed Yehia (ahmed.yehia.m@gmail.com)
*
* 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.
*/
/*
* This file has been modified for
*
* SMART FP7 - Search engine for MultimediA enviRonment generated contenT
* Webpage: http://smartfp7.eu
*
* The modifications are Copyright (c) 2012-2013 Athens Information Technology
* All Rights Reserved
*
* Contributors:
* Nikolaos Katsarakis nkat@ait.edu.gr
* Menelaos Bakopoulos mbak@ait.edu.gr
*
* Changes done:
* Try to URL encode the dbName in the constructor to support dbNames
* with forward slashes '/'
* Added putJsonText(URI uri, String jsonText) function
*
*/
package org.lightcouch;
import static java.lang.String.format;
import static org.lightcouch.CouchDbUtil.*;
import static org.lightcouch.URIBuilder.builder;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URLEncoder;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.HttpClient;
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.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.ClientConnectionManager;
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.scheme.SchemeSocketFactory;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import eu.smartfp7.utils.Json;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
/**
* Base client class to be extended by a concrete subclass, responsible for establishing
* a connection with the database and the definition of the basic HTTP request handling and validation.
* @see CouchDbClient
* @author Ahmed Yehia
*/
@SuppressWarnings("deprecation")
abstract class CouchDbClientBase {
private static final Log log = LogFactory.getLog(CouchDbClientBase.class);
private HttpClient httpClient;
private URI baseURI;
private URI dbURI;
private Gson gson;
private CouchDbConfig config;
private HttpHost host;
private BasicHttpContext context;
protected CouchDbClientBase() {
this(new CouchDbConfig());
}
protected CouchDbClientBase(CouchDbConfig config) {
this.httpClient = createHttpClient(config);
this.gson = initGson(new GsonBuilder());
this.config = config;
baseURI = builder().scheme(config.getProtocol()).host(config.getHost()).port(config.getPort()).path("/").build();
String encodedName=config.getDbName();
try {
encodedName=URLEncoder.encode(config.getDbName(),"UTF-8");
} catch (UnsupportedEncodingException e) {
}
dbURI = builder(baseURI).path(encodedName).path("/").build();
}
// ---------------------------------------------- Getters
/**
* @return The database URI.
*/
protected URI getDBUri() {
return dbURI;
}
protected URI getBaseUri() {
return baseURI;
}
protected Gson getGson() {
return gson;
}
protected CouchDbConfig getConfig() {
return config;
}
// ------------------------------------------------- HTTP Requests
/**
* Performs a HTTP GET request.
* @return {@link InputStream}
*/
InputStream get(HttpGet httpGet) {
HttpResponse response = executeRequest(httpGet);
try {
return response.getEntity().getContent();
} catch (Exception e) {
log.error("Error reading response. " + e.getMessage());
throw new CouchDbException(e);
}
}
/**
* Performs a HTTP GET request.
* @return {@link InputStream}
*/
InputStream get(URI uri) {
return get(new HttpGet(uri));
}
/**
* Performs a HTTP GET request.
* @return An object of type T
*/
<T> T get(URI uri, Class<T> classType) {
InputStream instream = null;
try {
Reader reader = new InputStreamReader(instream = get(uri));
return getGson().fromJson(reader, classType);
} finally {
close(instream);
}
}
/**
* Performs a HTTP HEAD request.
* @return {@link HttpResponse}
*/
HttpResponse head(URI uri) {
return executeRequest(new HttpHead(uri));
}
/**
* Performs a HTTP PUT request, saves or updates a document.
* @return {@link Response}
*/
Response put(URI uri, Object object, boolean newEntity) {
assertNotEmpty(object, "object");
HttpResponse response = null;
try {
JsonObject json = objectToJson(getGson(), object);
String id = getElement(json, "_id");
String rev = getElement(json, "_rev");
if(newEntity) { // save
assertNull(rev, "revision");
id = (id == null) ? generateUUID() : id;
} else { // update
assertNotEmpty(id, "id");
assertNotEmpty(rev, "revision");
}
HttpPut put = new HttpPut(builder(uri).path(id).build());
setEntity(put, json.toString());
response = executeRequest(put);
return getResponse(response);
} finally {
close(response);
}
}
/**
* Performs a HTTP PUT request, saves an attachment.
* @return {@link Response}
*/
Response put(URI uri, InputStream instream, String contentType) {
HttpResponse response = null;
try {
HttpPut httpPut = new HttpPut(uri);
InputStreamEntity entity = new InputStreamEntity(instream, -1);
entity.setContentType(contentType);
httpPut.setEntity(entity);
response = executeRequest(httpPut);
return getResponse(response);
} finally {
close(response);
}
}
/**
* Performs a HTTP PUT save request to add a new document based on JSON text String.
* @return {@link Response}
*/
Response putJsonText(URI uri, String jsonText) {
assertNotEmpty(jsonText, "jsonText");
HttpResponse response = null;
try {
String id = Json.getSimpleTextKey(jsonText, "_id");
if (id==null)
id = generateUUID();
HttpPut put = new HttpPut(builder(uri).path(id).build());
setEntity(put, jsonText);
response = executeRequest(put);
return getResponse(response);
} finally {
close(response);
}
}
/**
* Performs a HTTP POST request.
* @return {@link HttpResponse}
*/
HttpResponse post(URI uri, String json) {
HttpPost post = new HttpPost(uri);
setEntity(post, json);
return executeRequest(post);
}
/**
* Performs a HTTP DELETE request.
* @return {@link Response}
*/
Response delete(URI uri) {
HttpResponse response = null;
try {
HttpDelete delete = new HttpDelete(uri);
response = executeRequest(delete);
return getResponse(response);
} finally {
close(response);
}
}
/**
* Executes a HTTP request.
* @param request The HTTP request to execute.
* @return {@link HttpResponse}
*/
protected HttpResponse executeRequest(HttpRequestBase request) {
HttpResponse response = null;
try {
response = httpClient.execute(host, request, context);
} catch (IOException e) {
request.abort();
log.error("Error executing request. " + e.getMessage());
throw new CouchDbException(e);
}
return response;
}
// ----------------------------------------------- Helpers
/**
* @return {@link DefaultHttpClient} instance.
*/
private HttpClient createHttpClient(CouchDbConfig cf) {
DefaultHttpClient httpclient = null;
try {
SchemeSocketFactory ssf = null;
if(cf.getProtocol().equals("https")) {
TrustManager trustManager = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain,
String authType) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
SSLContext sslcontext = SSLContext.getInstance("TLS");
sslcontext.init(null, new TrustManager[] { trustManager }, null);
ssf = new SSLSocketFactory(sslcontext, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
SSLSocket socket = (SSLSocket) ssf.createSocket(null);
socket.setEnabledCipherSuites(new String[] { "SSL_RSA_WITH_RC4_128_MD5" });
} else {
ssf = PlainSocketFactory.getSocketFactory();
}
SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme(cf.getProtocol(), cf.getPort(), ssf));
ClientConnectionManager ccm = new ThreadSafeClientConnManager(schemeRegistry);
httpclient = new DefaultHttpClient(ccm);
host = new HttpHost(cf.getHost(), cf.getPort(), cf.getProtocol());
context = new BasicHttpContext();
// Http params
httpclient.getParams().setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET, "UTF-8");
httpclient.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT, cf.getSocketTimeout());
httpclient.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, cf.getConnectionTimeout());
// basic authentication
if(cf.getUsername() != null && cf.getPassword() != null) {
httpclient.getCredentialsProvider().setCredentials(
new AuthScope(cf.getHost(), cf.getPort()),
new UsernamePasswordCredentials(cf.getUsername(), cf.getPassword()));
cf.resetPassword();
AuthCache authCache = new BasicAuthCache();
BasicScheme basicAuth = new BasicScheme();
authCache.put(host, basicAuth);
context.setAttribute(ClientContext.AUTH_CACHE, authCache);
}
// request interceptor
httpclient.addRequestInterceptor(new HttpRequestInterceptor() {
public void process(
final HttpRequest request,
final HttpContext context) throws IOException {
if(log.isInfoEnabled())
log.info(">> " + request.getRequestLine());
}
});
// response interceptor
httpclient.addResponseInterceptor(new HttpResponseInterceptor() {
public void process(
final HttpResponse response,
final HttpContext context) throws IOException {
validate(response);
if(log.isInfoEnabled())
log.info("<< Status: " + response.getStatusLine().getStatusCode());
}
});
} catch (Exception e) {
log.error("Error Creating HTTP client. " + e.getMessage());
throw new IllegalStateException(e);
}
return httpclient;
}
/**
* Validates a HTTP response; on error cases logs status and throws relevant exceptions.
* @param response The HTTP response.
*/
private void validate(HttpResponse response) throws IOException {
int code = response.getStatusLine().getStatusCode();
if(code == 200 || code == 201 || code == 202) { // success (ok | created | accepted)
return;
}
String msg = format("<< Status: %s (%s) ", code, response.getStatusLine().getReasonPhrase());
switch (code) {
case HttpStatus.SC_NOT_FOUND: {
log.info(msg);
throw new NoDocumentException(msg);
}
case HttpStatus.SC_CONFLICT: {
log.warn(msg);
throw new DocumentConflictException(msg);
}
default: { // other errors: 400 | 401 | 500 etc.
log.error(msg += EntityUtils.toString(response.getEntity()));
throw new CouchDbException(msg);
}
}
}
/**
* @param response The {@link HttpResponse}
* @return {@link Response}
*/
Response getResponse(HttpResponse response) throws CouchDbException {
try {
Reader reader = new InputStreamReader(response.getEntity().getContent());
return getGson().fromJson(reader, Response.class);
} catch (Exception e) {
log.error("Error reading response. " + e.getMessage());
throw new CouchDbException(e.getMessage(), e);
}
}
/**
* Sets a JSON String as a request entity.
* @param httpRequest The request to set entity.
* @param json The JSON String to set.
*/
protected void setEntity(HttpEntityEnclosingRequestBase httpRequest, String json) {
try {
StringEntity entity = new StringEntity(json, "UTF-8");
entity.setContentType("application/json");
httpRequest.setEntity(entity);
} catch (UnsupportedEncodingException e) {
log.error("Error setting request data. " + e.getMessage());
throw new IllegalArgumentException(e);
}
}
/**
* <p>The supplied {@link GsonBuilder} is used to create a new {@link Gson} instance.
* Useful for registering custom serializers/deserializers, for example JodaTime DateTime class.
*/
protected void setGsonBuilder(GsonBuilder gsonBuilder) {
this.gson = initGson(gsonBuilder);
}
/**
* Builds {@link Gson} and registers any required serializer/deserializer.
* @return {@link Gson} instance
*/
private Gson initGson(GsonBuilder gsonBuilder) {
gsonBuilder.registerTypeAdapter(JsonObject.class, new JsonDeserializer<JsonObject>() {
public JsonObject deserialize(JsonElement json,
Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
return json.getAsJsonObject();
}
});
return gsonBuilder.create();
}
/**
* Shuts down the connection manager used by this client instance.
*/
protected void shutdown() {
this.httpClient.getConnectionManager().shutdown();
}
}