From 76303fc3471954405efa44a6706653edbd14a3de Mon Sep 17 00:00:00 2001 From: orolle Date: Sun, 23 Jul 2017 16:27:27 +0200 Subject: [PATCH 1/2] Cleaned some tests. --- src/main/java/org/lightcouch/Changes.java | 329 ++-- .../org/lightcouch/CouchDbClientBase.java | 1387 +++++++++-------- .../tests/ChangeNotificationsTest.java | 181 ++- 3 files changed, 1024 insertions(+), 873 deletions(-) diff --git a/src/main/java/org/lightcouch/Changes.java b/src/main/java/org/lightcouch/Changes.java index 0e54f72..b7e61d7 100644 --- a/src/main/java/org/lightcouch/Changes.java +++ b/src/main/java/org/lightcouch/Changes.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lightcouch; import java.io.BufferedReader; @@ -26,15 +25,18 @@ import org.lightcouch.ChangesResult.Row; import com.google.gson.Gson; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; /** - *

Contains the Change Notifications API, supports normal and continuous feed Changes. + *

+ * Contains the Change Notifications API, supports normal and continuous feed Changes. *

Usage Example:

*
- * // feed type normal 
+ * // feed type normal
  * String since = dbClient.context().info().getUpdateSeq(); // latest update seq
  * ChangesResult changeResult = dbClient.changes()
- *	.since(since) 
+ *	.since(since)
  *	.limit(10)
  *	.filter("example/filter")
  *	.getChanges();
@@ -46,164 +48,187 @@
  *
  * // feed type continuous
  * Changes changes = dbClient.changes()
- *	.includeDocs(true) 
+ *	.includeDocs(true)
  *	.heartBeat(30000)
- *	.continuousChanges(); 
- * 
- * while (changes.hasNext()) { 
+ *	.continuousChanges();
+ *
+ * while (changes.hasNext()) {
  *	ChangesResult.Row feed = changes.next();
  *  String docId = feed.getId();
  *  JsonObject doc = feed.getDoc();
  *	// changes.stop(); // stop continuous feed
  * }
  * 
+ * * @see ChangesResult * @since 0.0.2 * @author Ahmed Yehia */ public class Changes { - - private BufferedReader reader; - private HttpGet httpGet; - private Row nextRow; - private boolean stop; - - private CouchDbClientBase dbc; - private Gson gson; - private URIBuilder uriBuilder; - - 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. - */ - 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. - */ - 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. - */ - 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 HttpRequestBase httpRequest; + private Row nextRow; + private boolean stop; + + private CouchDbClientBase dbc; + private Gson gson; + private URIBuilder uriBuilder; + + private String selector; + + 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. + */ + 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; + } + + // Query Params + public Changes selector(String json) { + uriBuilder.query("filter", "_selector"); + this.selector = json; + return this; + } + + /** + * Checks whether a feed is available in the continuous stream, blocking until a feed is received. + */ + 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. + */ + public ChangesResult getChanges() { + final URI uri = uriBuilder.query("feed", "normal").build(); + if (selector == null) { + return dbc.get(uri, ChangesResult.class); + } else { + return dbc.post(uri, selector, 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() { + httpRequest.abort(); + CouchDbUtil.close(getReader()); + } } diff --git a/src/main/java/org/lightcouch/CouchDbClientBase.java b/src/main/java/org/lightcouch/CouchDbClientBase.java index 08208b7..9f7e4bf 100644 --- a/src/main/java/org/lightcouch/CouchDbClientBase.java +++ b/src/main/java/org/lightcouch/CouchDbClientBase.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lightcouch; import static org.lightcouch.CouchDbUtil.assertNotEmpty; @@ -69,666 +68,740 @@ /** * 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. - * @see CouchDbContext - */ - public CouchDbContext context() { - return context; - } - - /** - * Provides access to CouchDB Design Documents. - * @see CouchDbDesign - */ - public CouchDbDesign design() { - return design; - } - - /** - * Provides access to CouchDB View APIs. - * @see View - */ - public View view(String viewId) { - return new View(this, viewId); - } - - /** - * Provides access to CouchDB replication APIs. - * @see Replication - */ - public Replication replication() { - return new Replication(this); - } - - /** - * Provides access to the replicator database. - * @see Replicator - */ - public Replicator replicator() { - return new Replicator(this); - } - - /** - * Provides access to Change Notifications API. - * @see 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()).path(id, true).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()).path(id, true).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()).path(id, true).query("rev", rev).build(); - return get(uri, classType); - } - - /** - * This method finds any document given a URI. - *

