com.jzboy.couchdb.Database.java Source code

Java tutorial

Introduction

Here is the source code for com.jzboy.couchdb.Database.java

Source

/*
 * Copyright (c) 2011. Elad Kehat.
 * This software is provided under the MIT License:
 * http://www.opensource.org/licenses/mit-license.php
 */

package com.jzboy.couchdb;

import com.jzboy.couchdb.http.CouchResponse;
import com.jzboy.couchdb.util.JsonUtils;
import com.jzboy.couchdb.http.URITemplates;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.JsonNodeFactory;
import org.codehaus.jackson.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A wrapper around CouchDB database level API
 *
 * @see <a href="http://wiki.apache.org/couchdb/API_Cheatsheet">CouchDB API Wiki</a>
 */
public class Database {

    private static final Logger logger = LoggerFactory.getLogger(Database.class);

    final Server server;
    final String dbName;

    private ArrayList<Document> bulkUpdatesCache = new ArrayList<Document>();
    private int bulkUpdatesLimit = 1000;

    /**
     * Create a Database object
     * @param server   a Server that contains information on the CouchDB server
     * @param dbName   unique name of this database on the server
     */
    public Database(Server server, String dbName) {
        this.server = server;
        this.dbName = dbName;
    }

    /**
     * Create a Database object, and its encapsulated Server
     * @param host      the server's host
     * @param port      the server's port
     * @param dbName   unique name of this database on the server
     */
    public Database(String host, int port, String dbName) {
        this.server = new Server(host, port);
        this.dbName = dbName;
    }

    /**
     * Create a Database object, and a default encapsulated Server.
     * The encapsulated server points to localhost:5984
     * @param dbName   unique name of this database on the server
     */
    public Database(String dbName) {
        this.server = new Server();
        this.dbName = dbName;
    }

    public String getDbName() {
        return dbName;
    }

    public Server getServer() {
        return server;
    }

    /**
     * Returns the current size of the bulk updates cache.
     * When using {@link #saveInBulk(com.jzboy.couchdb.Document) saveInBulk}, this size is used to determine
     * after how many documents the whole cache is saved in a bulk operation.
     */
    public int getBulkUpdatesLimit() {
        return bulkUpdatesLimit;
    }

    /**
     * Set the size of the bulk updates cache.
     * This <strong>will not</strong> automatically flush the cache if its current size is greater than the new limit.
     */
    public void setBulkUpdatesLimit(int bulkUpdatesLimit) {
        if (bulkUpdatesLimit < 1)
            throw new IllegalArgumentException("bulkUpdatesLimit must be greater than 0");
        this.bulkUpdatesLimit = bulkUpdatesLimit;
    }

    /**
     * Returns information about the database.
     * @see <a href="http://wiki.apache.org/couchdb/HTTP_database_API#Database_Information">
     * CouchDB Wiki for details on the information returned</a>
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public JsonNode info() throws IOException, URISyntaxException, CouchDBException {
        return server.getHttpClient().get(URITemplates.database(this));
    }

    /**
     * Returns information about the database.
     * Parses the raw JSON result into a Java Map.
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public Map<String, String> infoAsMap() throws IOException, URISyntaxException, CouchDBException {
        HashMap<String, String> map = new HashMap<String, String>();
        Iterator<Map.Entry<String, JsonNode>> it = ((ObjectNode) info()).getFields();
        while (it.hasNext()) {
            Map.Entry<String, JsonNode> entry = it.next();
            map.put(entry.getKey(), entry.getValue().getValueAsText());
        }
        return map;
    }

    /**
     * Check if this Database exists on the server.
     * This is a better way than calling {@link #info} and catching an exception, as it avoids the
     * exception's overhead. If CouchDB's response is not 200 (exists) or 404 (missing), then
     * the usual CocuhDBException is thrown.
     * @return   true iff a database with this name exists on the server
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code other than 200 or 404.
     */
    public boolean exists() throws IOException, URISyntaxException, CouchDBException {
        CouchResponse res = server.getHttpClient().getCouchResponse(URITemplates.database(this));
        if (res.getStatusCode() == 200)
            return true;
        else if (res.getStatusCode() == 404)
            return false;
        else
            throw new CouchDBException(res.getStatusCode(), res.getBody());
    }

