/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2011, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.data.couchdb.client;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpConnectionManager;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.URI;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.geotools.util.logging.Logging;
import org.json.simple.JSONArray;
/**
* A CouchDBClient is the entry point to interacting with a CouchDB instance.
*
* The goal of the client is to abstract much of the HTTP client interaction
* as possible to allow higher level use and hide the details of the client.
*
* The lower level HTTP public APIs should not be used except by internal code.
*
* All 'path' parameters, unless otherwise specified, are relative to the
* root URL.
*
* All 'IOException's thrown in the client are due to HTTP errors, CouchDB
* specific errors are covered by CouchDBException.
*
* The Client does not perform any checking of CouchDBResponses for errors. All
* methods that return CouchDBResponses will never return null.
*
* A Client instance should always be closed when done to release pooled
* connections.
*
* Notes:
* * Using older httpclient 3.1, could upgrade to 4.x, but what impact on other
* libraries
*
* @todo thread safety requirements?
* @author Ian Schneider (OpenGeo)
*/
public class CouchDBClient implements Closeable {
private static final String DEFAULT_CHARSET = "UTF-8";
private static final String MIME_TYPE_JSON = "application/json";
private final URI root;
private final MultiThreadedHttpConnectionManager manager;
private final HttpClientParams clientParams;
private static final Logger logger = Logging.getLogger(CouchDBClient.class);
/**
* Create a new client that will connect to a CouchDB instance at the
* specified root URL
* @param root URL to instance, for example: "http://127.0.0.1:5984/"
* @throws URIException if provided URL is invalid
*/
public CouchDBClient(String root) throws URIException {
this.root = new URI(root, false);
clientParams = new HttpClientParams();
clientParams.setParameter(HttpClientParams.USER_AGENT, "gtcouchclient");
manager = new MultiThreadedHttpConnectionManager();
}
/**
* Get a list of all database names on the instance.
* @return non-null List of database names
* @throws IOException if an error in communication occurs
*/
public List<String> getDatabaseNames() throws IOException {
CouchDBResponse response = get("_all_dbs");
JSONArray array = response.getBodyAsJSONArray();
if (array.size() > 0) {
assert array.get(0).getClass() == String.class;
}
return (List<String>) array;
}
/**
* Open a connection to an existing database.
* @param name The name of the database to connect to
* @return the database
* @throws CouchDBException If there is a couch specific error (db doesn't exist)
* @throws IOException if an error in communication occurs
*/
public CouchDBConnection openDBConnection(String name) throws CouchDBException, IOException {
CouchDBResponse resp = get(name);
resp.checkOK("Unable to open DB '" + name + "'");
return new CouchDBConnection(resp, this);
}
/**
* Create a new database with the given name
* @param name The name of the database
* @return the new database connection
* @throws CouchDBException If there is a couch specific error (db exists or name is invalid)
* @throws IOException if an error in communication occurs
*/
public CouchDBConnection createDB(String name) throws CouchDBException, IOException {
// @todo check db name compliance as per wiki:
//A database must be named with all lowercase letters (a-z), digits (0-9),
//or any of the _$()+-/ characters and must end with a slash in the URL.
//The name has to start with a lowercase letter (a-z).
//Uppercase characters are NOT ALLOWED in database names
CouchDBResponse resp = put(name);
resp.checkOK("Unable to create DB");
return openDBConnection(name);
}
/**
* Send a DELETE request to the given relative path
* @param path relative path
* @return CouchDBResponse
* @throws IOException if an error in communication occurs
*/
public CouchDBResponse delete(String path) throws IOException {
DeleteMethod delete = new DeleteMethod(url(path));
return executeMethod(delete);
}
/**
* Send a POST request with body to the given relative path
* @param path relative path
* @param content
* @return CouchDBResponse
* @throws IOException
*/
public CouchDBResponse post(String path, String content) throws IOException {
PostMethod put = new PostMethod(url(path));
try {
put.setRequestEntity(new StringRequestEntity(content, MIME_TYPE_JSON, DEFAULT_CHARSET));
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException(ex);
}
return executeMethod(put);
}
/**
* Send a streaming POST request with body to the given relative path
* @param path relative path
* @param content
* @return CouchDBResponse
* @throws IOException
*/
public CouchDBResponse post(String path, RequestEntity content) throws IOException {
PostMethod post = new PostMethod(url(path));
post.setRequestEntity(content);
return executeMethod(post);
}
/**
*
* @param path
* @param content
* @return
* @throws IOException
*/
public CouchDBResponse post(String path, File content) throws IOException {
return post(path, CouchDBUtils.read(content));
}
/**
* Send a PUT (for creating databases)
* @param path
* @return
* @throws IOException
*/
public CouchDBResponse put(String path) throws IOException {
return put(path, (String) null);
}
/**
* Send a PUT with a body (for creating documents)
* @param path
* @param content
* @return
* @throws IOException
*/
public CouchDBResponse put(String path, String content) throws IOException {
PutMethod put = new PutMethod(url(path));
if (content != null) {
try {
put.setRequestEntity(new StringRequestEntity(content, MIME_TYPE_JSON, DEFAULT_CHARSET));
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException(ex);
}
}
return executeMethod(put);
}
/**
*
* @param path
* @param content
* @return
* @throws IOException
*/
public CouchDBResponse put(String path, File content) throws IOException {
return put(path,CouchDBUtils.read(content));
}
/**
*
* @param path
* @return
* @throws IOException
*/
public CouchDBResponse get(String path) throws IOException {
return get(path,null);
}
/**
*
* @param path
* @param queryParams
* @return
* @throws IOException
*/
public CouchDBResponse get(String path, NameValuePair[] queryParams) throws IOException {
HttpMethod get = new GetMethod(url(path));
if (queryParams != null) {
get.setQueryString(queryParams);
}
return executeMethod(get);
}
/**
*
* @param method
* @return
* @throws IOException
*/
private CouchDBResponse executeMethod(HttpMethod method) throws IOException {
IOException expected = null;
int result = -1;
HttpClient client = new HttpClient(clientParams,manager);
try {
result = client.executeMethod(method);
} catch (IOException ex) {
expected = ex;
}
CouchDBResponse resp;
try {
resp = new CouchDBResponse(method, result, expected);
} finally {
// @revisit if method doesn't read contents upfront
method.releaseConnection();
}
if (logger.isLoggable(Level.FINEST)) {
logger.finest("Request to : " + method.getPath());
logger.finest("Response status : " + result);
logger.finest("Response body:\n" + method.getResponseBodyAsString());
}
return resp;
}
private String url(String path) {
try {
return new URI(root, path, false).toString();
} catch (URIException ex) {
throw new RuntimeException("Error building URL for " + root + " " + path, ex);
}
}
public void close() throws IOException {
manager.shutdown();
}
// this should support the concept of parent component, otherwise
// children components must use their parent uri function ...
static abstract class Component {
protected final String root;
protected final CouchDBClient client;
protected Component(String root,CouchDBClient client) {
this.root = root;
this.client = client;
}
protected final String uri(String path) {
return root + (path.charAt(0) == '/' ? "" : "/") + path;
}
}
}