Feed notifications are accessed in an iterator style.
- * @return {@link Changes}
- */
- public Changes continuousChanges() {
- final URI uri = uriBuilder.query("feed", "continuous").build();
- httpGet = new HttpGet(uri);
- final InputStream in = dbc.get(httpGet);
- final InputStreamReader is = new InputStreamReader(in, Charsets.UTF_8);
- setReader(new BufferedReader(is));
- return this;
- }
-
- /**
- * Checks whether a feed is available in the continuous stream, blocking
- * until a feed is received.
- * @return true If a feed is available
- */
- public boolean hasNext() {
- return readNextRow();
- }
-
- /**
- * @return The next feed in the stream.
- */
- public Row next() {
- return getNextRow();
- }
-
- /**
- * Stops a running continuous feed.
- */
- public void stop() {
- stop = true;
- }
-
- /**
- * Requests Change notifications of feed type normal.
- * @return {@link ChangesResult}
- */
- public ChangesResult getChanges() {
- final URI uri = uriBuilder.query("feed", "normal").build();
- return dbc.get(uri, ChangesResult.class);
- }
-
- // Query Params
-
- public Changes since(String since) {
- uriBuilder.query("since", since);
- return this;
- }
-
- public Changes limit(int limit) {
- uriBuilder.query("limit", limit);
- return this;
- }
-
- public Changes heartBeat(long heartBeat) {
- uriBuilder.query("heartbeat", heartBeat);
- return this;
- }
-
- public Changes timeout(long timeout) {
- uriBuilder.query("timeout", timeout);
- return this;
- }
-
- public Changes filter(String filter) {
- uriBuilder.query("filter", filter);
- return this;
- }
-
- public Changes includeDocs(boolean includeDocs) {
- uriBuilder.query("include_docs", includeDocs);
- return this;
- }
-
- public Changes style(String style) {
- uriBuilder.query("style", style);
- return this;
- }
-
- // Helper
-
- /**
- * Reads and sets the next feed in the stream.
- */
- private boolean readNextRow() {
- boolean hasNext = false;
- try {
- if(!stop) {
- String row = "";
- do {
- row = getReader().readLine();
- } while(row.length() == 0);
-
- if(!row.startsWith("{\"last_seq\":")) {
- setNextRow(gson.fromJson(row, Row.class));
- hasNext = true;
- }
- }
- } catch (Exception e) {
- terminate();
- throw new CouchDbException("Error reading continuous stream.", e);
- }
- if(!hasNext)
- terminate();
- return hasNext;
- }
-
- private BufferedReader getReader() {
- return reader;
- }
-
- private void setReader(BufferedReader reader) {
- this.reader = reader;
- }
-
- private Row getNextRow() {
- return nextRow;
- }
-
- private void setNextRow(Row nextRow) {
- this.nextRow = nextRow;
- }
-
- private void terminate() {
- httpGet.abort();
- CouchDbUtil.close(getReader());
- }
+
+ private BufferedReader reader;
+ private HttpUriRequest httpRequest;
+ private Row nextRow;
+ private boolean stop;
+
+ private CouchDbClientBase dbc;
+ private Gson gson;
+ private URIBuilder uriBuilder;
+
+ private String filter;
+ private String selector;
+ private List docIds;
+
+ Changes(CouchDbClientBase dbc) {
+ this.dbc = dbc;
+ this.gson = dbc.getGson();
+ this.uriBuilder = URIBuilder.buildUri(dbc.getDBUri()).path("_changes");
+ }
+
+ /**
+ * Requests Change notifications of feed type continuous.
+ *
+ * Feed notifications are accessed in an iterator style.
+ *
+ * @return {@link Changes}
+ */
+ public Changes continuousChanges() {
+ final URI uri = uriBuilder.query("feed", "continuous").build();
+ if (selector == null) {
+ final HttpGet get = new HttpGet(uri);
+ httpRequest = get;
+ final InputStream in = dbc.get(get);
+ final InputStreamReader is = new InputStreamReader(in, Charsets.UTF_8);
+ setReader(new BufferedReader(is));
+ } else {
+ final HttpPost post = new HttpPost(uri);
+ httpRequest = post;
+ final InputStream in = dbc.post(post, selector);
+ final InputStreamReader is = new InputStreamReader(in, Charsets.UTF_8);
+ setReader(new BufferedReader(is));
+ }
+ return this;
+ }
+
+ /**
+ * Checks whether a feed is available in the continuous stream, blocking until a feed is received.
+ *
+ * @return true If a feed is available
+ */
+ public boolean hasNext() {
+ return readNextRow();
+ }
+
+ /**
+ * @return The next feed in the stream.
+ */
+ public Row next() {
+ return getNextRow();
+ }
+
+ /**
+ * Stops a running continuous feed.
+ */
+ public void stop() {
+ stop = true;
+ }
+
+ /**
+ * Requests Change notifications of feed type normal.
+ *
+ * @return {@link ChangesResult}
+ */
+ public ChangesResult getChanges() {
+ final URI uri = uriBuilder.query("feed", "normal").build();
+ if (selector == null && docIds == null) {
+ return dbc.get(uri, ChangesResult.class);
+ } else {
+ String json = selector;
+ if (docIds != null) {
+ JsonObject docIdsJson = new JsonObject();
+ JsonArray jArray = new JsonArray();
+ for (String id : docIds) {
+ jArray.add(id);
+ }
+ docIdsJson.add("doc_ids", jArray);
+ json = docIdsJson.toString();
+ }
+
+ return dbc.post(uri, json, ChangesResult.class);
+ }
+ }
+
+ // Query Params
+
+ public Changes since(String since) {
+ uriBuilder.query("since", since);
+ return this;
+ }
+
+ public Changes limit(int limit) {
+ uriBuilder.query("limit", limit);
+ return this;
+ }
+
+ public Changes heartBeat(long heartBeat) {
+ uriBuilder.query("heartbeat", heartBeat);
+ return this;
+ }
+
+ public Changes timeout(long timeout) {
+ uriBuilder.query("timeout", timeout);
+ return this;
+ }
+
+ public Changes filter(String filter) {
+ if (docIds!=null || selector != null) {
+ throw new IllegalArgumentException("Filter is not compatible with selector or docIds filters");
+ }
+ uriBuilder.query("filter", filter);
+ this.filter=filter;
+ return this;
+ }
+
+ public Changes selector(String json) {
+ if (docIds!=null || filter != null) {
+ throw new IllegalArgumentException("Selector is not compatible with filters or docIds filters");
+ }
+ uriBuilder.query("filter", "_selector");
+ this.selector = json;
+ return this;
+ }
+
+ public Changes docIds(List docIds) {
+ if (selector!=null || filter != null) {
+ throw new IllegalArgumentException("DocIds filter is not compatible with filter or selector");
+ }
+ uriBuilder.query("filter", "_doc_ids");
+ this.docIds = docIds;
+ return this;
+ }
+
+ public Changes includeDocs(boolean includeDocs) {
+ uriBuilder.query("include_docs", includeDocs);
+ return this;
+ }
+
+ public Changes style(String style) {
+ uriBuilder.query("style", style);
+ return this;
+ }
+
+ public Changes seqInterval(long batchSize) {
+ uriBuilder.query("seq_interval", batchSize);
+ return this;
+ }
+
+ // Helper
+
+ /**
+ * Reads and sets the next feed in the stream.
+ */
+ private boolean readNextRow() {
+ boolean hasNext = false;
+ try {
+ if (!stop) {
+ String row = "";
+ do {
+ row = getReader().readLine();
+ } while (row.length() == 0 && !stop);
+
+ if (!stop) {
+ if (!row.startsWith("{\"last_seq\":")) {
+ setNextRow(gson.fromJson(row, Row.class));
+ hasNext = true;
+ }
+ }
+ }
+ } catch (Exception e) {
+ terminate();
+ throw new CouchDbException("Error reading continuous stream.", e);
+ }
+ if (!hasNext)
+ terminate();
+ return hasNext;
+ }
+
+ private BufferedReader getReader() {
+ return reader;
+ }
+
+ private void setReader(BufferedReader reader) {
+ this.reader = reader;
+ }
+
+ private Row getNextRow() {
+ return nextRow;
+ }
+
+ private void setNextRow(Row nextRow) {
+ this.nextRow = nextRow;
+ }
+
+ private void terminate() {
+ httpRequest.abort();
+ CouchDbUtil.close(getReader());
+ }
}
diff --git a/src/main/java/org/lightcouch/CouchDbClient.java b/src/main/java/org/lightcouch/CouchDbClient.java
index 5694e92..14378b8 100644
--- a/src/main/java/org/lightcouch/CouchDbClient.java
+++ b/src/main/java/org/lightcouch/CouchDbClient.java
@@ -229,7 +229,6 @@ public void shutdown() {
HttpClientUtils.closeQuietly(this.httpClient);
}
- @Override
public void close() throws IOException {
shutdown();
}
diff --git a/src/main/java/org/lightcouch/CouchDbClientBase.java b/src/main/java/org/lightcouch/CouchDbClientBase.java
index 956eb85..026e4c9 100644
--- a/src/main/java/org/lightcouch/CouchDbClientBase.java
+++ b/src/main/java/org/lightcouch/CouchDbClientBase.java
@@ -1,17 +1,14 @@
/*
- * Copyright (C) 2011 lightcouch.org
+ * Copyright (C) 2019 indaba.es 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
+ * 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.
+ * 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;
@@ -34,10 +31,12 @@
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import org.apache.commons.codec.Charsets;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
@@ -51,6 +50,7 @@
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
+import org.apache.http.message.BasicHeader;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
@@ -69,647 +69,799 @@
/**
* Contains a client Public API implementation.
+ *
* @see CouchDbClient
* @see CouchDbClientAndroid
* @author Ahmed Yehia
*/
public abstract class CouchDbClientBase {
- static final Log log = LogFactory.getLog(CouchDbClient.class);
-
- private URI baseURI;
- private URI dbURI;
- private Gson gson;
- private CouchDbContext context;
- private CouchDbDesign design;
- final HttpClient httpClient;
- final HttpHost host;
-
- CouchDbClientBase() {
- this(new CouchDbConfig());
- }
-
- CouchDbClientBase(CouchDbConfig config) {
- final CouchDbProperties props = config.getProperties();
- this.httpClient = createHttpClient(props);
- this.gson = initGson(new GsonBuilder());
- this.host = new HttpHost(props.getHost(), props.getPort(), props.getProtocol());
-
- final String path = props.getPath() != null ? props.getPath() : "";
- this.baseURI = buildUri().scheme(props.getProtocol()).host(props.getHost()).port(props.getPort()).path("/").path(path).build();
- this.dbURI = buildUri(baseURI).path(props.getDbName()).path("/").build();
-
- this.context = new CouchDbContext(this, props);
- this.design = new CouchDbDesign(this);
- }
-
- // Client(s) provided implementation
-
- /**
- * @return {@link HttpClient} instance for HTTP request execution.
- */
- abstract HttpClient createHttpClient(CouchDbProperties properties);
-
- /**
- * @return {@link HttpContext} instance for HTTP request execution.
- */
- abstract HttpContext createContext();
-
- /**
- * Shuts down the connection manager used by this client instance.
- */
- abstract void shutdown();
-
- // Public API
-
- /**
- * Provides access to DB server APIs.
- * @return {@link CouchDbContext}
- */
- public CouchDbContext context() {
- return context;
- }
-
- /**
- * Provides access to CouchDB Design Documents.
- * @return {@link CouchDbDesign}
- */
- public CouchDbDesign design() {
- return design;
- }
-
- /**
- * Provides access to CouchDB View APIs.
- * @param viewId The view id.
- * @return {@link View}
- */
- public View view(String viewId) {
- return new View(this, viewId);
- }
-
- /**
- * Provides access to CouchDB replication APIs.
- * @return {@link Replication}
- */
- public Replication replication() {
- return new Replication(this);
- }
-
- /**
- * Provides access to the replicator database.
- * @return {@link Replicator}
- */
- public Replicator replicator() {
- return new Replicator(this);
- }
-
- /**
- * Provides access to Change Notifications API.
- * @return {@link Changes}
- */
- public Changes changes() {
- return new Changes(this);
- }
-
- /**
- * Finds an Object of the specified type.
- * @param Object type.
- * @param classType The class of type T.
- * @param id The document id.
- * @return An object of type T.
- * @throws NoDocumentException If the document is not found in the database.
- */
- public T find(Class classType, String id) {
- assertNotEmpty(classType, "Class");
- assertNotEmpty(id, "id");
- final URI uri = buildUri(getDBUri()).pathEncoded(id).build();
- return get(uri, classType);
- }
-
- /**
- * Finds an Object of the specified type.
- * @param Object type.
- * @param classType The class of type T.
- * @param id The document id.
- * @param params Extra parameters to append.
- * @return An object of type T.
- * @throws NoDocumentException If the document is not found in the database.
- */
- public T find(Class classType, String id, Params params) {
- assertNotEmpty(classType, "Class");
- assertNotEmpty(id, "id");
- final URI uri = buildUri(getDBUri()).pathEncoded(id).query(params).build();
- return get(uri, classType);
- }
-
- /**
- * Finds an Object of the specified type.
- * @param Object type.
- * @param classType The class of type T.
- * @param id The document _id field.
- * @param rev The document _rev field.
- * @return An object of type T.
- * @throws NoDocumentException If the document is not found in the database.
- */
- public T find(Class classType, String id, String rev) {
- assertNotEmpty(classType, "Class");
- assertNotEmpty(id, "id");
- assertNotEmpty(id, "rev");
- final URI uri = buildUri(getDBUri()).pathEncoded(id).query("rev", rev).build();
- return get(uri, classType);
- }
-
- /**
- * This method finds any document given a URI.
- *
The URI must be URI-encoded.
- * @param The class type.
- * @param classType The class of type T.
- * @param uri The URI as string.
- * @return An object of type T.
- */
- public T findAny(Class classType, String uri) {
- assertNotEmpty(classType, "Class");
- assertNotEmpty(uri, "uri");
- return get(URI.create(uri), classType);
- }
-
- /**
- * Finds a document and return the result as {@link InputStream}.
- *
Note: The stream must be closed after use to release the connection.
- * @param id The document _id field.
- * @return The result as {@link InputStream}
- * @throws NoDocumentException If the document is not found in the database.
- * @see #find(String, String)
- */
- public InputStream find(String id) {
- assertNotEmpty(id, "id");
- return get(buildUri(getDBUri()).path(id).build());
- }
-
- /**
- * Finds a document given id and revision and returns the result as {@link InputStream}.
- *
Note: The stream must be closed after use to release the connection.
- * @param id The document _id field.
- * @param rev The document _rev field.
- * @return The result as {@link InputStream}
- * @throws NoDocumentException If the document is not found in the database.
- */
- public InputStream find(String id, String rev) {
- assertNotEmpty(id, "id");
- assertNotEmpty(rev, "rev");
- final URI uri = buildUri(getDBUri()).path(id).query("rev", rev).build();
- return get(uri);
- }
-
- /**
- * Find documents using a declarative JSON querying syntax.
- * @param The class type.
- * @param jsonQuery The JSON query string.
- * @param classOfT The class of type T.
- * @return The result of the query as a {@code List }
- * @throws CouchDbException If the query failed to execute or the request is invalid.
- */
- public List findDocs(String jsonQuery, Class classOfT) {
- assertNotEmpty(jsonQuery, "jsonQuery");
- HttpResponse response = null;
- try {
- response = post(buildUri(getDBUri()).path("_find").build(), jsonQuery);
- Reader reader = new InputStreamReader(getStream(response), Charsets.UTF_8);
- JsonArray jsonArray = new JsonParser().parse(reader)
- .getAsJsonObject().getAsJsonArray("docs");
- List list = new ArrayList();
- for (JsonElement jsonElem : jsonArray) {
- JsonElement elem = jsonElem.getAsJsonObject();
- T t = this.gson.fromJson(elem, classOfT);
- list.add(t);
- }
- return list;
- } finally {
- close(response);
- }
- }
-
- /**
- * Checks if a document exist in the database.
- * @param id The document _id field.
- * @return true If the document is found, false otherwise.
- */
- public boolean contains(String id) {
- assertNotEmpty(id, "id");
- HttpResponse response = null;
- try {
- response = head(buildUri(getDBUri()).pathEncoded(id).build());
- } catch (NoDocumentException e) {
- return false;
- } finally {
- close(response);
- }
- return true;
- }
-
- /**
- * Saves an object in the database, using HTTP PUT request.
- *
If the object doesn't have an _id value, the code will assign a UUID as the document id.
- * @param object The object to save
- * @throws DocumentConflictException If a conflict is detected during the save.
- * @return {@link Response}
- */
- public Response save(Object object) {
- return put(getDBUri(), object, true);
- }
-
- /**
- * Saves an object in the database using HTTP POST request.
- *
The database will be responsible for generating the document id.
- * @param object The object to save
- * @return {@link Response}
- */
- public Response post(Object object) {
- assertNotEmpty(object, "object");
- HttpResponse response = null;
- try {
- URI uri = buildUri(getDBUri()).build();
- response = post(uri, getGson().toJson(object));
- return getResponse(response);
- } finally {
- close(response);
- }
- }
-
- /**
- * Saves a document with batch=ok query param.
- * @param object The object to save.
- */
- public void batch(Object object) {
- assertNotEmpty(object, "object");
- HttpResponse response = null;
- try {
- URI uri = buildUri(getDBUri()).query("batch", "ok").build();
- response = post(uri, getGson().toJson(object));
- } finally {
- close(response);
- }
- }
-
- /**
- * Updates an object in the database, the object must have the correct _id and _rev values.
- * @param object The object to update
- * @throws DocumentConflictException If a conflict is detected during the update.
- * @return {@link Response}
- */
- public Response update(Object object) {
- return put(getDBUri(), object, false);
- }
-
- /**
- * Removes a document from the database.
- *
The object must have the correct _id and _rev values.
- * @param object The document to remove as object.
- * @throws NoDocumentException If the document is not found in the database.
- * @return {@link Response}
- */
- public Response remove(Object object) {
- assertNotEmpty(object, "object");
- JsonObject jsonObject = getGson().toJsonTree(object).getAsJsonObject();
- final String id = getAsString(jsonObject, "_id");
- final String rev = getAsString(jsonObject, "_rev");
- return remove(id, rev);
- }
-
- /**
- * Removes a document from the database given both a document _id and _rev values.
- * @param id The document _id field.
- * @param rev The document _rev field.
- * @throws NoDocumentException If the document is not found in the database.
- * @return {@link Response}
- */
- public Response remove(String id, String rev) {
- assertNotEmpty(id, "id");
- assertNotEmpty(rev, "rev");
- final URI uri = buildUri(getDBUri()).pathEncoded(id).query("rev", rev).build();
- return delete(uri);
- }
-
- /**
- * Performs bulk documents create and update request.
- * @param objects The {@link List} of documents objects.
- * @param newEdits If false, prevents the database from assigning documents new revision IDs.
- * @return {@code List} Containing the resulted entries.
- */
- public List bulk(List> objects, boolean newEdits) {
- assertNotEmpty(objects, "objects");
- HttpResponse response = null;
- try {
- final String newEditsVal = newEdits ? "\"new_edits\": true, " : "\"new_edits\": false, ";
- final String json = String.format("{%s%s%s}", newEditsVal, "\"docs\": ", getGson().toJson(objects));
- final URI uri = buildUri(getDBUri()).path("_bulk_docs").build();
- response = post(uri, json);
- return getResponseList(response);
- } finally {
- close(response);
- }
- }
-
- /**
- * Saves an attachment to a new document with a generated UUID as the document id.
- *
To retrieve an attachment, see {@link #find(String)}.
- * @param in The {@link InputStream} holding the binary data.
- * @param name The attachment name.
- * @param contentType The attachment "Content-Type".
- * @return {@link Response}
- */
- public Response saveAttachment(InputStream in, String name, String contentType) {
- assertNotEmpty(in, "in");
- assertNotEmpty(name, "name");
- assertNotEmpty(contentType, "ContentType");
- final URI uri = buildUri(getDBUri()).path(generateUUID()).path("/").path(name).build();
- return put(uri, in, contentType);
- }
-
- /**
- * Saves an attachment to an existing document given both a document id
- * and revision, or save to a new document given only the id, and rev as {@code null}.
- *
To retrieve an attachment, see {@link #find(String)}.
- * @param in The {@link InputStream} holding the binary data.
- * @param name The attachment name.
- * @param contentType The attachment "Content-Type".
- * @param docId The document id to save the attachment under, or {@code null} to save under a new document.
- * @param docRev The document revision to save the attachment under, or {@code null} when saving to a new document.
- * @return {@link Response}
- */
- public Response saveAttachment(InputStream in, String name, String contentType, String docId, String docRev) {
- assertNotEmpty(in, "in");
- assertNotEmpty(name, "name");
- assertNotEmpty(contentType, "ContentType");
- assertNotEmpty(docId, "docId");
- final URI uri = buildUri(getDBUri()).pathEncoded(docId).path("/").path(name).query("rev", docRev).build();
- return put(uri, in, contentType);
- }
-
- /**
- * Invokes an Update Handler.
- *
- * @param updateHandlerUri The Update Handler URI, in the format: designDoc/update1
- * @param docId The document id to update.
- * @param params The query parameters as {@link Params}.
- * @return The output of the request.
- */
- public String invokeUpdateHandler(String updateHandlerUri, String docId, Params params) {
- assertNotEmpty(updateHandlerUri, "uri");
- assertNotEmpty(docId, "docId");
- final String[] v = updateHandlerUri.split("/");
- final String path = String.format("_design/%s/_update/%s/", v[0], v[1]);
- final URI uri = buildUri(getDBUri()).path(path).path(docId).query(params).build();
- final HttpResponse response = executeRequest(new HttpPut(uri));
- return streamToString(getStream(response));
- }
-
- /**
- * Executes a HTTP request.
- *
Note: The response must be closed after use to release the connection.
- * @param request The HTTP request to execute.
- * @return {@link HttpResponse}
- */
- public HttpResponse executeRequest(HttpRequestBase request) {
- try {
- return httpClient.execute(host, request, createContext());
- } catch (IOException e) {
- request.abort();
- throw new CouchDbException("Error executing request. ", e);
- }
- }
-
- /**
- * Synchronize all design documents with the database.
- */
- public void syncDesignDocsWithDb() {
- design().synchronizeAllWithDb();
- }
-
- /**
- * Sets a {@link GsonBuilder} to create {@link Gson} instance.
- *
Useful for registering custom serializers/deserializers, such as JodaTime classes.
- * @param gsonBuilder The {@link GsonBuilder}
- */
- public void setGsonBuilder(GsonBuilder gsonBuilder) {
- this.gson = initGson(gsonBuilder);
- }
-
- /**
- * @return The base URI.
- */
- public URI getBaseUri() {
- return baseURI;
- }
-
- /**
- * @return The database URI.
- */
- public URI getDBUri() {
- return dbURI;
- }
-
- /**
- * @return The Gson instance.
- */
- public Gson getGson() {
- return gson;
- }
-
- // End - Public API
-
- /**
- * Performs a HTTP GET request.
- * @return {@link InputStream}
- */
- InputStream get(HttpGet httpGet) {
- HttpResponse response = executeRequest(httpGet);
- return getStream(response);
- }
-
- /**
- * Performs a HTTP GET request.
- * @return {@link InputStream}
- */
- InputStream get(URI uri) {
- HttpGet get = new HttpGet(uri);
- get.addHeader("Accept", "application/json");
- return get(get);
- }
-
- /**
- * Performs a HTTP GET request.
- * @return An object of type T
- */
- T get(URI uri, Class classType) {
- InputStream in = null;
- try {
- in = get(uri);
- return getGson().fromJson(new InputStreamReader(in, "UTF-8"), classType);
- } catch (UnsupportedEncodingException e) {
- throw new CouchDbException(e);
- } finally {
- close(in);
- }
- }
-
- /**
- * 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 {
- final JsonObject json = getGson().toJsonTree(object).getAsJsonObject();
- String id = getAsString(json, "_id");
- String rev = getAsString(json, "_rev");
- if(newEntity) { // save
- assertNull(rev, "rev");
- id = (id == null) ? generateUUID() : id;
- } else { // update
- assertNotEmpty(id, "id");
- assertNotEmpty(rev, "rev");
- }
- final HttpPut put = new HttpPut(buildUri(uri).pathEncoded(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 {
- final HttpPut httpPut = new HttpPut(uri);
- final InputStreamEntity entity = new InputStreamEntity(instream, -1);
- entity.setContentType(contentType);
- httpPut.setEntity(entity);
- response = executeRequest(httpPut);
- 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);
- }
- }
-
- // Helpers
-
- /**
- * Validates a HTTP response; on error cases logs status and throws relevant exceptions.
- * @param response The HTTP response.
- */
- void validate(HttpResponse response) throws IOException {
- final int code = response.getStatusLine().getStatusCode();
- if(code == 200 || code == 201 || code == 202) { // success (ok | created | accepted)
- return;
- }
- String reason = response.getStatusLine().getReasonPhrase();
- switch (code) {
- case HttpStatus.SC_NOT_FOUND: {
- throw new NoDocumentException(reason);
- }
- case HttpStatus.SC_CONFLICT: {
- throw new DocumentConflictException(reason);
- }
- default: { // other errors: 400 | 401 | 500 etc.
- throw new CouchDbException(reason += EntityUtils.toString(response.getEntity()));
- }
- }
- }
-
- /**
- * @param response The {@link HttpResponse}
- * @return {@link Response}
- */
- private Response getResponse(HttpResponse response) throws CouchDbException {
- InputStreamReader reader = new InputStreamReader(getStream(response), Charsets.UTF_8);
- return getGson().fromJson(reader, Response.class);
- }
-
- /**
- * @param response The {@link HttpResponse}
- * @return {@link Response}
- */
- private List getResponseList(HttpResponse response) throws CouchDbException {
- InputStream instream = getStream(response);
- Reader reader = new InputStreamReader(instream, Charsets.UTF_8);
- return getGson().fromJson(reader, new TypeToken>(){}.getType());
- }
-
- /**
- * Sets a JSON String as a request entity.
- * @param httpRequest The request to set entity.
- * @param json The JSON String to set.
- */
- private void setEntity(HttpEntityEnclosingRequestBase httpRequest, String json) {
- StringEntity entity = new StringEntity(json, "UTF-8");
- entity.setContentType("application/json");
- httpRequest.setEntity(entity);
- }
-
- /**
- * Builds {@link Gson} and registers any required serializer/deserializer.
- * @return {@link Gson} instance
- */
- private Gson initGson(GsonBuilder gsonBuilder) {
- gsonBuilder.registerTypeAdapter(JsonObject.class, new JsonDeserializer() {
- public JsonObject deserialize(JsonElement json,
- Type typeOfT, JsonDeserializationContext context)
- throws JsonParseException {
- return json.getAsJsonObject();
- }
- });
- gsonBuilder.registerTypeAdapter(JsonObject.class, new JsonSerializer() {
- public JsonElement serialize(JsonObject src, Type typeOfSrc,
- JsonSerializationContext context) {
- return src.getAsJsonObject();
- }
-
- });
- return gsonBuilder.create();
- }
+ static final Log log = LogFactory.getLog(CouchDbClient.class);
+
+ private URI baseURI;
+ private URI dbURI;
+ private Gson gson;
+ private CouchDbContext context;
+ private CouchDbDesign design;
+ private Local local;
+ final HttpClient httpClient;
+ final HttpHost host;
+
+ CouchDbClientBase() {
+ this(new CouchDbConfig());
+ }
+
+ CouchDbClientBase(CouchDbConfig config) {
+ final CouchDbProperties props = config.getProperties();
+ this.httpClient = createHttpClient(props);
+ this.gson = initGson(new GsonBuilder());
+ this.host = new HttpHost(props.getHost(), props.getPort(), props.getProtocol());
+
+ final String path = props.getPath() != null ? props.getPath() : "";
+ this.baseURI = buildUri().scheme(props.getProtocol()).host(props.getHost()).port(props.getPort()).path("/")
+ .path(path).build();
+ this.dbURI = buildUri(baseURI).path(props.getDbName()).path("/").build();
+
+ this.context = new CouchDbContext(this, props);
+ this.design = new CouchDbDesign(this);
+ this.local = new Local(this);
+ }
+
+ // Client(s) provided implementation
+
+ /**
+ * @return {@link HttpClient} instance for HTTP request execution.
+ */
+ abstract HttpClient createHttpClient(CouchDbProperties properties);
+
+ /**
+ * @return {@link HttpContext} instance for HTTP request execution.
+ */
+ abstract HttpContext createContext();
+
+ /**
+ * Shuts down the connection manager used by this client instance.
+ */
+ abstract void shutdown();
+
+ // Public API
+
+ /**
+ * Provides access to DB server APIs.
+ *
+ * @return {@link CouchDbContext}
+ */
+ public CouchDbContext context() {
+ return context;
+ }
+
+ /**
+ * Provides access to CouchDB Design Documents.
+ *
+ * @return {@link CouchDbDesign}
+ */
+ public CouchDbDesign design() {
+ return design;
+ }
+
+ /**
+ * Provides access to CouchDB View APIs.
+ *
+ * @param viewId The view id.
+ * @return {@link View}
+ */
+ public View view(String viewId) {
+ return new View(this, viewId);
+ }
+
+ public Local local() {
+ return local;
+ }
+
+ /**
+ * Provides access to CouchDB replication APIs.
+ *
+ * @return {@link Replication}
+ */
+ public Replication replication() {
+ return new Replication(this);
+ }
+
+ /**
+ * Provides access to the replicator database.
+ *
+ * @return {@link Replicator}
+ */
+ public Replicator replicator() {
+ return new Replicator(this);
+ }
+
+ /**
+ * Provides access to Change Notifications API.
+ *
+ * @return {@link Changes}
+ */
+ public Changes changes() {
+ return new Changes(this);
+ }
+
+ /**
+ * Purge operation over database
+ *
+ * @param toPurge - Map of Ids and the list of revs to purge
+ * @return Ids and revs purged
+ */
+ public PurgeResponse purge(Map> toPurge) {
+ assertNotEmpty(toPurge, "to purge map");
+ HttpResponse response = null;
+ Reader reader = null;
+ try {
+ String jsonToPurge = getGson().toJson(toPurge);
+ response = post(buildUri(getDBUri()).path("_purge").build(), jsonToPurge);
+ reader = new InputStreamReader(getStream(response), Charsets.UTF_8);
+ return getGson().fromJson(reader, PurgeResponse.class);
+ } finally {
+ close(reader);
+ close(response);
+ }
+ }
+
+ /**
+ * Finds an Object of the specified type.
+ *
+ * @param Object type.
+ * @param classType The class of type T.
+ * @param id The document id.
+ * @return An object of type T.
+ * @throws NoDocumentException If the document is not found in the database.
+ */
+ public T find(Class classType, String id) {
+ assertNotEmpty(classType, "Class");
+ assertNotEmpty(id, "id");
+ final URI uri = buildUri(getDBUri()).pathEncoded(id).build();
+ return get(uri, classType);
+ }
+
+ /**
+ * Finds an Object of the specified type.
+ *
+ * @param Object type.
+ * @param classType The class of type T.
+ * @param id The document id.
+ * @param params Extra parameters to append.
+ * @return An object of type T.
+ * @throws NoDocumentException If the document is not found in the database.
+ */
+ public T find(Class classType, String id, Params params) {
+ assertNotEmpty(classType, "Class");
+ assertNotEmpty(id, "id");
+ final URI uri = buildUri(getDBUri()).pathEncoded(id).query(params).build();
+ return get(uri, classType);
+ }
+
+ /**
+ * Finds an Object of the specified type.
+ *
+ * @param Object type.
+ * @param classType The class of type T.
+ * @param id The document _id field.
+ * @param rev The document _rev field.
+ * @return An object of type T.
+ * @throws NoDocumentException If the document is not found in the database.
+ */
+ public T find(Class classType, String id, String rev) {
+ assertNotEmpty(classType, "Class");
+ assertNotEmpty(id, "id");
+ assertNotEmpty(id, "rev");
+ final URI uri = buildUri(getDBUri()).pathEncoded(id).query("rev", rev).build();
+ return get(uri, classType);
+ }
+
+ /**
+ * This method finds any document given a URI.
+ *
+ * The URI must be URI-encoded.
+ *
+ * @param The class type.
+ * @param classType The class of type T.
+ * @param uri The URI as string.
+ * @return An object of type T.
+ */
+ public T findAny(Class classType, String uri) {
+ assertNotEmpty(classType, "Class");
+ assertNotEmpty(uri, "uri");
+ return get(URI.create(uri), classType);
+ }
+
+ /**
+ * Finds a document and return the result as {@link InputStream}.
+ *
+ * Note: The stream must be closed after use to release the connection.
+ *
+ * @param id The document _id field.
+ * @return The result as {@link InputStream}
+ * @throws NoDocumentException If the document is not found in the database.
+ * @see #find(String, String)
+ */
+ public InputStream find(String id) {
+ assertNotEmpty(id, "id");
+ return get(buildUri(getDBUri()).path(id).build());
+ }
+
+ /**
+ * Finds a document given id and revision and returns the result as {@link InputStream}.
+ *
+ * Note: The stream must be closed after use to release the connection.
+ *
+ * @param id The document _id field.
+ * @param rev The document _rev field.
+ * @return The result as {@link InputStream}
+ * @throws NoDocumentException If the document is not found in the database.
+ */
+ public InputStream find(String id, String rev) {
+ assertNotEmpty(id, "id");
+ assertNotEmpty(rev, "rev");
+ final URI uri = buildUri(getDBUri()).path(id).query("rev", rev).build();
+ return get(uri);
+ }
+
+ /**
+ * Find documents using a declarative JSON querying syntax.
+ *
+ * @param The class type.
+ * @param jsonQuery The JSON query string.
+ * @param classOfT The class of type T.
+ * @return The result of the query as a {@code List }
+ * @throws CouchDbException If the query failed to execute or the request is invalid.
+ */
+ public List findDocs(String jsonQuery, Class classOfT) {
+ assertNotEmpty(jsonQuery, "jsonQuery");
+ HttpResponse response = null;
+ try {
+ response = post(buildUri(getDBUri()).path("_find").build(), jsonQuery);
+ Reader reader = new InputStreamReader(getStream(response), Charsets.UTF_8);
+ JsonArray jsonArray = new JsonParser().parse(reader).getAsJsonObject().getAsJsonArray("docs");
+ List list = new ArrayList();
+ for (JsonElement jsonElem : jsonArray) {
+ JsonElement elem = jsonElem.getAsJsonObject();
+ T t = this.gson.fromJson(elem, classOfT);
+ list.add(t);
+ }
+ return list;
+ } finally {
+ close(response);
+ }
+ }
+
+ /**
+ * Checks if a document exist in the database.
+ *
+ * @param id The document _id field.
+ * @return true If the document is found, false otherwise.
+ */
+ public boolean contains(String id) {
+ assertNotEmpty(id, "id");
+ HttpResponse response = null;
+ try {
+ response = head(buildUri(getDBUri()).pathEncoded(id).build());
+ } catch (NoDocumentException e) {
+ return false;
+ } finally {
+ close(response);
+ }
+ return true;
+ }
+
+ /**
+ * Saves an object in the database, using HTTP PUT request.
+ *
+ * If the object doesn't have an _id value, the code will assign a UUID as the document
+ * id.
+ *
+ * @param object The object to save
+ * @throws DocumentConflictException If a conflict is detected during the save.
+ * @return {@link Response}
+ */
+ public Response save(Object object) {
+ return put(getDBUri(), object, true);
+ }
+
+ /**
+ * Saves an object in the database using HTTP POST request.
+ *
+ * The database will be responsible for generating the document id.
+ *
+ * @param object The object to save
+ * @return {@link Response}
+ */
+ public Response post(Object object) {
+ assertNotEmpty(object, "object");
+ HttpResponse response = null;
+ try {
+ URI uri = buildUri(getDBUri()).build();
+ response = post(uri, getGson().toJson(object));
+ return getResponse(response);
+ } finally {
+ close(response);
+ }
+ }
+
+ /**
+ * Saves a document with batch=ok query param.
+ *
+ * @param object The object to save.
+ */
+ public void batch(Object object) {
+ assertNotEmpty(object, "object");
+ HttpResponse response = null;
+ try {
+ URI uri = buildUri(getDBUri()).query("batch", "ok").build();
+ response = post(uri, getGson().toJson(object));
+ } finally {
+ close(response);
+ }
+ }
+
+ /**
+ * Updates an object in the database, the object must have the correct _id and _rev
+ * values.
+ *
+ * @param object The object to update
+ * @throws DocumentConflictException If a conflict is detected during the update.
+ * @return {@link Response}
+ */
+ public Response update(Object object) {
+ return put(getDBUri(), object, false);
+ }
+
+ /**
+ * Removes a document from the database.
+ *
+ * The object must have the correct _id and _rev values.
+ *
+ * @param object The document to remove as object.
+ * @throws NoDocumentException If the document is not found in the database.
+ * @return {@link Response}
+ */
+ public Response remove(Object object) {
+ assertNotEmpty(object, "object");
+ JsonObject jsonObject = getGson().toJsonTree(object).getAsJsonObject();
+ final String id = getAsString(jsonObject, "_id");
+ final String rev = getAsString(jsonObject, "_rev");
+ return remove(id, rev);
+ }
+
+ /**
+ * Removes a document from the database given both a document _id and _rev values.
+ *
+ * @param id The document _id field.
+ * @param rev The document _rev field.
+ * @throws NoDocumentException If the document is not found in the database.
+ * @return {@link Response}
+ */
+ public Response remove(String id, String rev) {
+ assertNotEmpty(id, "id");
+ assertNotEmpty(rev, "rev");
+ final URI uri = buildUri(getDBUri()).pathEncoded(id).query("rev", rev).build();
+ return delete(uri);
+ }
+
+ /**
+ * Performs bulk documents create and update request.
+ *
+ * @param objects The {@link List} of documents objects.
+ * @param newEdits If false, prevents the database from assigning documents new revision IDs.
+ * @return {@code List} Containing the resulted entries.
+ */
+ public List bulk(List> objects, boolean newEdits) {
+ assertNotEmpty(objects, "objects");
+ HttpResponse response = null;
+ try {
+ final String newEditsVal = newEdits ? "\"new_edits\": true, " : "\"new_edits\": false, ";
+ final String json = String.format("{%s%s%s}", newEditsVal, "\"docs\": ", getGson().toJson(objects));
+ final URI uri = buildUri(getDBUri()).path("_bulk_docs").build();
+ response = post(uri, json);
+ return getResponseList(response);
+ } finally {
+ close(response);
+ }
+ }
+
+ /**
+ * Saves an attachment to a new document with a generated UUID as the document id.
+ *
+ * To retrieve an attachment, see {@link #find(String)}.
+ *
+ * @param in The {@link InputStream} holding the binary data.
+ * @param name The attachment name.
+ * @param contentType The attachment "Content-Type".
+ * @return {@link Response}
+ */
+ public Response saveAttachment(InputStream in, String name, String contentType) {
+ assertNotEmpty(in, "in");
+ assertNotEmpty(name, "name");
+ assertNotEmpty(contentType, "ContentType");
+ final URI uri = buildUri(getDBUri()).path(generateUUID()).path("/").path(name).build();
+ return put(uri, in, contentType);
+ }
+
+ /**
+ * Saves an attachment to an existing document given both a document id and revision, or save to a new document
+ * given only the id, and rev as {@code null}.
+ *
+ * To retrieve an attachment, see {@link #find(String)}.
+ *
+ * @param in The {@link InputStream} holding the binary data.
+ * @param name The attachment name.
+ * @param contentType The attachment "Content-Type".
+ * @param docId The document id to save the attachment under, or {@code null} to save under a new document.
+ * @param docRev The document revision to save the attachment under, or {@code null} when saving to a new document.
+ * @return {@link Response}
+ */
+ public Response saveAttachment(InputStream in, String name, String contentType, String docId, String docRev) {
+ assertNotEmpty(in, "in");
+ assertNotEmpty(name, "name");
+ assertNotEmpty(contentType, "ContentType");
+ assertNotEmpty(docId, "docId");
+ final URI uri = buildUri(getDBUri()).pathEncoded(docId).path("/").path(name).query("rev", docRev).build();
+ return put(uri, in, contentType);
+ }
+
+ /**
+ * Invokes an Update Handler.
+ *
+ *