    /**
     * Creates this database
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.    */
    public void create() throws IOException, URISyntaxException, CouchDBException {
        server.getHttpClient().put(URITemplates.database(this));
    }

    /**
     * Tests if this database exists. If it doesn't, create it.
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error.
     */
    public void createIfNotExists() throws IOException, URISyntaxException, CouchDBException {
        if (!exists())
            create();
    }

    /**
     * Delete this database
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error.
     */
    public void delete() throws IOException, URISyntaxException, CouchDBException {
        // On Windows, an attempt to delete the database frequently results in a eaccess(500) error.
        // see - https://issues.apache.org/jira/browse/COUCHDB-326
        // The code here retries the request several times on such errors
        int attemptsLeft = 5;
        boolean retry = true;
        while (retry && attemptsLeft > 0) {
            try {
                server.getHttpClient().delete(URITemplates.database(this));
                retry = false;
            } catch (CouchDBException ex) {
                if (ex.getStatusCode() == 500 && attemptsLeft > 1) {
                    attemptsLeft--;
                    logger.debug("Got {} on delete attempt #{} on {}",
                            new Object[] { ex.getMessage(), (3 - attemptsLeft), this });
                    // wait for a short while between delete attempts
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ie) {
                    }
                } else {
                    throw ex;
                }
            }
        }
    }

    /**
     * Returns the upper bound of document revisions which CouchDB keeps track of.
     * @see <a href="http://wiki.apache.org/couchdb/HTTP_database_API#Accessing_Database-specific_options">
     * CouchDB Wiki for more</a>
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public int getRevsLimit() throws IOException, URISyntaxException, CouchDBException {
        String res = server.getHttpClient().getBody(URITemplates.revsLimit(this));
        // the result is followed by a newline, which must be trimmed or parseInt fails
        return Integer.parseInt(res.trim());
    }

    /**
     * Set the upper bound of document revisions which CouchDB keeps track of.
     * @see <a href="http://wiki.apache.org/couchdb/HTTP_database_API#Accessing_Database-specific_options">
     * CouchDB Wiki</a>
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public void setRevsLimit(int limit) throws IOException, URISyntaxException, CouchDBException {
        server.getHttpClient().put(URITemplates.revsLimit(this), String.valueOf(limit));
    }

    /**
     * Returns a list of changes made to documents in the database.
     * This method doesn't support the 'continuous' option and throws an exception if it is
     * present in the params map.
     * @param params   parameters for the Changes API call. Keys are parameter names, mapped
     * to the string representation of the value, which is copied as-is to the JSON sent to
     * CouchDB. It is OK to pass either null or an empty map.
     * @return   the list of changes, where each change is a JSON
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     * @see <a href="http://wiki.apache.org/couchdb/HTTP_database_API#Changes">
     * CouchDB Wiki for a description of the possible parameters</a>
     */
    public ArrayList<JsonNode> changes(List<NameValuePair> params)
            throws IOException, URISyntaxException, CouchDBException {
        if (params.contains(new BasicNameValuePair("continuous", "true")))
            throw new CouchDBException("'continuous' feeds are not supported by this method");

        JsonNode res = server.getHttpClient()
                .get(URITemplates.dbChanges(this, URLEncodedUtils.format(params, "utf-8")));
        ArrayList<JsonNode> results = new ArrayList<JsonNode>();
        for (JsonNode row : res.get("results"))
            results.add(row);
        return results;
    }

    /**
     * Triggers database compaction.
     * @see <a href="http://wiki.apache.org/couchdb/Compaction#Database_Compaction">CouchDB Wiki</a>
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public void compact() throws IOException, URISyntaxException, CouchDBException {
        server.getHttpClient().post(URITemplates.compact(this));
    }

    /**
     * Retrieve a document.
     * @param id   identifier of the document to retrieve
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if the document is missing, throws a CouchDBException with 404
     * as its status code
     */
    public Document getDocument(String id) throws IOException, URISyntaxException, CouchDBException {
        JsonNode json = server.getHttpClient().get(URITemplates.document(this, id));
        return buildDocumentFromJsonResponse(json);
    }

    /**
     * Retrieve a document. If that document is missing, returns null rather than throw an exception.
     * Use this to check if a document exists without the overhead of exception handling.
     * Note that if CouchDB's responds with an error code other than or 404, then the usual
     * CocuhDBException is thrown.
     * @param id   identifier of the document to retrieve
     * @return the document, or null if no document with this id is found
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      on all CouchDB errors other than 404
     */
    public Document getDocumentOrNull(String id) throws IOException, URISyntaxException, CouchDBException {
        CouchResponse res = server.getHttpClient().getCouchResponse(URITemplates.document(this, id));
        if (res.getStatusCode() >= 300) {
            if (res.getStatusCode() == 404)
                return null;
            else
                throw new CouchDBException(res.getStatusCode(), res.getBody());
        }
        return buildDocumentFromJsonResponse(res.getBodyAsJson());
    }

    private Document buildDocumentFromJsonResponse(JsonNode json) {
        ObjectNode obj = (ObjectNode) json;
        String rev = obj.remove("_rev").getTextValue();
        String retId = obj.remove("_id").getTextValue();
        return new Document(retId, rev, obj);
    }

    /**
     * Parse a JSON response with a list of documents.
     * The source JSON is the response from either an _all_docs or a view query.
     * If the JSON rows contain a "doc" element (include_docs=true was passed as a parameter to the query), it is used
     * in constructing the returned documents, and the contents of "value" elements in the rows are ignored. Otherwise,
     * the returned documents are constructed with the arbitrary JSON inside the "value" element.
     */
    private ArrayList<Document> parseDocuments(JsonNode json) {
        int totalRows = json.get("total_rows").getIntValue();
        ArrayList<Document> results = new ArrayList<Document>(totalRows);
        for (JsonNode row : json.get("rows")) {
            // if include_docs=true was in the params, we get the full document within each result row
            JsonNode docNode = row.get("doc");
            String id = row.get("id").getTextValue();
            String key = row.get("key").getTextValue();
            if (docNode != null) {
                ObjectNode doc = (ObjectNode) row.get("doc");
                String rev = doc.remove("_rev").getTextValue();
                doc.remove("_id").getTextValue(); // same as "id" retrieved from the row
                results.add(new Document(id, rev, key, doc));
            } else if (row.get("error") != null) {
                continue;
            } else { // docNode is null
                JsonNode value = row.get("value");
                JsonNode rev = null;
                if (value.isObject()) {
                    ObjectNode objValue = (ObjectNode) value;
                    rev = objValue.remove("rev");
                    if (rev == null)
                        rev = objValue.remove("_rev");
                }
                String revStr = (rev != null) ? rev.getTextValue() : null;
                Document doc = new Document(id, revStr, key, value);
                results.add(doc);
            }
        }
        return results;
    }

    /**
     * Retrieve a collection of documents, given their ids.
     * @param ids      ids of the documents
     * @param params   view API params
     * @return if params contain include_docs:true, a list of the full documents; otherwise each Document in the result
     * list contains an id and rev only. Ids that weren't found are ignored, so the result's size may be smaller than
     * the input ids'.
     * @see <a href="http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API#Fetch_Multiple_Documents_With_a_Single_Request">
     * CouchDB Wiki for more information on valid parameters</a>
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public ArrayList<Document> getDocuments(Collection<String> ids, List<NameValuePair> params)
            throws IOException, URISyntaxException, CouchDBException {
        // build a json document with the requested document ids
        ObjectNode root = JsonNodeFactory.instance.objectNode();
        ArrayNode keys = root.putArray("keys");
        for (String id : ids)
            keys.add(id);
        String data = JsonUtils.serializeJson(root);
        String query = (params == null) ? null : URLEncodedUtils.format(params, "utf-8");
        JsonNode json = server.getHttpClient().post(URITemplates.allDocs(this, query), data);
        return parseDocuments(json);
    }

    /**
     * Convenience method that calls {@link #getDocuments(java.util.Collection, java.util.List) getDocuments}
     * method with the include_docs=true param.
     */
    public ArrayList<Document> getDocuments(Collection<String> ids, boolean includeDocs)
            throws IOException, URISyntaxException, CouchDBException {
        return getDocuments(ids, includeDocsParams(includeDocs));
    }

    /**
     * Optionally return a 1-item list, with the name-value pair include_docs=true
     * @return   a list with the include_docs param, or null if includeDocs is false
     */
    private List<NameValuePair> includeDocsParams(boolean includeDocs) {
        List<NameValuePair> params = null;
        if (includeDocs)
            params = new ArrayList<NameValuePair>() {
                {
                    add(new BasicNameValuePair("include_docs", "true"));
                }
            };
        return params;
    }

    /**
     * Retrieve a list of documents, optionally filtered by the parameters.
     * @param params   view API params
     * @return if params contain include_docs:true, a list of the full documents; otherwise each Document in the result
     * list contains an id and rev only.
     * @see <a href="http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API#Fetch_Multiple_Documents_With_a_Single_Request">
     * CouchDB Wiki for more information on valid parameters</a>
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public ArrayList<Document> getAllDocuments(List<NameValuePair> params)
            throws IOException, URISyntaxException, CouchDBException {
        String query = (params == null) ? null : URLEncodedUtils.format(params, "utf-8");
        JsonNode json = server.getHttpClient().get(URITemplates.allDocs(this, query));
        return parseDocuments(json);
    }

    /**
     * Convenience method that calls {@link #getAllDocuments(java.util.List) getAllDocuments} method with just the
     * include_docs param.
     */
    public ArrayList<Document> getAllDocuments(boolean includeDocs)
            throws IOException, URISyntaxException, CouchDBException {
        return getAllDocuments(includeDocsParams(includeDocs));
    }

    /**
     * Create a document object from a result that contains "id" and "rev" fields.
     * These results are usually returned by create/update/delete operations on documents.
     */
    private Document parseDocumentResult(JsonNode json) {
        String retId = json.get("id").getTextValue();
        JsonNode revNode = json.get("rev");
        // no rev is returned when saving in batch mode
        String rev = (revNode == null) ? null : revNode.getTextValue();
        return new Document(retId, rev, json);
    }

    /**
     * Create a new document with the specified id.
     * @param uuid      the uuid given to the new docuemnt
     * @param docJson   the document's content in JSON format
     * @param batch      if true, instructs CouchDB to insert the document in batch mode and not
     * right away
     * @return a new Document, with the new revision number, and CouchDB's JSON response. This Document
     * does not contain the original document content.
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public Document createDocument(String uuid, String docJson, boolean batch)
            throws IOException, URISyntaxException, CouchDBException {
        String doc = JsonUtils.reencodeUtf8ToIso88591(docJson);
        JsonNode json = server.getHttpClient().put(URITemplates.document(this, uuid, batch), doc);
        return parseDocumentResult(json);
    }

    public Document createDocument(String uuid, JsonNode json, boolean batch)
            throws IOException, URISyntaxException, CouchDBException {
        return createDocument(uuid, JsonUtils.serializeJson(json), batch);
    }

    /**
     * Create a new document, with server-generated id.
     */
    public Document createDocument(String docJson, boolean batch)
            throws IOException, URISyntaxException, CouchDBException {
        String doc = JsonUtils.reencodeUtf8ToIso88591(docJson);
        // use post, rather than put to have the server generate an id for us
        JsonNode json = server.getHttpClient().post(URITemplates.document(this, batch), doc);
        return parseDocumentResult(json);
    }

    public Document createDocument(JsonNode json, boolean batch)
            throws IOException, URISyntaxException, CouchDBException {
        return createDocument(JsonUtils.serializeJson(json), batch);
    }

    /**
     * Creates a new document from the given Document object.
     * If newDoc has no id field, then one is generated by the server.
     */
    public Document createDocument(Document newDoc, boolean batch)
            throws IOException, URISyntaxException, CouchDBException {
        if (newDoc.hasId())
            return createDocument(newDoc.getId(), newDoc.getJson(), batch);
        else
            return createDocument(newDoc.getJson(), batch);
    }

    public Document createDocument(Document newDoc) throws IOException, URISyntaxException, CouchDBException {
        return createDocument(newDoc, false);
    }

    /**
     * Given an existing document, update it.
     * @param doc   the Document to update. Must contain the id and revision number. Must also contain
     * the entire document data as a JSON - not just the new parts.
     * @return a new Document, with the new revision number, and CouchDB's JSON response. This Document
     * does not contain the original or updated document content.
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      a common problem here is a document update conflict. In this case a
     * CouchDBException is thrown, with 409 as its status code.
     */
    public Document updateDocument(Document doc) throws IOException, URISyntaxException, CouchDBException {
        // CouchDB semantics do not allow us to update a doc that has no revision field, so avoid this in advance
        if (doc.getRev() == null)
            throw new CouchDBException(
                    String.format("Cannot update %s - missing a current revision number", doc.getId()));

        ObjectNode root = (ObjectNode) doc.getJson();
        // add the current revision into the json string - as required by CouchDB for updates
        // if this isn't the latest revision we'll get a 409 response
        root.put("_rev", doc.getRev());
        String docValue = JsonUtils.reencodeUtf8ToIso88591(JsonUtils.serializeJson(root));
        // Connection resets are quite common with updates, so retry the update several times
        JsonNode json = server.getHttpClient().put(URITemplates.document(this, doc.getId()), docValue, 3);
        return parseDocumentResult(json);
    }

    /**
     * Delete the document.
     * @param doc   the Document to delete. Must include an id and revision. Its JSON content is ignored.
     * @return the revision id for the deletion stub
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException   if the revision number is not up-to-date.
     */
    public String deleteDocument(Document doc) throws IOException, URISyntaxException, CouchDBException {
        JsonNode json = server.getHttpClient().delete(URITemplates.deleteDocument(this, doc));
        return json.get("rev").getTextValue();
    }

    /**
     * Get the speficied document attachment as a byte array.
     * @param docId      id of the document to which the attachment belongs
     * @param fileName   name of the attachment
     * @return the streaming attachment
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public byte[] getAttachment(String docId, String fileName)
            throws URISyntaxException, IOException, CouchDBException {
        HttpResponse res = server.getHttpClient().getHttpResponse(URITemplates.attachment(this, docId, fileName));
        if (res.getStatusLine().getStatusCode() >= 300) {
            res.getEntity().consumeContent(); // necessary in order to release the underlying connection
            throw new CouchDBException(res.getStatusLine().getStatusCode(), res.getStatusLine().getReasonPhrase());
        }
        return EntityUtils.toByteArray(res.getEntity());
    }

    /**
     * Get the speficied document attachment, as a raw HttpResponse.
     * Use this method get get low-level access, for example, to the attachment's content-type header, or
     * to read an attachment from a stream.
     * Make sure you release the underlying resources - @see
     * <a href="http://hc.apache.org/httpcomponents-client/tutorial/html/fundamentals.html#d4e143">
     * HttpComponents Tutorial</a>
     * @param docId      id of the document to which the attachment belongs
     * @param fileName   name of the attachment
     * @return the HttpRespone object returned by executing a get request for the attachment
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     */
    public HttpResponse getAttachmentRaw(String docId, String fileName) throws IOException, URISyntaxException {
        return server.getHttpClient().getHttpResponse(URITemplates.attachment(this, docId, fileName));
    }

    /**
     * Create or update a document attachment.
     * @param doc      the document to which the attachment belongs. If the document object contains no revision id,
     * a new document is created along with the attachment.
     * @param fileName   name of the attachment. If it matches an attachment that already exists for this document,
     * then it is updated. Otherwise, a new attachment is created for the document.
     * @param data      the attachment itself, as a byte array
     * @param mimeType   MIME type of the attachment
     * @return a new Document, with the new revision number, and CouchDB's JSON response. This Document
     * does not contain the original or updated document content.
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public Document saveAttachment(Document doc, String fileName, byte[] data, String mimeType)
            throws IOException, URISyntaxException, CouchDBException {
        JsonNode json = server.getHttpClient().put(URITemplates.attachment(this, doc, fileName), data, mimeType);
        return parseDocumentResult(json);
    }

    /**
     * Delete a document attachment.
     * @param doc      the document ot which the attachment belongs. Must contain both an id and a revision.
     * @param fileName   name of the attachment to delete
     * @return         a document object with the updated revision number
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public Document deleteAttachment(Document doc, String fileName)
            throws IOException, URISyntaxException, CouchDBException {
        JsonNode json = server.getHttpClient().delete(URITemplates.attachment(this, doc, fileName));
        return parseDocumentResult(json);
    }

    /**
     * Save the document in bulk mode: add the new document to an internal buffer rather than sending it to
     * CouchDB right away.
     * <p>
     * Bulk inserts increase performance by a factor of 500! (See O'Reilly - CouchDB - The Definitive Guide)
     * <p>
     * The bulk cache is flushed when the number of documents in it reaches bulkCacheLimit, or when flushBulkCache
     * is called.
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public void saveInBulk(Document doc) throws IOException, URISyntaxException, CouchDBException {
        // if the document has no JSON content, then there's nothing to save
        if (!doc.hasContent())
            throw new IllegalArgumentException("Document has no JSON content");

        synchronized (this) {
            bulkUpdatesCache.add(doc);
            if (bulkUpdatesCache.size() >= bulkUpdatesLimit)
                flushBulkUpdatesCache();
        }
    }

    /**
     * Delete the document in bulk mode - calls {@link #saveInBulk(com.jzboy.couchdb.Document) saveInBulk}, adding
     * a _deleted=true element to the document.
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public void deleteInBulk(Document doc) throws IOException, URISyntaxException, CouchDBException {
        ObjectNode json;
        if (doc.getJson() == null) {
            json = JsonNodeFactory.instance.objectNode();
            doc.setJson(json);
        } else {
            json = ((ObjectNode) doc.getJson());
        }
        json.put("_deleted", true);
        saveInBulk(doc);
    }

    /**
     * Saves a collection of document, stored internally in a cache, to CouchDB.
     * @param allOrNothing   update in bulk using transactional semantics.
     * @param returnReport   if true returns the results received from CouchDB. The JSON may be large and takes a
     * while to process, so if don't plan on doing anything with it, pass false to avoid this processing and the
     * method will return null.
     * @see <a href="http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API#Transactional_Semantics_with_Bulk_Updates">
     * CouchDB Wiki on transactional semantics for more information regarding the allOrNothing option</a>
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public synchronized ArrayList<JsonNode> flushBulkUpdatesCache(boolean allOrNothing, boolean returnReport)
            throws IOException, URISyntaxException, CouchDBException {
        if (bulkUpdatesCache.isEmpty())
            return (returnReport) ? new ArrayList<JsonNode>() : null;
        // build a json document from all docs in the cache
        ObjectNode root = JsonNodeFactory.instance.objectNode();
        ArrayNode docs = root.putArray("docs");
        for (Document doc : bulkUpdatesCache) {
            ObjectNode node = (ObjectNode) doc.getJson();
            if (doc.hasId()) // if the doc has no id, a new one is assigned to it automatically by CouchDB
                node.put("_id", doc.getId());
            if (doc.hasRev()) // if the doc has a revision, it's updated rather than inserted
                node.put("_rev", doc.getRev());
            docs.add(node);
        }
        if (allOrNothing)
            root.put("all_or_nothing", true);

        String jsonStr = JsonUtils.reencodeUtf8ToIso88591(JsonUtils.serializeJson(root));
        // according to the docs (http://wiki.apache.org/couchdb/HTTP_Bulk_Document_API) we'll never get an error here
        // - problems are reported on an individual node basis
        JsonNode res = server.getHttpClient().post(URITemplates.bulkDocs(this), jsonStr);
        bulkUpdatesCache.clear();

        if (!returnReport)
            return null;
        ArrayList<JsonNode> results = new ArrayList<JsonNode>();
        for (JsonNode row : res)
            results.add(row);
        return results;
    }

    /**
     * Convenience method that calls flushBulkUpdatesCache with parameters set to false.
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public void flushBulkUpdatesCache() throws IOException, URISyntaxException, CouchDBException {
        flushBulkUpdatesCache(false, false);
    }

    /**
     * Returns the number of pending documents for bulk save in the internal cache
     */
    public synchronized int numDocsPendingBulkUpdate() {
        return bulkUpdatesCache.size();
    }

    /**
     * Removes all the documents pending bulk update
     */
    public synchronized void clearBulkUpdatesCache() {
        bulkUpdatesCache.clear();
    }

    /**
     * Retrieve information about a design document
     * @see <a href="http://wiki.apache.org/couchdb/HTTP_view_API#Getting_Information_about_Design_Documents_.28and_their_Views.29">
     * CouchDB Wiki for a description of the information contained in the returned JSON</a>
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public JsonNode designDocumentInfo(String designDocName)
            throws IOException, URISyntaxException, CouchDBException {
        return server.getHttpClient().get(URITemplates.designDocInfo(this, designDocName));
    }

    /**
     * Retrieve documents from a view, filtered by the specified parameters.
     * @param designDocName   name of the design document that contains the code for the view
     * @param viewName      name of the view within the design document
     * @param params      query parameters, e.g. startkey, endkey, limit, etc.
     * @see <a href="http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options">CouchDB Wiki
     * for a complete description of valid parameters</a>
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public ArrayList<Document> queryView(String designDocName, String viewName, List<NameValuePair> params)
            throws IOException, URISyntaxException, CouchDBException {
        java.net.URI uri = URITemplates.queryView(this, designDocName, viewName,
                URLEncodedUtils.format(params, "utf-8"));
        JsonNode json = server.getHttpClient().get(uri);
        return parseDocuments(json);
    }

    /**
     * Query a view and return the raw JSON.
     * Use this instead of {@link #queryView(java.lang.String, java.lang.String, java.util.List) queryView} method
     * if you prefer to receive metadata and/or process the results yourself and not have JZBoy create documents
     * for you.
     * For example, to retrieve the metadata for a view, such as the number of documents in it,  but not any documents,
     * use this method and pass limit=0 in the params.
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public JsonNode queryViewRaw(String designDocName, String viewName, List<NameValuePair> params)
            throws IOException, URISyntaxException, CouchDBException {
        java.net.URI uri = URITemplates.queryView(this, designDocName, viewName,
                URLEncodedUtils.format(params, "utf-8"));
        return server.getHttpClient().get(uri);
    }

    /**
     * Retrieve rows from a view, whose keys match the specified keys.
     * @param designDocName   name of the design document that contains the code for the view
     * @param viewName      name of the view within the design document
     * @param keys         the keys to retrieve. If the collection is ordered (e.g. a List), then
     * the results match this order.
     * @param params      query parameters, e.g. include_docs
     * @return see {@link #queryView(java.lang.String, java.lang.String, java.util.List) queryView}
     * for information on the result structure.
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public ArrayList<Document> getFromView(String designDocName, String viewName, Collection<String> keys,
            List<NameValuePair> params) throws IOException, URISyntaxException, CouchDBException {
        // build a json document with the requested keys
        ObjectNode root = JsonNodeFactory.instance.objectNode();
        ArrayNode keysNode = root.putArray("keys");
        for (String key : keys)
            keysNode.add(key);
        String data = JsonUtils.serializeJson(root);
        java.net.URI uri = URITemplates.queryView(this, designDocName, viewName,
                URLEncodedUtils.format(params, "utf-8"));
        JsonNode json = server.getHttpClient().post(uri, data);
        return parseDocuments(json);
    }

    /**
     * Run an ad-hoc view
     * @param viewJsonStr   the view code
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public ArrayList<Document> tempView(String viewJsonStr)
            throws IOException, URISyntaxException, CouchDBException {
        String encoded = JsonUtils.reencodeUtf8ToIso88591(viewJsonStr);
        JsonNode json = server.getHttpClient().post(URITemplates.tempView(this), encoded);
        return parseDocuments(json);
    }

    /**
     * Run an ad-hoc view
     * @param viewJson   the view code as a JSON object
     */
    public ArrayList<Document> tempView(JsonNode viewJson)
            throws IOException, URISyntaxException, CouchDBException {
        return tempView(JsonUtils.serializeJson(viewJson));
    }

    /**
     * Compact the views in the specified design document.
     * @param designDocName      name the design document, excluding the '_design/' prefix
     * @see <a href="http://wiki.apache.org/couchdb/Compaction#View_compaction">CouchDB Wiki</a>
      * @throws IOException         if the HttpClient throws an IOException
     * @throws URISyntaxException   if there was a problem constructing a URI for this database
     * @throws CouchDBException      if CouchDB returns an error - response code >= 300. The response
     * details are encapsulated in the exception.
     */
    public void compactViews(String designDocName) throws IOException, URISyntaxException, CouchDBException {
        server.getHttpClient().post(URITemplates.compactView(this, designDocName));
    }

    /**
     * Remove outdated view indexes that remain on the disk.
     * @see <a href="http://wiki.apache.org/couchdb/Compaction#View_compaction">CouchDB Wiki</a>
     */
    public void cleanupViews() throws IOException, URISyntaxException, CouchDBException {
        server.getHttpClient().post(URITemplates.cleanupViews(this));
    }

    @Override
    public String toString() {
        return this.server.toString() + this.dbName;
    }

}