The URI must be URI-encoded. - * @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 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()).path(id, true).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()).path(id, true).query("rev", rev).build(); - return delete(uri); - } - - /** - * Performs a Bulk Documents insert request. - * @param objects The {@link List} of objects. - * @param allOrNothing Indicates whether the request has all-or-nothing semantics. - * @return {@code List} Containing the resulted entries. - */ - public List bulk(List objects, boolean allOrNothing) { - assertNotEmpty(objects, "objects"); - HttpResponse response = null; - try { - final String allOrNothingVal = allOrNothing ? "\"all_or_nothing\": true, " : ""; - final URI uri = buildUri(getDBUri()).path("_bulk_docs").build(); - final String json = String.format("{%s%s%s}", allOrNothingVal, "\"docs\": ", getGson().toJson(objects)); - 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 instream 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 instream 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. - * @throws DocumentConflictException - * @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()).path(docId, true).path("/").path(name).query("rev", docRev).build(); - return put(uri, in, contentType); - } - - /** - * Invokes an Update Handler. - *

-	 * String query = "field=foo&value=bar";
-	 * String output = dbClient.invokeUpdateHandler("designDoc/update1", "docId", query);
-	 * 
- * @param updateHandlerUri The Update Handler URI, in the format: designDoc/update1 - * @param docId The document id to update. - * @param query The query string parameters, e.g, field=field1&value=value1 - * @return The output of the request. - */ - public String invokeUpdateHandler(String updateHandlerUri, String docId, String query) { - 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(query).build(); - final HttpResponse response = executeRequest(new HttpPut(uri)); - return streamToString(getStream(response)); - } - - /** - * Invokes an Update Handler. - *

Use this method in particular when the docId contain special characters such as slashes (/). - *

-	 * Params params = new Params()
-	 *	.addParam("field", "foo")
-	 *	.addParam("value", "bar"); 
-	 * String output = dbClient.invokeUpdateHandler("designDoc/update1", "docId", params);
-	 * 
- * @param updateHandlerUri The Update Handler URI, in the format: designDoc/update1 - * @param docId The document id to update. - * @param query 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. - */ - 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).path(id, true).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; + 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. + * + * @see CouchDbContext + */ + public CouchDbContext context() { + return context; + } + + /** + * Provides access to CouchDB Design Documents. + * + * @see CouchDbDesign + */ + public CouchDbDesign design() { + return design; + } + + /** + * Provides access to CouchDB View APIs. + * + * @see View + */ + public View view(String viewId) { + return new View(this, viewId); + } + + /** + * Provides access to CouchDB replication APIs. + * + * @see Replication + */ + public Replication replication() { + return new Replication(this); + } + + /** + * Provides access to the replicator database. + * + * @see Replicator + */ + public Replicator replicator() { + return new Replicator(this); + } + + /** + * Provides access to Change Notifications API. + * + * @see 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()).path(id, true).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()).path(id, true).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()).path(id, true).query("rev", rev).build(); + return get(uri, classType); + } + + /** + * This method finds any document given a URI. + *

+ * The URI must be URI-encoded. + * + * @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 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()).path(id, true).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()).path(id, true).query("rev", rev).build(); + return delete(uri); + } + + /** + * Performs a Bulk Documents insert request. + * + * @param objects The {@link List} of objects. + * @param allOrNothing Indicates whether the request has all-or-nothing semantics. + * @return {@code List} Containing the resulted entries. + */ + public List bulk(List objects, boolean allOrNothing) { + assertNotEmpty(objects, "objects"); + HttpResponse response = null; + try { + final String allOrNothingVal = allOrNothing ? "\"all_or_nothing\": true, " : ""; + final URI uri = buildUri(getDBUri()).path("_bulk_docs").build(); + final String json = String.format("{%s%s%s}", allOrNothingVal, "\"docs\": ", getGson().toJson(objects)); + 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 instream 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 instream 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. + * @throws DocumentConflictException + * @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()).path(docId, true).path("/").path(name).query("rev", docRev).build(); + return put(uri, in, contentType); + } + + /** + * Invokes an Update Handler. + *

+   * String query = "field=foo&value=bar";
+   * String output = dbClient.invokeUpdateHandler("designDoc/update1", "docId", query);
+   * 
+ * + * @param updateHandlerUri The Update Handler URI, in the format: designDoc/update1 + * @param docId The document id to update. + * @param query The query string parameters, e.g, field=field1&value=value1 + * @return The output of the request. + */ + public String invokeUpdateHandler(String updateHandlerUri, String docId, String query) { + 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(query).build(); + final HttpResponse response = executeRequest(new HttpPut(uri)); + return streamToString(getStream(response)); + } + + /** + * Invokes an Update Handler. + *

+ * Use this method in particular when the docId contain special characters such as slashes (/). + *

+   * Params params = new Params()
+   *	.addParam("field", "foo")
+   *	.addParam("value", "bar");
+   * String output = dbClient.invokeUpdateHandler("designDoc/update1", "docId", params);
+   * 
+ * + * @param updateHandlerUri The Update Handler URI, in the format: designDoc/update1 + * @param docId The document id to update. + * @param query 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. + */ + 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).path(id, true).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 POST request. + * + * @return {@link HttpResponse} + */ + InputStream post(HttpPost post, String json) { + setEntity(post, json); + HttpResponse resp = executeRequest(post); + return getStream(resp); + } + + /** + * Performs a HTTP GET request. + * + * @return An object of type T + */ + T post(URI uri, String json, Class classType) { + InputStream in = null; + try { + in = getStream(post(uri, json)); + return getGson().fromJson(new InputStreamReader(in, "UTF-8"), classType); + } catch (UnsupportedEncodingException e) { + throw new CouchDbException(e); + } finally { + close(in); + } + } + + /** + * 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(); + } } diff --git a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java index bd3d4c8..7c020cd 100644 --- a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java +++ b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.lightcouch.tests; import static org.hamcrest.CoreMatchers.is; @@ -36,67 +35,121 @@ import com.google.gson.JsonObject; public class ChangeNotificationsTest { - - private static CouchDbClient dbClient; - - @BeforeClass - public static void setUpClass() { - dbClient = new CouchDbClient(); - } - - @AfterClass - public static void tearDownClass() { - dbClient.shutdown(); - } - - @Test - public void changes_normalFeed() { - dbClient.save(new Foo()); - - ChangesResult changes = dbClient.changes() - .includeDocs(true) - .limit(1) - .getChanges(); - - List rows = changes.getResults(); - - for (Row row : rows) { - List revs = row.getChanges(); - String docId = row.getId(); - JsonObject doc = row.getDoc(); - - assertNotNull(revs); - assertNotNull(docId); - assertNotNull(doc); - } - - assertThat(rows.size(), is(1)); - } - - @Test - public void changes_continuousFeed() { - dbClient.save(new Foo()); - - CouchDbInfo dbInfo = dbClient.context().info(); - String since = dbInfo.getUpdateSeq(); - - Changes changes = dbClient.changes() - .includeDocs(true) - .since(since) - .heartBeat(30000) - .continuousChanges(); - - Response response = dbClient.save(new Foo()); - - while (changes.hasNext()) { - ChangesResult.Row feed = changes.next(); - final JsonObject feedObject = feed.getDoc(); - final String docId = feed.getId(); - - assertEquals(response.getId(), docId); - assertNotNull(feedObject); - - changes.stop(); - } - } + + private static CouchDbClient dbClient; + + @BeforeClass + public static void setUpClass() { + dbClient = new CouchDbClient(); + } + + @AfterClass + public static void tearDownClass() { + dbClient.shutdown(); + } + + @Test + public void changes_normalFeed() { + dbClient.save(new Foo()); + + ChangesResult changes = dbClient.changes() + .includeDocs(true) + .limit(1) + .getChanges(); + + List rows = changes.getResults(); + + for (Row row : rows) { + List revs = row.getChanges(); + String docId = row.getId(); + JsonObject doc = row.getDoc(); + + assertNotNull(revs); + assertNotNull(docId); + assertNotNull(doc); + } + + assertThat(rows.size(), is(1)); + } + + @Test + public void changes_normalFeed_selector() { + dbClient.save(new Foo()); + + ChangesResult changes = dbClient.changes() + .includeDocs(true) + .limit(1) + .selector("{\"selector\":{\"_id\": {\"$gt\": null}}}") + .getChanges(); + + List rows = changes.getResults(); + + for (Row row : rows) { + List revs = row.getChanges(); + String docId = row.getId(); + JsonObject doc = row.getDoc(); + + assertNotNull(revs); + assertNotNull(docId); + assertNotNull(doc); + } + + assertThat(rows.size(), is(1)); + } + + @Test + public void changes_continuousFeed() { + dbClient.save(new Foo()); + + CouchDbInfo dbInfo = dbClient.context().info(); + String since = dbInfo.getUpdateSeq(); + + Changes changes = dbClient.changes() + .includeDocs(true) + .since(since) + .heartBeat(1000) + .continuousChanges(); + + Response response = dbClient.save(new Foo()); + + while (changes.hasNext()) { + ChangesResult.Row feed = changes.next(); + final JsonObject feedObject = feed.getDoc(); + final String docId = feed.getId(); + + assertEquals(response.getId(), docId); + assertNotNull(feedObject); + + changes.stop(); + } + } + + @Test + public void changes_continuousFeed_selector() { + dbClient.save(new Foo()); + + CouchDbInfo dbInfo = dbClient.context().info(); + String since = dbInfo.getUpdateSeq(); + + Changes changes = dbClient.changes() + .includeDocs(true) + .since(since) + .heartBeat(1000) + .selector("{\"selector\":{\"_id\": {\"$gt\": null}}}") + .continuousChanges(); + + Response response = dbClient.save(new Foo()); + + while (changes.hasNext()) { + ChangesResult.Row feed = changes.next(); + final JsonObject feedObject = feed.getDoc(); + final String docId = feed.getId(); + System.out.println("next()=" + docId); + + assertEquals(response.getId(), docId); + assertNotNull(feedObject); + + changes.stop(); + } + } } From f56e5a25940a79a5ff54090f0951b352fcea74ab Mon Sep 17 00:00:00 2001 From: orolle Date: Sun, 23 Jul 2017 16:37:29 +0200 Subject: [PATCH 2/2] Fixed intention to tabs. --- src/main/java/org/lightcouch/Changes.java | 330 ++-- .../org/lightcouch/CouchDbClientBase.java | 1458 ++++++++--------- .../tests/ChangeNotificationsTest.java | 224 +-- 3 files changed, 1006 insertions(+), 1006 deletions(-) diff --git a/src/main/java/org/lightcouch/Changes.java b/src/main/java/org/lightcouch/Changes.java index b7e61d7..5c5a0b2 100644 --- a/src/main/java/org/lightcouch/Changes.java +++ b/src/main/java/org/lightcouch/Changes.java @@ -66,169 +66,169 @@ */ public class Changes { - private BufferedReader reader; - private HttpRequestBase httpRequest; - private Row nextRow; - private boolean stop; - - private CouchDbClientBase dbc; - private Gson gson; - private URIBuilder uriBuilder; - - private String selector; - - 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. - */ - 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; - } - - // Query Params - public Changes selector(String json) { - uriBuilder.query("filter", "_selector"); - this.selector = json; - return this; - } - - /** - * Checks whether a feed is available in the continuous stream, blocking until a feed is received. - */ - 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. - */ - public ChangesResult getChanges() { - final URI uri = uriBuilder.query("feed", "normal").build(); - if (selector == null) { - return dbc.get(uri, ChangesResult.class); - } else { - return dbc.post(uri, selector, 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() { - httpRequest.abort(); - CouchDbUtil.close(getReader()); - } + private BufferedReader reader; + private HttpRequestBase httpRequest; + private Row nextRow; + private boolean stop; + + private CouchDbClientBase dbc; + private Gson gson; + private URIBuilder uriBuilder; + + private String selector; + + 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. + */ + 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; + } + + // Query Params + public Changes selector(String json) { + uriBuilder.query("filter", "_selector"); + this.selector = json; + return this; + } + + /** + * Checks whether a feed is available in the continuous stream, blocking until a feed is received. + */ + 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. + */ + public ChangesResult getChanges() { + final URI uri = uriBuilder.query("feed", "normal").build(); + if (selector == null) { + return dbc.get(uri, ChangesResult.class); + } else { + return dbc.post(uri, selector, 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() { + httpRequest.abort(); + CouchDbUtil.close(getReader()); + } } diff --git a/src/main/java/org/lightcouch/CouchDbClientBase.java b/src/main/java/org/lightcouch/CouchDbClientBase.java index 9f7e4bf..b9cb662 100644 --- a/src/main/java/org/lightcouch/CouchDbClientBase.java +++ b/src/main/java/org/lightcouch/CouchDbClientBase.java @@ -75,733 +75,733 @@ */ 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. - * - * @see CouchDbContext - */ - public CouchDbContext context() { - return context; - } - - /** - * Provides access to CouchDB Design Documents. - * - * @see CouchDbDesign - */ - public CouchDbDesign design() { - return design; - } - - /** - * Provides access to CouchDB View APIs. - * - * @see View - */ - public View view(String viewId) { - return new View(this, viewId); - } - - /** - * Provides access to CouchDB replication APIs. - * - * @see Replication - */ - public Replication replication() { - return new Replication(this); - } - - /** - * Provides access to the replicator database. - * - * @see Replicator - */ - public Replicator replicator() { - return new Replicator(this); - } - - /** - * Provides access to Change Notifications API. - * - * @see 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()).path(id, true).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()).path(id, true).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()).path(id, true).query("rev", rev).build(); - return get(uri, classType); - } - - /** - * This method finds any document given a URI. - *

- * The URI must be URI-encoded. - * - * @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 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()).path(id, true).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()).path(id, true).query("rev", rev).build(); - return delete(uri); - } - - /** - * Performs a Bulk Documents insert request. - * - * @param objects The {@link List} of objects. - * @param allOrNothing Indicates whether the request has all-or-nothing semantics. - * @return {@code List} Containing the resulted entries. - */ - public List bulk(List objects, boolean allOrNothing) { - assertNotEmpty(objects, "objects"); - HttpResponse response = null; - try { - final String allOrNothingVal = allOrNothing ? "\"all_or_nothing\": true, " : ""; - final URI uri = buildUri(getDBUri()).path("_bulk_docs").build(); - final String json = String.format("{%s%s%s}", allOrNothingVal, "\"docs\": ", getGson().toJson(objects)); - 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 instream 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 instream 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. - * @throws DocumentConflictException - * @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()).path(docId, true).path("/").path(name).query("rev", docRev).build(); - return put(uri, in, contentType); - } - - /** - * Invokes an Update Handler. - *

-   * String query = "field=foo&value=bar";
-   * String output = dbClient.invokeUpdateHandler("designDoc/update1", "docId", query);
-   * 
- * - * @param updateHandlerUri The Update Handler URI, in the format: designDoc/update1 - * @param docId The document id to update. - * @param query The query string parameters, e.g, field=field1&value=value1 - * @return The output of the request. - */ - public String invokeUpdateHandler(String updateHandlerUri, String docId, String query) { - 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(query).build(); - final HttpResponse response = executeRequest(new HttpPut(uri)); - return streamToString(getStream(response)); - } - - /** - * Invokes an Update Handler. - *

- * Use this method in particular when the docId contain special characters such as slashes (/). - *

-   * Params params = new Params()
-   *	.addParam("field", "foo")
-   *	.addParam("value", "bar");
-   * String output = dbClient.invokeUpdateHandler("designDoc/update1", "docId", params);
-   * 
- * - * @param updateHandlerUri The Update Handler URI, in the format: designDoc/update1 - * @param docId The document id to update. - * @param query 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. - */ - 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).path(id, true).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 POST request. - * - * @return {@link HttpResponse} - */ - InputStream post(HttpPost post, String json) { - setEntity(post, json); - HttpResponse resp = executeRequest(post); - return getStream(resp); - } - - /** - * Performs a HTTP GET request. - * - * @return An object of type T - */ - T post(URI uri, String json, Class classType) { - InputStream in = null; - try { - in = getStream(post(uri, json)); - return getGson().fromJson(new InputStreamReader(in, "UTF-8"), classType); - } catch (UnsupportedEncodingException e) { - throw new CouchDbException(e); - } finally { - close(in); - } - } - - /** - * 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; + 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. + * + * @see CouchDbContext + */ + public CouchDbContext context() { + return context; + } + + /** + * Provides access to CouchDB Design Documents. + * + * @see CouchDbDesign + */ + public CouchDbDesign design() { + return design; + } + + /** + * Provides access to CouchDB View APIs. + * + * @see View + */ + public View view(String viewId) { + return new View(this, viewId); + } + + /** + * Provides access to CouchDB replication APIs. + * + * @see Replication + */ + public Replication replication() { + return new Replication(this); + } + + /** + * Provides access to the replicator database. + * + * @see Replicator + */ + public Replicator replicator() { + return new Replicator(this); + } + + /** + * Provides access to Change Notifications API. + * + * @see 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()).path(id, true).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()).path(id, true).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()).path(id, true).query("rev", rev).build(); + return get(uri, classType); + } + + /** + * This method finds any document given a URI. + *

+ * The URI must be URI-encoded. + * + * @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 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()).path(id, true).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()).path(id, true).query("rev", rev).build(); + return delete(uri); + } + + /** + * Performs a Bulk Documents insert request. + * + * @param objects The {@link List} of objects. + * @param allOrNothing Indicates whether the request has all-or-nothing semantics. + * @return {@code List} Containing the resulted entries. + */ + public List bulk(List objects, boolean allOrNothing) { + assertNotEmpty(objects, "objects"); + HttpResponse response = null; + try { + final String allOrNothingVal = allOrNothing ? "\"all_or_nothing\": true, " : ""; + final URI uri = buildUri(getDBUri()).path("_bulk_docs").build(); + final String json = String.format("{%s%s%s}", allOrNothingVal, "\"docs\": ", getGson().toJson(objects)); + 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 instream 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 instream 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. + * @throws DocumentConflictException + * @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()).path(docId, true).path("/").path(name).query("rev", docRev).build(); + return put(uri, in, contentType); + } + + /** + * Invokes an Update Handler. + *

+	 * String query = "field=foo&value=bar";
+	 * String output = dbClient.invokeUpdateHandler("designDoc/update1", "docId", query);
+	 * 
+ * + * @param updateHandlerUri The Update Handler URI, in the format: designDoc/update1 + * @param docId The document id to update. + * @param query The query string parameters, e.g, field=field1&value=value1 + * @return The output of the request. + */ + public String invokeUpdateHandler(String updateHandlerUri, String docId, String query) { + 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(query).build(); + final HttpResponse response = executeRequest(new HttpPut(uri)); + return streamToString(getStream(response)); + } + + /** + * Invokes an Update Handler. + *

+ * Use this method in particular when the docId contain special characters such as slashes (/). + *

+	 * Params params = new Params()
+	 *	.addParam("field", "foo")
+	 *	.addParam("value", "bar");
+	 * String output = dbClient.invokeUpdateHandler("designDoc/update1", "docId", params);
+	 * 
+ * + * @param updateHandlerUri The Update Handler URI, in the format: designDoc/update1 + * @param docId The document id to update. + * @param query 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. + */ + 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).path(id, true).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 POST request. + * + * @return {@link HttpResponse} + */ + InputStream post(HttpPost post, String json) { + setEntity(post, json); + HttpResponse resp = executeRequest(post); + return getStream(resp); + } + + /** + * Performs a HTTP GET request. + * + * @return An object of type T + */ + T post(URI uri, String json, Class classType) { + InputStream in = null; + try { + in = getStream(post(uri, json)); + return getGson().fromJson(new InputStreamReader(in, "UTF-8"), classType); + } catch (UnsupportedEncodingException e) { + throw new CouchDbException(e); + } finally { + close(in); + } + } + + /** + * 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(); + } } diff --git a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java index 7c020cd..7a6ce0d 100644 --- a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java +++ b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java @@ -36,120 +36,120 @@ public class ChangeNotificationsTest { - private static CouchDbClient dbClient; + private static CouchDbClient dbClient; - @BeforeClass - public static void setUpClass() { - dbClient = new CouchDbClient(); - } + @BeforeClass + public static void setUpClass() { + dbClient = new CouchDbClient(); + } - @AfterClass - public static void tearDownClass() { - dbClient.shutdown(); - } - - @Test - public void changes_normalFeed() { - dbClient.save(new Foo()); - - ChangesResult changes = dbClient.changes() - .includeDocs(true) - .limit(1) - .getChanges(); - - List rows = changes.getResults(); - - for (Row row : rows) { - List revs = row.getChanges(); - String docId = row.getId(); - JsonObject doc = row.getDoc(); - - assertNotNull(revs); - assertNotNull(docId); - assertNotNull(doc); - } - - assertThat(rows.size(), is(1)); - } - - @Test - public void changes_normalFeed_selector() { - dbClient.save(new Foo()); - - ChangesResult changes = dbClient.changes() - .includeDocs(true) - .limit(1) - .selector("{\"selector\":{\"_id\": {\"$gt\": null}}}") - .getChanges(); - - List rows = changes.getResults(); - - for (Row row : rows) { - List revs = row.getChanges(); - String docId = row.getId(); - JsonObject doc = row.getDoc(); - - assertNotNull(revs); - assertNotNull(docId); - assertNotNull(doc); - } - - assertThat(rows.size(), is(1)); - } - - @Test - public void changes_continuousFeed() { - dbClient.save(new Foo()); - - CouchDbInfo dbInfo = dbClient.context().info(); - String since = dbInfo.getUpdateSeq(); - - Changes changes = dbClient.changes() - .includeDocs(true) - .since(since) - .heartBeat(1000) - .continuousChanges(); - - Response response = dbClient.save(new Foo()); - - while (changes.hasNext()) { - ChangesResult.Row feed = changes.next(); - final JsonObject feedObject = feed.getDoc(); - final String docId = feed.getId(); - - assertEquals(response.getId(), docId); - assertNotNull(feedObject); - - changes.stop(); - } - } - - @Test - public void changes_continuousFeed_selector() { - dbClient.save(new Foo()); - - CouchDbInfo dbInfo = dbClient.context().info(); - String since = dbInfo.getUpdateSeq(); - - Changes changes = dbClient.changes() - .includeDocs(true) - .since(since) - .heartBeat(1000) - .selector("{\"selector\":{\"_id\": {\"$gt\": null}}}") - .continuousChanges(); + @AfterClass + public static void tearDownClass() { + dbClient.shutdown(); + } + + @Test + public void changes_normalFeed() { + dbClient.save(new Foo()); + + ChangesResult changes = dbClient.changes() + .includeDocs(true) + .limit(1) + .getChanges(); + + List rows = changes.getResults(); + + for (Row row : rows) { + List revs = row.getChanges(); + String docId = row.getId(); + JsonObject doc = row.getDoc(); + + assertNotNull(revs); + assertNotNull(docId); + assertNotNull(doc); + } + + assertThat(rows.size(), is(1)); + } + + @Test + public void changes_normalFeed_selector() { + dbClient.save(new Foo()); + + ChangesResult changes = dbClient.changes() + .includeDocs(true) + .limit(1) + .selector("{\"selector\":{\"_id\": {\"$gt\": null}}}") + .getChanges(); + + List rows = changes.getResults(); + + for (Row row : rows) { + List revs = row.getChanges(); + String docId = row.getId(); + JsonObject doc = row.getDoc(); + + assertNotNull(revs); + assertNotNull(docId); + assertNotNull(doc); + } + + assertThat(rows.size(), is(1)); + } + + @Test + public void changes_continuousFeed() { + dbClient.save(new Foo()); + + CouchDbInfo dbInfo = dbClient.context().info(); + String since = dbInfo.getUpdateSeq(); + + Changes changes = dbClient.changes() + .includeDocs(true) + .since(since) + .heartBeat(1000) + .continuousChanges(); + + Response response = dbClient.save(new Foo()); + + while (changes.hasNext()) { + ChangesResult.Row feed = changes.next(); + final JsonObject feedObject = feed.getDoc(); + final String docId = feed.getId(); + + assertEquals(response.getId(), docId); + assertNotNull(feedObject); + + changes.stop(); + } + } + + @Test + public void changes_continuousFeed_selector() { + dbClient.save(new Foo()); + + CouchDbInfo dbInfo = dbClient.context().info(); + String since = dbInfo.getUpdateSeq(); + + Changes changes = dbClient.changes() + .includeDocs(true) + .since(since) + .heartBeat(1000) + .selector("{\"selector\":{\"_id\": {\"$gt\": null}}}") + .continuousChanges(); - Response response = dbClient.save(new Foo()); - - while (changes.hasNext()) { - ChangesResult.Row feed = changes.next(); - final JsonObject feedObject = feed.getDoc(); - final String docId = feed.getId(); - System.out.println("next()=" + docId); + Response response = dbClient.save(new Foo()); + + while (changes.hasNext()) { + ChangesResult.Row feed = changes.next(); + final JsonObject feedObject = feed.getDoc(); + final String docId = feed.getId(); + System.out.println("next()=" + docId); - assertEquals(response.getId(), docId); - assertNotNull(feedObject); - - changes.stop(); - } - } + assertEquals(response.getId(), docId); + assertNotNull(feedObject); + + changes.stop(); + } + } }