diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..200c26a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,54 @@ +sudo: required + +language: java +addons: + sonarcloud: + organization: "indaba" + apt: + packages: + - docker-ce + +services: + - docker + +jobs: + include: + - stage: Build and Test on CouchDB 3.0.x + install: + - docker pull couchdb:3.0.0 + - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:3.0.0 + - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb.properties + - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb-2.properties + - sleep 20 + - "curl -X PUT http://127.0.0.1:5984/_users -u couchdb:couchdb" + - "curl -X PUT http://127.0.0.1:5984/_replicator -u couchdb:couchdb" + - "curl -X PUT http://127.0.0.1:5984/_global_changes -u couchdb:couchdb" + script: + - mvn org.jacoco:jacoco-maven-plugin:prepare-agent test sonar:sonar + - stage: Build and Test on CouchDB 2.3.x + install: + - docker pull couchdb:2.3.1 + - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.3.1 + - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb.properties + - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb-2.properties + - sleep 20 + - "curl -X PUT http://127.0.0.1:5984/_users -u couchdb:couchdb" + - "curl -X PUT http://127.0.0.1:5984/_replicator -u couchdb:couchdb" + - "curl -X PUT http://127.0.0.1:5984/_global_changes -u couchdb:couchdb" + - stage: Build and Test on CouchDB 2.2.x + install: + - docker pull couchdb:2.2.0 + - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.2.0 + - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb.properties + - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb-2.properties + - sleep 20 + - "curl -X PUT http://127.0.0.1:5984/_users -u couchdb:couchdb" + - "curl -X PUT http://127.0.0.1:5984/_replicator -u couchdb:couchdb" + - "curl -X PUT http://127.0.0.1:5984/_global_changes -u couchdb:couchdb" + - stage: Build and Test on CouchDB 1.7.x + install: + - docker pull couchdb:1.7.2 + - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:1.7.2 + - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb.properties + - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb-2.properties + - sleep 20 diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..31137df --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,22 @@ +# 0.2.6 (18/02/2019) +- [NEW] Added support for /db/_purge endpoint implemented in CouchDB 2.3.0 + +# 0.2.5 (23/12/2018) +- [FIX] Support for string purge_seqs in CouchDB 2.3 + +# 0.2.4 (08/10/2018) +- [NEW] Added API for checking design doc existence + +# 0.2.3 (18/09/2018) +- [NEW] Added API for specifying docIds filter to _changes operation + +# 0.2.2 (21/03/2018) +- [NEW] Added explicit API for local document management. +- [NEW] Added seq_interval parameter in Changes API +- [NEW] Added _db_updates endpoint support +- [IMPROVED] Make more robust stop process in Changes hasNext + +# 0.2.1 (21/02/2018) +- [NEW] Added API for specifying a mango selector _changes operation +- [IMPROVED] Test are cleaned up and executed in CouchDB 1.x an CouchDb 2.x +- [SUPPORT] Added support for travis build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ebd91f6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing to this library + +LightCouch is written in Java and uses [maven](https://maven.apache.org/) as its build tool. + +## Requirements + +The main requirements are: + +* Java 1.6 +* CouchDB + +## Coding guidelines + +Adopting the [Google Java Style](https://google-styleguide.googlecode.com/svn/trunk/javaguide.html) +with the following changes: + +``` +4.2 + Our block indent is +4 characters + +4.4 + Our line length is 100 characters. + +4.5.2 + Indent continuation of +4 characters fine, but I think + IDEA defaults to 8, which is okay too. +``` diff --git a/LICENSE b/LICENSE index be289da..261eeb9 100644 --- a/LICENSE +++ b/LICENSE @@ -184,9 +184,9 @@ comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier - identification within third-party archives. + identification within third-party archives. - Copyright 2011 lightcouch.org + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 0bc8aa8..e158691 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,19 @@ CouchDB Java API ================ +[![Travis CI](https://api.travis-ci.org/IndabaConsultores/LightCouch.svg?branch=master)](https://travis-ci.org/IndabaConsultores/LightCouch) +[![Sonarcloud](https://sonarcloud.io/api/project_badges/measure?project=es.indaba:lightcouch&metric=alert_status)](https://sonarcloud.io/dashboard?id=es.indaba:lightcouch) + + A Java _client_ for [CouchDB](http://couchdb.apache.org/) database. -* Homepage: +This is an active fork of LightCouch CouchDB Java API. Our intention is to mantain an active development of the library to cover the CouchDB REST API. + +The releases of this fork are published via Jitpack +```xml + + com.github.IndabaConsultores + lightCouch + 0.2.6 + +``` diff --git a/pom.xml b/pom.xml index b688454..df3316e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,76 +1,89 @@ - - 4.0.0 - - org.lightcouch - lightcouch - 0.2.1-SNAPSHOT - jar - LightCouch - CouchDB Java API - 2011 - http://www.lightcouch.org - - org.sonatype.oss - oss-parent - 7 - - - - The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - scm:git:git://github.com/lightcouch/LightCouch.git - scm:git:git@github.com:lightcouch/LightCouch.git - https://lightcouch@github.com/lightcouch/LightCouch.git - - - - ahmedyha - Ahmed Yehia - ahmed.yehia.m@gmail.com - - - - UTF-8 - 4.5.3 - 2.8.2 - 4.8.2 - - - - - org.apache.httpcomponents - httpclient - ${httpclient.version} - - - - com.google.code.gson - gson - ${gson.version} - - - - junit - junit - ${junit.version} - test - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - 1.5 - 1.5 - - - - - + + 4.0.0 + + es.indaba + lightcouch + 0.2.6 + jar + LightCouch + CouchDB Java API + + org.sonatype.oss + oss-parent + 7 + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + scm:git:https://github.com/IndabaConsultores/LightCouch.git + https://github.com/IndabaConsultores/LightCouch + + + + + Juan José Rodríguez + jjrodriguez@indaba.es + Indaba Consultores S.L. + http://www.indaba.es + + + Joseba Urkiri + jurkiri@indaba.es + Indaba Consultores S.L. + http://www.indaba.es + + + + + UTF-8 + 4.5.3 + 2.8.2 + 4.8.2 + + + + + org.apache.httpcomponents + httpclient + ${httpclient.version} + + + + com.google.code.gson + gson + ${gson.version} + + + + junit + junit + ${junit.version} + test + + + com.github.zafarkhaja + java-semver + 0.9.0 + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.6 + 1.6 + + + + + diff --git a/src/main/java/org/lightcouch/Changes.java b/src/main/java/org/lightcouch/Changes.java index a544d46..38faeb6 100644 --- a/src/main/java/org/lightcouch/Changes.java +++ b/src/main/java/org/lightcouch/Changes.java @@ -1,17 +1,14 @@ /* - * Copyright (C) 2011 lightcouch.org + * Copyright (C) 2011 lightcouch.org Copyright (C) 2018 indaba.es * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ package org.lightcouch; @@ -20,16 +17,23 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.net.URI; +import java.util.List; import org.apache.commons.codec.Charsets; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; import org.lightcouch.ChangesResult.Row; import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; /** - *

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 
  * String since = dbClient.context().info().getUpdateSeq(); // latest update seq
@@ -56,157 +60,226 @@
  *  JsonObject doc = feed.getDoc();
  *	// changes.stop(); // stop continuous feed
  * }
+ * 
+ * Selector filter:
+ * ChangesResult changeResult = dbClient.changes()
+ *	.since(since) 
+ *	.limit(10)
+ *	.selector("{\"selector":{\"_deleted\":true}}")
+ *	.getChanges();
+ *
  * 
+ * * @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. - * @return {@link Changes} - */ - public Changes continuousChanges() { - final URI uri = uriBuilder.query("feed", "continuous").build(); - httpGet = new HttpGet(uri); - final InputStream in = dbc.get(httpGet); - final InputStreamReader is = new InputStreamReader(in, Charsets.UTF_8); - setReader(new BufferedReader(is)); - return this; - } - - /** - * Checks whether a feed is available in the continuous stream, blocking - * until a feed is received. - * @return true If a feed is available - */ - public boolean hasNext() { - return readNextRow(); - } - - /** - * @return The next feed in the stream. - */ - public Row next() { - return getNextRow(); - } - - /** - * Stops a running continuous feed. - */ - public void stop() { - stop = true; - } - - /** - * Requests Change notifications of feed type normal. - * @return {@link ChangesResult} - */ - public ChangesResult getChanges() { - final URI uri = uriBuilder.query("feed", "normal").build(); - return dbc.get(uri, ChangesResult.class); - } - - // Query Params - - public Changes since(String since) { - uriBuilder.query("since", since); - return this; - } - - public Changes limit(int limit) { - uriBuilder.query("limit", limit); - return this; - } - - public Changes heartBeat(long heartBeat) { - uriBuilder.query("heartbeat", heartBeat); - return this; - } - - public Changes timeout(long timeout) { - uriBuilder.query("timeout", timeout); - return this; - } - - public Changes filter(String filter) { - uriBuilder.query("filter", filter); - return this; - } - - public Changes includeDocs(boolean includeDocs) { - uriBuilder.query("include_docs", includeDocs); - return this; - } - - public Changes style(String style) { - uriBuilder.query("style", style); - return this; - } - - // Helper - - /** - * Reads and sets the next feed in the stream. - */ - private boolean readNextRow() { - boolean hasNext = false; - try { - if(!stop) { - String row = ""; - do { - row = getReader().readLine(); - } while(row.length() == 0); - - if(!row.startsWith("{\"last_seq\":")) { - setNextRow(gson.fromJson(row, Row.class)); - hasNext = true; - } - } - } catch (Exception e) { - terminate(); - throw new CouchDbException("Error reading continuous stream.", e); - } - if(!hasNext) - terminate(); - return hasNext; - } - - private BufferedReader getReader() { - return reader; - } - - private void setReader(BufferedReader reader) { - this.reader = reader; - } - - private Row getNextRow() { - return nextRow; - } - - private void setNextRow(Row nextRow) { - this.nextRow = nextRow; - } - - private void terminate() { - httpGet.abort(); - CouchDbUtil.close(getReader()); - } + + private BufferedReader reader; + private HttpUriRequest httpRequest; + private Row nextRow; + private boolean stop; + + private CouchDbClientBase dbc; + private Gson gson; + private URIBuilder uriBuilder; + + private String filter; + private String selector; + private List docIds; + + Changes(CouchDbClientBase dbc) { + this.dbc = dbc; + this.gson = dbc.getGson(); + this.uriBuilder = URIBuilder.buildUri(dbc.getDBUri()).path("_changes"); + } + + /** + * Requests Change notifications of feed type continuous. + *

+ * Feed notifications are accessed in an iterator style. + * + * @return {@link Changes} + */ + public Changes continuousChanges() { + final URI uri = uriBuilder.query("feed", "continuous").build(); + if (selector == null) { + final HttpGet get = new HttpGet(uri); + httpRequest = get; + final InputStream in = dbc.get(get); + final InputStreamReader is = new InputStreamReader(in, Charsets.UTF_8); + setReader(new BufferedReader(is)); + } else { + final HttpPost post = new HttpPost(uri); + httpRequest = post; + final InputStream in = dbc.post(post, selector); + final InputStreamReader is = new InputStreamReader(in, Charsets.UTF_8); + setReader(new BufferedReader(is)); + } + return this; + } + + /** + * Checks whether a feed is available in the continuous stream, blocking until a feed is received. + * + * @return true If a feed is available + */ + public boolean hasNext() { + return readNextRow(); + } + + /** + * @return The next feed in the stream. + */ + public Row next() { + return getNextRow(); + } + + /** + * Stops a running continuous feed. + */ + public void stop() { + stop = true; + } + + /** + * Requests Change notifications of feed type normal. + * + * @return {@link ChangesResult} + */ + public ChangesResult getChanges() { + final URI uri = uriBuilder.query("feed", "normal").build(); + if (selector == null && docIds == null) { + return dbc.get(uri, ChangesResult.class); + } else { + String json = selector; + if (docIds != null) { + JsonObject docIdsJson = new JsonObject(); + JsonArray jArray = new JsonArray(); + for (String id : docIds) { + jArray.add(id); + } + docIdsJson.add("doc_ids", jArray); + json = docIdsJson.toString(); + } + + return dbc.post(uri, json, ChangesResult.class); + } + } + + // Query Params + + public Changes since(String since) { + uriBuilder.query("since", since); + return this; + } + + public Changes limit(int limit) { + uriBuilder.query("limit", limit); + return this; + } + + public Changes heartBeat(long heartBeat) { + uriBuilder.query("heartbeat", heartBeat); + return this; + } + + public Changes timeout(long timeout) { + uriBuilder.query("timeout", timeout); + return this; + } + + public Changes filter(String filter) { + if (docIds!=null || selector != null) { + throw new IllegalArgumentException("Filter is not compatible with selector or docIds filters"); + } + uriBuilder.query("filter", filter); + this.filter=filter; + return this; + } + + public Changes selector(String json) { + if (docIds!=null || filter != null) { + throw new IllegalArgumentException("Selector is not compatible with filters or docIds filters"); + } + uriBuilder.query("filter", "_selector"); + this.selector = json; + return this; + } + + public Changes docIds(List docIds) { + if (selector!=null || filter != null) { + throw new IllegalArgumentException("DocIds filter is not compatible with filter or selector"); + } + uriBuilder.query("filter", "_doc_ids"); + this.docIds = docIds; + return this; + } + + public Changes includeDocs(boolean includeDocs) { + uriBuilder.query("include_docs", includeDocs); + return this; + } + + public Changes style(String style) { + uriBuilder.query("style", style); + return this; + } + + public Changes seqInterval(long batchSize) { + uriBuilder.query("seq_interval", batchSize); + return this; + } + + // Helper + + /** + * Reads and sets the next feed in the stream. + */ + private boolean readNextRow() { + boolean hasNext = false; + try { + if (!stop) { + String row = ""; + do { + row = getReader().readLine(); + } while (row.length() == 0 && !stop); + + if (!stop) { + if (!row.startsWith("{\"last_seq\":")) { + setNextRow(gson.fromJson(row, Row.class)); + hasNext = true; + } + } + } + } catch (Exception e) { + terminate(); + throw new CouchDbException("Error reading continuous stream.", e); + } + if (!hasNext) + terminate(); + return hasNext; + } + + private BufferedReader getReader() { + return reader; + } + + private void setReader(BufferedReader reader) { + this.reader = reader; + } + + private Row getNextRow() { + return nextRow; + } + + private void setNextRow(Row nextRow) { + this.nextRow = nextRow; + } + + private void terminate() { + httpRequest.abort(); + CouchDbUtil.close(getReader()); + } } diff --git a/src/main/java/org/lightcouch/CouchDbClient.java b/src/main/java/org/lightcouch/CouchDbClient.java index 5694e92..14378b8 100644 --- a/src/main/java/org/lightcouch/CouchDbClient.java +++ b/src/main/java/org/lightcouch/CouchDbClient.java @@ -229,7 +229,6 @@ public void shutdown() { HttpClientUtils.closeQuietly(this.httpClient); } - @Override public void close() throws IOException { shutdown(); } diff --git a/src/main/java/org/lightcouch/CouchDbClientBase.java b/src/main/java/org/lightcouch/CouchDbClientBase.java index 956eb85..026e4c9 100644 --- a/src/main/java/org/lightcouch/CouchDbClientBase.java +++ b/src/main/java/org/lightcouch/CouchDbClientBase.java @@ -1,17 +1,14 @@ /* - * Copyright (C) 2011 lightcouch.org + * Copyright (C) 2019 indaba.es Copyright (C) 2011 lightcouch.org * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ package org.lightcouch; @@ -34,10 +31,12 @@ import java.net.URI; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.apache.commons.codec.Charsets; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.http.Header; import org.apache.http.HttpHost; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; @@ -51,6 +50,7 @@ import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.InputStreamEntity; import org.apache.http.entity.StringEntity; +import org.apache.http.message.BasicHeader; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; @@ -69,647 +69,799 @@ /** * Contains a client Public API implementation. + * * @see CouchDbClient * @see CouchDbClientAndroid * @author Ahmed Yehia */ public abstract class CouchDbClientBase { - static final Log log = LogFactory.getLog(CouchDbClient.class); - - private URI baseURI; - private URI dbURI; - private Gson gson; - private CouchDbContext context; - private CouchDbDesign design; - final HttpClient httpClient; - final HttpHost host; - - CouchDbClientBase() { - this(new CouchDbConfig()); - } - - CouchDbClientBase(CouchDbConfig config) { - final CouchDbProperties props = config.getProperties(); - this.httpClient = createHttpClient(props); - this.gson = initGson(new GsonBuilder()); - this.host = new HttpHost(props.getHost(), props.getPort(), props.getProtocol()); - - final String path = props.getPath() != null ? props.getPath() : ""; - this.baseURI = buildUri().scheme(props.getProtocol()).host(props.getHost()).port(props.getPort()).path("/").path(path).build(); - this.dbURI = buildUri(baseURI).path(props.getDbName()).path("/").build(); - - this.context = new CouchDbContext(this, props); - this.design = new CouchDbDesign(this); - } - - // Client(s) provided implementation - - /** - * @return {@link HttpClient} instance for HTTP request execution. - */ - abstract HttpClient createHttpClient(CouchDbProperties properties); - - /** - * @return {@link HttpContext} instance for HTTP request execution. - */ - abstract HttpContext createContext(); - - /** - * Shuts down the connection manager used by this client instance. - */ - abstract void shutdown(); - - // Public API - - /** - * Provides access to DB server APIs. - * @return {@link CouchDbContext} - */ - public CouchDbContext context() { - return context; - } - - /** - * Provides access to CouchDB Design Documents. - * @return {@link CouchDbDesign} - */ - public CouchDbDesign design() { - return design; - } - - /** - * Provides access to CouchDB View APIs. - * @param viewId The view id. - * @return {@link View} - */ - public View view(String viewId) { - return new View(this, viewId); - } - - /** - * Provides access to CouchDB replication APIs. - * @return {@link Replication} - */ - public Replication replication() { - return new Replication(this); - } - - /** - * Provides access to the replicator database. - * @return {@link Replicator} - */ - public Replicator replicator() { - return new Replicator(this); - } - - /** - * Provides access to Change Notifications API. - * @return {@link Changes} - */ - public Changes changes() { - return new Changes(this); - } - - /** - * Finds an Object of the specified type. - * @param Object type. - * @param classType The class of type T. - * @param id The document id. - * @return An object of type T. - * @throws NoDocumentException If the document is not found in the database. - */ - public T find(Class classType, String id) { - assertNotEmpty(classType, "Class"); - assertNotEmpty(id, "id"); - final URI uri = buildUri(getDBUri()).pathEncoded(id).build(); - return get(uri, classType); - } - - /** - * Finds an Object of the specified type. - * @param Object type. - * @param classType The class of type T. - * @param id The document id. - * @param params Extra parameters to append. - * @return An object of type T. - * @throws NoDocumentException If the document is not found in the database. - */ - public T find(Class classType, String id, Params params) { - assertNotEmpty(classType, "Class"); - assertNotEmpty(id, "id"); - final URI uri = buildUri(getDBUri()).pathEncoded(id).query(params).build(); - return get(uri, classType); - } - - /** - * Finds an Object of the specified type. - * @param Object type. - * @param classType The class of type T. - * @param id The document _id field. - * @param rev The document _rev field. - * @return An object of type T. - * @throws NoDocumentException If the document is not found in the database. - */ - public T find(Class classType, String id, String rev) { - assertNotEmpty(classType, "Class"); - assertNotEmpty(id, "id"); - assertNotEmpty(id, "rev"); - final URI uri = buildUri(getDBUri()).pathEncoded(id).query("rev", rev).build(); - return get(uri, classType); - } - - /** - * This method finds any document given a URI. - *

The URI must be URI-encoded. - * @param The class type. - * @param classType The class of type T. - * @param uri The URI as string. - * @return An object of type T. - */ - public T findAny(Class classType, String uri) { - assertNotEmpty(classType, "Class"); - assertNotEmpty(uri, "uri"); - return get(URI.create(uri), classType); - } - - /** - * Finds a document and return the result as {@link InputStream}. - *

Note: The stream must be closed after use to release the connection. - * @param id The document _id field. - * @return The result as {@link InputStream} - * @throws NoDocumentException If the document is not found in the database. - * @see #find(String, String) - */ - public InputStream find(String id) { - assertNotEmpty(id, "id"); - return get(buildUri(getDBUri()).path(id).build()); - } - - /** - * Finds a document given id and revision and returns the result as {@link InputStream}. - *

Note: The stream must be closed after use to release the connection. - * @param id The document _id field. - * @param rev The document _rev field. - * @return The result as {@link InputStream} - * @throws NoDocumentException If the document is not found in the database. - */ - public InputStream find(String id, String rev) { - assertNotEmpty(id, "id"); - assertNotEmpty(rev, "rev"); - final URI uri = buildUri(getDBUri()).path(id).query("rev", rev).build(); - return get(uri); - } - - /** - * Find documents using a declarative JSON querying syntax. - * @param The class type. - * @param jsonQuery The JSON query string. - * @param classOfT The class of type T. - * @return The result of the query as a {@code List } - * @throws CouchDbException If the query failed to execute or the request is invalid. - */ - public List findDocs(String jsonQuery, Class classOfT) { - assertNotEmpty(jsonQuery, "jsonQuery"); - HttpResponse response = null; - try { - response = post(buildUri(getDBUri()).path("_find").build(), jsonQuery); - Reader reader = new InputStreamReader(getStream(response), Charsets.UTF_8); - JsonArray jsonArray = new JsonParser().parse(reader) - .getAsJsonObject().getAsJsonArray("docs"); - List list = new ArrayList(); - for (JsonElement jsonElem : jsonArray) { - JsonElement elem = jsonElem.getAsJsonObject(); - T t = this.gson.fromJson(elem, classOfT); - list.add(t); - } - return list; - } finally { - close(response); - } - } - - /** - * Checks if a document exist in the database. - * @param id The document _id field. - * @return true If the document is found, false otherwise. - */ - public boolean contains(String id) { - assertNotEmpty(id, "id"); - HttpResponse response = null; - try { - response = head(buildUri(getDBUri()).pathEncoded(id).build()); - } catch (NoDocumentException e) { - return false; - } finally { - close(response); - } - return true; - } - - /** - * Saves an object in the database, using HTTP PUT request. - *

If the object doesn't have an _id value, the code will assign a UUID as the document id. - * @param object The object to save - * @throws DocumentConflictException If a conflict is detected during the save. - * @return {@link Response} - */ - public Response save(Object object) { - return put(getDBUri(), object, true); - } - - /** - * Saves an object in the database using HTTP POST request. - *

The database will be responsible for generating the document id. - * @param object The object to save - * @return {@link Response} - */ - public Response post(Object object) { - assertNotEmpty(object, "object"); - HttpResponse response = null; - try { - URI uri = buildUri(getDBUri()).build(); - response = post(uri, getGson().toJson(object)); - return getResponse(response); - } finally { - close(response); - } - } - - /** - * Saves a document with batch=ok query param. - * @param object The object to save. - */ - public void batch(Object object) { - assertNotEmpty(object, "object"); - HttpResponse response = null; - try { - URI uri = buildUri(getDBUri()).query("batch", "ok").build(); - response = post(uri, getGson().toJson(object)); - } finally { - close(response); - } - } - - /** - * Updates an object in the database, the object must have the correct _id and _rev values. - * @param object The object to update - * @throws DocumentConflictException If a conflict is detected during the update. - * @return {@link Response} - */ - public Response update(Object object) { - return put(getDBUri(), object, false); - } - - /** - * Removes a document from the database. - *

The object must have the correct _id and _rev values. - * @param object The document to remove as object. - * @throws NoDocumentException If the document is not found in the database. - * @return {@link Response} - */ - public Response remove(Object object) { - assertNotEmpty(object, "object"); - JsonObject jsonObject = getGson().toJsonTree(object).getAsJsonObject(); - final String id = getAsString(jsonObject, "_id"); - final String rev = getAsString(jsonObject, "_rev"); - return remove(id, rev); - } - - /** - * Removes a document from the database given both a document _id and _rev values. - * @param id The document _id field. - * @param rev The document _rev field. - * @throws NoDocumentException If the document is not found in the database. - * @return {@link Response} - */ - public Response remove(String id, String rev) { - assertNotEmpty(id, "id"); - assertNotEmpty(rev, "rev"); - final URI uri = buildUri(getDBUri()).pathEncoded(id).query("rev", rev).build(); - return delete(uri); - } - - /** - * Performs bulk documents create and update request. - * @param objects The {@link List} of documents objects. - * @param newEdits If false, prevents the database from assigning documents new revision IDs. - * @return {@code List} Containing the resulted entries. - */ - public List bulk(List objects, boolean newEdits) { - assertNotEmpty(objects, "objects"); - HttpResponse response = null; - try { - final String newEditsVal = newEdits ? "\"new_edits\": true, " : "\"new_edits\": false, "; - final String json = String.format("{%s%s%s}", newEditsVal, "\"docs\": ", getGson().toJson(objects)); - final URI uri = buildUri(getDBUri()).path("_bulk_docs").build(); - response = post(uri, json); - return getResponseList(response); - } finally { - close(response); - } - } - - /** - * Saves an attachment to a new document with a generated UUID as the document id. - *

To retrieve an attachment, see {@link #find(String)}. - * @param in The {@link InputStream} holding the binary data. - * @param name The attachment name. - * @param contentType The attachment "Content-Type". - * @return {@link Response} - */ - public Response saveAttachment(InputStream in, String name, String contentType) { - assertNotEmpty(in, "in"); - assertNotEmpty(name, "name"); - assertNotEmpty(contentType, "ContentType"); - final URI uri = buildUri(getDBUri()).path(generateUUID()).path("/").path(name).build(); - return put(uri, in, contentType); - } - - /** - * Saves an attachment to an existing document given both a document id - * and revision, or save to a new document given only the id, and rev as {@code null}. - *

To retrieve an attachment, see {@link #find(String)}. - * @param in The {@link InputStream} holding the binary data. - * @param name The attachment name. - * @param contentType The attachment "Content-Type". - * @param docId The document id to save the attachment under, or {@code null} to save under a new document. - * @param docRev The document revision to save the attachment under, or {@code null} when saving to a new document. - * @return {@link Response} - */ - public Response saveAttachment(InputStream in, String name, String contentType, String docId, String docRev) { - assertNotEmpty(in, "in"); - assertNotEmpty(name, "name"); - assertNotEmpty(contentType, "ContentType"); - assertNotEmpty(docId, "docId"); - final URI uri = buildUri(getDBUri()).pathEncoded(docId).path("/").path(name).query("rev", docRev).build(); - return put(uri, in, contentType); - } - - /** - * Invokes an Update Handler. - *

-	 * 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 params The query parameters as {@link Params}. - * @return The output of the request. - */ - public String invokeUpdateHandler(String updateHandlerUri, String docId, Params params) { - assertNotEmpty(updateHandlerUri, "uri"); - assertNotEmpty(docId, "docId"); - final String[] v = updateHandlerUri.split("/"); - final String path = String.format("_design/%s/_update/%s/", v[0], v[1]); - final URI uri = buildUri(getDBUri()).path(path).path(docId).query(params).build(); - final HttpResponse response = executeRequest(new HttpPut(uri)); - return streamToString(getStream(response)); - } - - /** - * Executes a HTTP request. - *

Note: The response must be closed after use to release the connection. - * @param request The HTTP request to execute. - * @return {@link HttpResponse} - */ - public HttpResponse executeRequest(HttpRequestBase request) { - try { - return httpClient.execute(host, request, createContext()); - } catch (IOException e) { - request.abort(); - throw new CouchDbException("Error executing request. ", e); - } - } - - /** - * Synchronize all design documents with the database. - */ - public void syncDesignDocsWithDb() { - design().synchronizeAllWithDb(); - } - - /** - * Sets a {@link GsonBuilder} to create {@link Gson} instance. - *

Useful for registering custom serializers/deserializers, such as JodaTime classes. - * @param gsonBuilder The {@link GsonBuilder} - */ - public void setGsonBuilder(GsonBuilder gsonBuilder) { - this.gson = initGson(gsonBuilder); - } - - /** - * @return The base URI. - */ - public URI getBaseUri() { - return baseURI; - } - - /** - * @return The database URI. - */ - public URI getDBUri() { - return dbURI; - } - - /** - * @return The Gson instance. - */ - public Gson getGson() { - return gson; - } - - // End - Public API - - /** - * Performs a HTTP GET request. - * @return {@link InputStream} - */ - InputStream get(HttpGet httpGet) { - HttpResponse response = executeRequest(httpGet); - return getStream(response); - } - - /** - * Performs a HTTP GET request. - * @return {@link InputStream} - */ - InputStream get(URI uri) { - HttpGet get = new HttpGet(uri); - get.addHeader("Accept", "application/json"); - return get(get); - } - - /** - * Performs a HTTP GET request. - * @return An object of type T - */ - T get(URI uri, Class classType) { - InputStream in = null; - try { - in = get(uri); - return getGson().fromJson(new InputStreamReader(in, "UTF-8"), classType); - } catch (UnsupportedEncodingException e) { - throw new CouchDbException(e); - } finally { - close(in); - } - } - - /** - * Performs a HTTP HEAD request. - * @return {@link HttpResponse} - */ - HttpResponse head(URI uri) { - return executeRequest(new HttpHead(uri)); - } - - /** - * Performs a HTTP PUT request, saves or updates a document. - * @return {@link Response} - */ - Response put(URI uri, Object object, boolean newEntity) { - assertNotEmpty(object, "object"); - HttpResponse response = null; - try { - final JsonObject json = getGson().toJsonTree(object).getAsJsonObject(); - String id = getAsString(json, "_id"); - String rev = getAsString(json, "_rev"); - if(newEntity) { // save - assertNull(rev, "rev"); - id = (id == null) ? generateUUID() : id; - } else { // update - assertNotEmpty(id, "id"); - assertNotEmpty(rev, "rev"); - } - final HttpPut put = new HttpPut(buildUri(uri).pathEncoded(id).build()); - setEntity(put, json.toString()); - response = executeRequest(put); - return getResponse(response); - } finally { - close(response); - } - } - - /** - * Performs a HTTP PUT request, saves an attachment. - * @return {@link Response} - */ - Response put(URI uri, InputStream instream, String contentType) { - HttpResponse response = null; - try { - final HttpPut httpPut = new HttpPut(uri); - final InputStreamEntity entity = new InputStreamEntity(instream, -1); - entity.setContentType(contentType); - httpPut.setEntity(entity); - response = executeRequest(httpPut); - return getResponse(response); - } finally { - close(response); - } - } - - /** - * Performs a HTTP POST request. - * @return {@link HttpResponse} - */ - HttpResponse post(URI uri, String json) { - HttpPost post = new HttpPost(uri); - setEntity(post, json); - return executeRequest(post); - } - - /** - * Performs a HTTP DELETE request. - * @return {@link Response} - */ - Response delete(URI uri) { - HttpResponse response = null; - try { - HttpDelete delete = new HttpDelete(uri); - response = executeRequest(delete); - return getResponse(response); - } finally { - close(response); - } - } - - // Helpers - - /** - * Validates a HTTP response; on error cases logs status and throws relevant exceptions. - * @param response The HTTP response. - */ - void validate(HttpResponse response) throws IOException { - final int code = response.getStatusLine().getStatusCode(); - if(code == 200 || code == 201 || code == 202) { // success (ok | created | accepted) - return; - } - String reason = response.getStatusLine().getReasonPhrase(); - switch (code) { - case HttpStatus.SC_NOT_FOUND: { - throw new NoDocumentException(reason); - } - case HttpStatus.SC_CONFLICT: { - throw new DocumentConflictException(reason); - } - default: { // other errors: 400 | 401 | 500 etc. - throw new CouchDbException(reason += EntityUtils.toString(response.getEntity())); - } - } - } - - /** - * @param response The {@link HttpResponse} - * @return {@link Response} - */ - private Response getResponse(HttpResponse response) throws CouchDbException { - InputStreamReader reader = new InputStreamReader(getStream(response), Charsets.UTF_8); - return getGson().fromJson(reader, Response.class); - } - - /** - * @param response The {@link HttpResponse} - * @return {@link Response} - */ - private List getResponseList(HttpResponse response) throws CouchDbException { - InputStream instream = getStream(response); - Reader reader = new InputStreamReader(instream, Charsets.UTF_8); - return getGson().fromJson(reader, new TypeToken>(){}.getType()); - } - - /** - * Sets a JSON String as a request entity. - * @param httpRequest The request to set entity. - * @param json The JSON String to set. - */ - private void setEntity(HttpEntityEnclosingRequestBase httpRequest, String json) { - StringEntity entity = new StringEntity(json, "UTF-8"); - entity.setContentType("application/json"); - httpRequest.setEntity(entity); - } - - /** - * Builds {@link Gson} and registers any required serializer/deserializer. - * @return {@link Gson} instance - */ - private Gson initGson(GsonBuilder gsonBuilder) { - gsonBuilder.registerTypeAdapter(JsonObject.class, new JsonDeserializer() { - public JsonObject deserialize(JsonElement json, - Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - return json.getAsJsonObject(); - } - }); - gsonBuilder.registerTypeAdapter(JsonObject.class, new JsonSerializer() { - public JsonElement serialize(JsonObject src, Type typeOfSrc, - JsonSerializationContext context) { - return src.getAsJsonObject(); - } - - }); - return gsonBuilder.create(); - } + static final Log log = LogFactory.getLog(CouchDbClient.class); + + private URI baseURI; + private URI dbURI; + private Gson gson; + private CouchDbContext context; + private CouchDbDesign design; + private Local local; + final HttpClient httpClient; + final HttpHost host; + + CouchDbClientBase() { + this(new CouchDbConfig()); + } + + CouchDbClientBase(CouchDbConfig config) { + final CouchDbProperties props = config.getProperties(); + this.httpClient = createHttpClient(props); + this.gson = initGson(new GsonBuilder()); + this.host = new HttpHost(props.getHost(), props.getPort(), props.getProtocol()); + + final String path = props.getPath() != null ? props.getPath() : ""; + this.baseURI = buildUri().scheme(props.getProtocol()).host(props.getHost()).port(props.getPort()).path("/") + .path(path).build(); + this.dbURI = buildUri(baseURI).path(props.getDbName()).path("/").build(); + + this.context = new CouchDbContext(this, props); + this.design = new CouchDbDesign(this); + this.local = new Local(this); + } + + // Client(s) provided implementation + + /** + * @return {@link HttpClient} instance for HTTP request execution. + */ + abstract HttpClient createHttpClient(CouchDbProperties properties); + + /** + * @return {@link HttpContext} instance for HTTP request execution. + */ + abstract HttpContext createContext(); + + /** + * Shuts down the connection manager used by this client instance. + */ + abstract void shutdown(); + + // Public API + + /** + * Provides access to DB server APIs. + * + * @return {@link CouchDbContext} + */ + public CouchDbContext context() { + return context; + } + + /** + * Provides access to CouchDB Design Documents. + * + * @return {@link CouchDbDesign} + */ + public CouchDbDesign design() { + return design; + } + + /** + * Provides access to CouchDB View APIs. + * + * @param viewId The view id. + * @return {@link View} + */ + public View view(String viewId) { + return new View(this, viewId); + } + + public Local local() { + return local; + } + + /** + * Provides access to CouchDB replication APIs. + * + * @return {@link Replication} + */ + public Replication replication() { + return new Replication(this); + } + + /** + * Provides access to the replicator database. + * + * @return {@link Replicator} + */ + public Replicator replicator() { + return new Replicator(this); + } + + /** + * Provides access to Change Notifications API. + * + * @return {@link Changes} + */ + public Changes changes() { + return new Changes(this); + } + + /** + * Purge operation over database + * + * @param toPurge - Map of Ids and the list of revs to purge + * @return Ids and revs purged + */ + public PurgeResponse purge(Map> toPurge) { + assertNotEmpty(toPurge, "to purge map"); + HttpResponse response = null; + Reader reader = null; + try { + String jsonToPurge = getGson().toJson(toPurge); + response = post(buildUri(getDBUri()).path("_purge").build(), jsonToPurge); + reader = new InputStreamReader(getStream(response), Charsets.UTF_8); + return getGson().fromJson(reader, PurgeResponse.class); + } finally { + close(reader); + close(response); + } + } + + /** + * Finds an Object of the specified type. + * + * @param Object type. + * @param classType The class of type T. + * @param id The document id. + * @return An object of type T. + * @throws NoDocumentException If the document is not found in the database. + */ + public T find(Class classType, String id) { + assertNotEmpty(classType, "Class"); + assertNotEmpty(id, "id"); + final URI uri = buildUri(getDBUri()).pathEncoded(id).build(); + return get(uri, classType); + } + + /** + * Finds an Object of the specified type. + * + * @param Object type. + * @param classType The class of type T. + * @param id The document id. + * @param params Extra parameters to append. + * @return An object of type T. + * @throws NoDocumentException If the document is not found in the database. + */ + public T find(Class classType, String id, Params params) { + assertNotEmpty(classType, "Class"); + assertNotEmpty(id, "id"); + final URI uri = buildUri(getDBUri()).pathEncoded(id).query(params).build(); + return get(uri, classType); + } + + /** + * Finds an Object of the specified type. + * + * @param Object type. + * @param classType The class of type T. + * @param id The document _id field. + * @param rev The document _rev field. + * @return An object of type T. + * @throws NoDocumentException If the document is not found in the database. + */ + public T find(Class classType, String id, String rev) { + assertNotEmpty(classType, "Class"); + assertNotEmpty(id, "id"); + assertNotEmpty(id, "rev"); + final URI uri = buildUri(getDBUri()).pathEncoded(id).query("rev", rev).build(); + return get(uri, classType); + } + + /** + * This method finds any document given a URI. + *

+ * The URI must be URI-encoded. + * + * @param The class type. + * @param classType The class of type T. + * @param uri The URI as string. + * @return An object of type T. + */ + public T findAny(Class classType, String uri) { + assertNotEmpty(classType, "Class"); + assertNotEmpty(uri, "uri"); + return get(URI.create(uri), classType); + } + + /** + * Finds a document and return the result as {@link InputStream}. + *

+ * Note: The stream must be closed after use to release the connection. + * + * @param id The document _id field. + * @return The result as {@link InputStream} + * @throws NoDocumentException If the document is not found in the database. + * @see #find(String, String) + */ + public InputStream find(String id) { + assertNotEmpty(id, "id"); + return get(buildUri(getDBUri()).path(id).build()); + } + + /** + * Finds a document given id and revision and returns the result as {@link InputStream}. + *

+ * Note: The stream must be closed after use to release the connection. + * + * @param id The document _id field. + * @param rev The document _rev field. + * @return The result as {@link InputStream} + * @throws NoDocumentException If the document is not found in the database. + */ + public InputStream find(String id, String rev) { + assertNotEmpty(id, "id"); + assertNotEmpty(rev, "rev"); + final URI uri = buildUri(getDBUri()).path(id).query("rev", rev).build(); + return get(uri); + } + + /** + * Find documents using a declarative JSON querying syntax. + * + * @param The class type. + * @param jsonQuery The JSON query string. + * @param classOfT The class of type T. + * @return The result of the query as a {@code List } + * @throws CouchDbException If the query failed to execute or the request is invalid. + */ + public List findDocs(String jsonQuery, Class classOfT) { + assertNotEmpty(jsonQuery, "jsonQuery"); + HttpResponse response = null; + try { + response = post(buildUri(getDBUri()).path("_find").build(), jsonQuery); + Reader reader = new InputStreamReader(getStream(response), Charsets.UTF_8); + JsonArray jsonArray = new JsonParser().parse(reader).getAsJsonObject().getAsJsonArray("docs"); + List list = new ArrayList(); + for (JsonElement jsonElem : jsonArray) { + JsonElement elem = jsonElem.getAsJsonObject(); + T t = this.gson.fromJson(elem, classOfT); + list.add(t); + } + return list; + } finally { + close(response); + } + } + + /** + * Checks if a document exist in the database. + * + * @param id The document _id field. + * @return true If the document is found, false otherwise. + */ + public boolean contains(String id) { + assertNotEmpty(id, "id"); + HttpResponse response = null; + try { + response = head(buildUri(getDBUri()).pathEncoded(id).build()); + } catch (NoDocumentException e) { + return false; + } finally { + close(response); + } + return true; + } + + /** + * Saves an object in the database, using HTTP PUT request. + *

+ * If the object doesn't have an _id value, the code will assign a UUID as the document + * id. + * + * @param object The object to save + * @throws DocumentConflictException If a conflict is detected during the save. + * @return {@link Response} + */ + public Response save(Object object) { + return put(getDBUri(), object, true); + } + + /** + * Saves an object in the database using HTTP POST request. + *

+ * The database will be responsible for generating the document id. + * + * @param object The object to save + * @return {@link Response} + */ + public Response post(Object object) { + assertNotEmpty(object, "object"); + HttpResponse response = null; + try { + URI uri = buildUri(getDBUri()).build(); + response = post(uri, getGson().toJson(object)); + return getResponse(response); + } finally { + close(response); + } + } + + /** + * Saves a document with batch=ok query param. + * + * @param object The object to save. + */ + public void batch(Object object) { + assertNotEmpty(object, "object"); + HttpResponse response = null; + try { + URI uri = buildUri(getDBUri()).query("batch", "ok").build(); + response = post(uri, getGson().toJson(object)); + } finally { + close(response); + } + } + + /** + * Updates an object in the database, the object must have the correct _id and _rev + * values. + * + * @param object The object to update + * @throws DocumentConflictException If a conflict is detected during the update. + * @return {@link Response} + */ + public Response update(Object object) { + return put(getDBUri(), object, false); + } + + /** + * Removes a document from the database. + *

+ * The object must have the correct _id and _rev values. + * + * @param object The document to remove as object. + * @throws NoDocumentException If the document is not found in the database. + * @return {@link Response} + */ + public Response remove(Object object) { + assertNotEmpty(object, "object"); + JsonObject jsonObject = getGson().toJsonTree(object).getAsJsonObject(); + final String id = getAsString(jsonObject, "_id"); + final String rev = getAsString(jsonObject, "_rev"); + return remove(id, rev); + } + + /** + * Removes a document from the database given both a document _id and _rev values. + * + * @param id The document _id field. + * @param rev The document _rev field. + * @throws NoDocumentException If the document is not found in the database. + * @return {@link Response} + */ + public Response remove(String id, String rev) { + assertNotEmpty(id, "id"); + assertNotEmpty(rev, "rev"); + final URI uri = buildUri(getDBUri()).pathEncoded(id).query("rev", rev).build(); + return delete(uri); + } + + /** + * Performs bulk documents create and update request. + * + * @param objects The {@link List} of documents objects. + * @param newEdits If false, prevents the database from assigning documents new revision IDs. + * @return {@code List} Containing the resulted entries. + */ + public List bulk(List objects, boolean newEdits) { + assertNotEmpty(objects, "objects"); + HttpResponse response = null; + try { + final String newEditsVal = newEdits ? "\"new_edits\": true, " : "\"new_edits\": false, "; + final String json = String.format("{%s%s%s}", newEditsVal, "\"docs\": ", getGson().toJson(objects)); + final URI uri = buildUri(getDBUri()).path("_bulk_docs").build(); + response = post(uri, json); + return getResponseList(response); + } finally { + close(response); + } + } + + /** + * Saves an attachment to a new document with a generated UUID as the document id. + *

+ * To retrieve an attachment, see {@link #find(String)}. + * + * @param in The {@link InputStream} holding the binary data. + * @param name The attachment name. + * @param contentType The attachment "Content-Type". + * @return {@link Response} + */ + public Response saveAttachment(InputStream in, String name, String contentType) { + assertNotEmpty(in, "in"); + assertNotEmpty(name, "name"); + assertNotEmpty(contentType, "ContentType"); + final URI uri = buildUri(getDBUri()).path(generateUUID()).path("/").path(name).build(); + return put(uri, in, contentType); + } + + /** + * Saves an attachment to an existing document given both a document id and revision, or save to a new document + * given only the id, and rev as {@code null}. + *

+ * To retrieve an attachment, see {@link #find(String)}. + * + * @param in The {@link InputStream} holding the binary data. + * @param name The attachment name. + * @param contentType The attachment "Content-Type". + * @param docId The document id to save the attachment under, or {@code null} to save under a new document. + * @param docRev The document revision to save the attachment under, or {@code null} when saving to a new document. + * @return {@link Response} + */ + public Response saveAttachment(InputStream in, String name, String contentType, String docId, String docRev) { + assertNotEmpty(in, "in"); + assertNotEmpty(name, "name"); + assertNotEmpty(contentType, "ContentType"); + assertNotEmpty(docId, "docId"); + final URI uri = buildUri(getDBUri()).pathEncoded(docId).path("/").path(name).query("rev", docRev).build(); + return put(uri, in, contentType); + } + + /** + * Invokes an Update Handler. + * + *

+     * 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 params The query parameters as {@link Params}. + * @return The output of the request. + */ + public String invokeUpdateHandler(String updateHandlerUri, String docId, Params params) { + assertNotEmpty(updateHandlerUri, "uri"); + assertNotEmpty(docId, "docId"); + final String[] v = updateHandlerUri.split("/"); + final String path = String.format("_design/%s/_update/%s/", v[0], v[1]); + final URI uri = buildUri(getDBUri()).path(path).path(docId).query(params).build(); + final HttpResponse response = executeRequest(new HttpPut(uri)); + return streamToString(getStream(response)); + } + + /** + * Executes a HTTP request. + *

+ * Note: The response must be closed after use to release the connection. + * + * @param request The HTTP request to execute. + * @return {@link HttpResponse} + */ + public HttpResponse executeRequest(HttpRequestBase request) { + try { + return httpClient.execute(host, request, createContext()); + } catch (IOException e) { + request.abort(); + throw new CouchDbException("Error executing request. ", e); + } + } + + /** + * Synchronize all design documents with the database. + */ + public void syncDesignDocsWithDb() { + design().synchronizeAllWithDb(); + } + + /** + * Sets a {@link GsonBuilder} to create {@link Gson} instance. + *

+ * Useful for registering custom serializers/deserializers, such as JodaTime classes. + * + * @param gsonBuilder The {@link GsonBuilder} + */ + public void setGsonBuilder(GsonBuilder gsonBuilder) { + this.gson = initGson(gsonBuilder); + } + + /** + * @return The base URI. + */ + public URI getBaseUri() { + return baseURI; + } + + /** + * @return The database URI. + */ + public URI getDBUri() { + return dbURI; + } + + /** + * @return The Gson instance. + */ + public Gson getGson() { + return gson; + } + + // End - Public API + + /** + * Performs a HTTP GET request. + * + * @return {@link InputStream} + */ + InputStream get(HttpGet httpGet) { + HttpResponse response = executeRequest(httpGet); + return getStream(response); + } + + /** + * Performs a HTTP GET request. + * + * @return {@link InputStream} + */ + InputStream get(URI uri) { + HttpGet get = new HttpGet(uri); + get.addHeader("Accept", "application/json"); + return get(get); + } + + /** + * Performs a HTTP GET request with given Headers. + * + * @return {@link InputStream} + */ + InputStream get(URI uri, Header[] headers) { + HttpGet get = new HttpGet(uri); + get.setHeaders(headers); + 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 GET request with headers. + * + * @return An object of type T + */ + T get(URI uri, Class classType, Header[] headers) { + InputStream in = null; + try { + in = get(uri, headers); + return getGson().fromJson(new InputStreamReader(in, "UTF-8"), classType); + } catch (UnsupportedEncodingException e) { + throw new CouchDbException(e); + } finally { + close(in); + } + } + + /** + * Performs a HTTP HEAD request. + * + * @return {@link HttpResponse} + */ + HttpResponse head(URI uri) { + return executeRequest(new HttpHead(uri)); + } + + /** + * Performs a HTTP PUT request, saves or updates a document. + * + * @return {@link Response} + */ + Response put(URI uri, Object object, boolean newEntity) { + assertNotEmpty(object, "object"); + HttpResponse response = null; + try { + final JsonObject json = getGson().toJsonTree(object).getAsJsonObject(); + String id = getAsString(json, "_id"); + String rev = getAsString(json, "_rev"); + if (newEntity) { // save + assertNull(rev, "rev"); + id = (id == null) ? generateUUID() : id; + } else { // update + assertNotEmpty(id, "id"); + assertNotEmpty(rev, "rev"); + } + final HttpPut put = new HttpPut(buildUri(uri).pathEncoded(id).build()); + setEntity(put, json.toString()); + response = executeRequest(put); + return getResponse(response); + } finally { + close(response); + } + } + + /** + * Performs a HTTP PUT request, saves an attachment. + * + * @return {@link Response} + */ + Response put(URI uri, InputStream instream, String contentType) { + HttpResponse response = null; + try { + final HttpPut httpPut = new HttpPut(uri); + final InputStreamEntity entity = new InputStreamEntity(instream, -1); + entity.setContentType(contentType); + httpPut.setEntity(entity); + response = executeRequest(httpPut); + return getResponse(response); + } finally { + close(response); + } + } + + /** + * Performs a HTTP POST request. + * + * @return {@link HttpResponse} + */ + HttpResponse post(URI uri, String json) { + HttpPost post = new HttpPost(uri); + setEntity(post, json); + return executeRequest(post); + } + + /** + * Performs a HTTP POST request. + * + * @return {@link HttpResponse} + */ + InputStream post(HttpPost post, String json) { + setEntity(post, json); + HttpResponse resp = executeRequest(post); + return getStream(resp); + } + + /** + * Performs a HTTP POST 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); + } + case HttpStatus.SC_NOT_MODIFIED: { + throw new DocumentNotModifiedException(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(); + } + + /** + * @param Object type. + * @param classType The class of type T. + * @param id The document _id field. + * @param rev The document revision to check against. + * @return An Object of type T if it has been modified since the specified revision + * @throws DocumentNotModifiedException If the document has not been modified + */ + public T findIfModified(Class classType, String id, String rev) { + assertNotEmpty(classType, "Class"); + assertNotEmpty(id, "id"); + assertNotEmpty(rev, "rev"); + + final URI uri = buildUri(getDBUri()).pathEncoded(id).build(); + Header[] headers = new Header[]{new BasicHeader("If-None-Match", String.format("\"%s\"", rev))}; + return get(uri, classType, headers); + } } diff --git a/src/main/java/org/lightcouch/CouchDbContext.java b/src/main/java/org/lightcouch/CouchDbContext.java index f0b4949..b4846cc 100644 --- a/src/main/java/org/lightcouch/CouchDbContext.java +++ b/src/main/java/org/lightcouch/CouchDbContext.java @@ -1,5 +1,6 @@ /* * Copyright (C) lightcouch.org + * Copyright (C) 2018 indaba.es * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,11 +76,25 @@ public void deleteDB(String dbName, String confirm) { * @param dbName The Database name */ public void createDB(String dbName) { + this.createDB(dbName, 0); + } + + /** + * Requests CouchDB creates a new database; if one doesn't exist. + * @param dbName The Database name + * @param shards The number of range partitions (> 0) + */ + public void createDB(String dbName, int shards) { assertNotEmpty(dbName, "dbName"); InputStream getresp = null; HttpResponse putresp = null; - final URI uri = buildUri(dbc.getBaseUri()).path(dbName).build(); + URIBuilder builder = buildUri(dbc.getBaseUri()).path(dbName); + if(shards > 0) { + builder = builder.query("q", shards); + } + final URI uri = builder.build(); try { + getresp = dbc.get(uri); } catch (NoDocumentException e) { // db doesn't exist final HttpPut put = new HttpPut(uri); @@ -161,4 +176,24 @@ public List uuids(long count) { final JsonObject json = dbc.findAny(JsonObject.class, uri); return dbc.getGson().fromJson(json.get("uuids").toString(), new TypeToken>(){}.getType()); } + + /** + * Request all database update events in the CouchDB instance. + * @param since + * @return a list of all database events in the CouchDB instance + */ + public DbUpdates dbUpdates(String since) { + InputStream instream = null; + try { + URIBuilder builder = buildUri(dbc.getBaseUri()).path("_db_updates"); + if(since != null && !"".equals(since)) { + builder.query("since", since); + } + instream = dbc.get(builder.build()); + Reader reader = new InputStreamReader(instream, Charsets.UTF_8); + return dbc.getGson().fromJson(reader, DbUpdates.class); + } finally { + close(instream); + } + } } diff --git a/src/main/java/org/lightcouch/CouchDbDesign.java b/src/main/java/org/lightcouch/CouchDbDesign.java index 33da85a..b4e79de 100644 --- a/src/main/java/org/lightcouch/CouchDbDesign.java +++ b/src/main/java/org/lightcouch/CouchDbDesign.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2018 indaba.es * Copyright (C) 2011 lightcouch.org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +19,8 @@ import static java.lang.String.format; import static org.lightcouch.CouchDbUtil.assertNotEmpty; +import static org.lightcouch.CouchDbUtil.assertTrue; +import static org.lightcouch.CouchDbUtil.close; import static org.lightcouch.CouchDbUtil.listResources; import static org.lightcouch.CouchDbUtil.readFile; import static org.lightcouch.CouchDbUtil.removeExtension; @@ -29,6 +32,7 @@ import java.util.List; import java.util.Map; +import org.apache.http.HttpResponse; import org.lightcouch.DesignDocument.MapReduce; import com.google.gson.JsonArray; @@ -139,6 +143,25 @@ public DesignDocument getFromDb(String id, String rev) { return dbc.get(uri, DesignDocument.class); } + /** + * Checks if a design document exist in the database. + * @param id The document _id field excluding "_desing/" prefix. + * @return true If the document is found, false otherwise. + */ + public boolean contains(String id) { + assertNotEmpty(id, "id"); + assertTrue(id.startsWith(DESIGN_PREFIX),"Desing document id should start with "+DESIGN_PREFIX); + HttpResponse response = null; + try { + response = dbc.head(buildUri(dbc.getDBUri()).path(id).build()); + } catch (NoDocumentException e) { + return false; + } finally { + close(response); + } + return true; + } + /** * Gets all design documents from desk. * @see #getFromDesk(String) diff --git a/src/main/java/org/lightcouch/CouchDbInfo.java b/src/main/java/org/lightcouch/CouchDbInfo.java index caaa70f..e634724 100644 --- a/src/main/java/org/lightcouch/CouchDbInfo.java +++ b/src/main/java/org/lightcouch/CouchDbInfo.java @@ -1,4 +1,5 @@ /* + * Copyright (C) 2018 indaba.es * Copyright (C) 2011 lightcouch.org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,7 +34,7 @@ public class CouchDbInfo { @SerializedName("update_seq") private String updateSeq; @SerializedName("purge_seq") - private long purgeSeq; + private String purgeSeq; @SerializedName("compact_running") private boolean compactRunning; @SerializedName("disk_size") @@ -59,7 +60,7 @@ public String getUpdateSeq() { return updateSeq; } - public long getPurgeSeq() { + public String getPurgeSeq() { return purgeSeq; } diff --git a/src/main/java/org/lightcouch/CouchDbUtil.java b/src/main/java/org/lightcouch/CouchDbUtil.java index 35d03e8..f086f82 100644 --- a/src/main/java/org/lightcouch/CouchDbUtil.java +++ b/src/main/java/org/lightcouch/CouchDbUtil.java @@ -1,210 +1,230 @@ -/* - * Copyright (C) 2011 lightcouch.org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.lightcouch; - -import static java.lang.String.format; - -import java.io.Closeable; -import java.io.File; -import java.io.InputStream; -import java.net.URL; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.List; -import java.util.Scanner; -import java.util.Set; -import java.util.UUID; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -import org.apache.http.HttpResponse; - -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - -/** - * Provides various utility methods, for internal use. - * @author Ahmed Yehia - */ -final class CouchDbUtil { - - private CouchDbUtil() { - // Utility class - } - - public static void assertNotEmpty(Object object, String prefix) throws IllegalArgumentException { - if(object == null) { - throw new IllegalArgumentException(format("%s may not be null.", prefix)); - } else if(object instanceof String && ((String)object).length() == 0) { - throw new IllegalArgumentException(format("%s may not be empty.", prefix)); - } - } - - public static void assertNull(Object object, String prefix) throws IllegalArgumentException { - if(object != null) { - throw new IllegalArgumentException(format("%s should be null.", prefix)); - } - } - - public static String generateUUID() { - return UUID.randomUUID().toString().replace("-", ""); - } - - // JSON - - public static T JsonToObject(Gson gson, JsonElement elem, String key, Class classType) { - return gson.fromJson(elem.getAsJsonObject().get(key), classType); - } - - /** - * @return A JSON element as a String, or null if not found. - */ - public static String getAsString(JsonObject j, String e) { - return (j.get(e) == null || j.get(e).isJsonNull()) ? null : j.get(e).getAsString(); - } - - /** - * @return A JSON element as long, or 0 if not found. - */ - public static long getAsLong(JsonObject j, String e) { - return (j.get(e) == null || j.get(e).isJsonNull()) ? 0L : j.get(e).getAsLong(); - } - - /** - * @return A JSON element as int, or 0 if not found. - */ - public static int getAsInt(JsonObject j, String e) { - return (j.get(e) == null || j.get(e).isJsonNull()) ? 0 : j.get(e).getAsInt(); - } - - // Files - - private static final String LINE_SEP = System.getProperty("line.separator"); - - private static final String SPRING_BOOT_DIR = "BOOT-INF/classes/"; - - /** - * List directory contents for a resource folder. Not recursive. - * This is basically a brute-force implementation. - * Works for regular files and also JARs. - * - * @author Greg Briggs - * @param path Should end with "/", but not start with one. - * @return Just the name of each member item, not the full paths. - */ - public static List listResources(String path) { - try { - Class clazz = CouchDbUtil.class; - URL dirURL = clazz.getClassLoader().getResource(path); - if (dirURL != null && dirURL.getProtocol().equals("file")) { - return Arrays.asList(new File(dirURL.toURI()).list()); - } - if (dirURL != null && dirURL.getProtocol().equals("jar")) { - String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")); - JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8")); - Enumeration entries = jar.entries(); - Set result = new HashSet(); - while(entries.hasMoreElements()) { - String name = entries.nextElement().getName(); - if (name.startsWith(SPRING_BOOT_DIR)) { - name = name.substring(SPRING_BOOT_DIR.length()); - } - if (name.startsWith(path)) { - String entry = name.substring(path.length()); - int checkSubdir = entry.indexOf("/"); - if (checkSubdir >= 0) { - entry = entry.substring(0, checkSubdir); - } - if(entry.length() > 0) { - result.add(entry); - } - } - } - close(jar); - return new ArrayList(result); - } - return null; - } catch (Exception e) { - throw new CouchDbException(e); - } - } - - public static String readFile(String path) { - InputStream instream = CouchDbUtil.class.getResourceAsStream(path); - StringBuilder content = new StringBuilder(); - Scanner scanner = null; - try { - scanner = new Scanner(instream); - while(scanner.hasNextLine()) { - content.append(scanner.nextLine() + LINE_SEP); - } - } finally { - scanner.close(); - } - return content.toString(); - } - - /** - * @return {@link InputStream} of {@link HttpResponse} - */ - public static InputStream getStream(HttpResponse response) { - try { - return response.getEntity().getContent(); - } catch (Exception e) { - throw new CouchDbException("Error reading response. ", e); - } - } - - public static String removeExtension(String fileName) { - return fileName.substring(0, fileName.lastIndexOf('.')); - } - - public static String streamToString(InputStream in) { - Scanner s = new Scanner(in); - s.useDelimiter("\\A"); - String str = s.hasNext() ? s.next() : null; - close(in); - close(s); - return str; - } - - /** - * Closes the response input stream. - * - * @param response The {@link HttpResponse} - */ - public static void close(HttpResponse response) { - try { - close(response.getEntity().getContent()); - } catch (Exception e) {} - } - - /** - * Closes a resource. - * - * @param c The {@link Closeable} resource. - */ - public static void close(Closeable c) { - try { - c.close(); - } catch (Exception e) {} - } -} +/* + * Copyright (C) 2018 indaba.es + * Copyright (C) 2011 lightcouch.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.lightcouch; + +import static java.lang.String.format; + +import java.io.Closeable; +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.Set; +import java.util.UUID; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.apache.http.HttpResponse; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * Provides various utility methods, for internal use. + * @author Ahmed Yehia + */ +final class CouchDbUtil { + + private static final String MSG_NOT_NULL = "%s may not be null."; + private static final String MSG_NOT_EMPTY = "%s may not be empty."; + + private CouchDbUtil() { + // Utility class + } + + @SuppressWarnings("rawtypes") + public static void assertNotEmpty(Object object, String prefix) throws IllegalArgumentException { + if(object == null) { + throw new IllegalArgumentException(format(MSG_NOT_NULL, prefix)); + } else if(object instanceof String && ((String)object).length() == 0) { + throw new IllegalArgumentException(format(MSG_NOT_EMPTY, prefix)); + } else if(object instanceof Collection && ((Collection)object).isEmpty()) { + throw new IllegalArgumentException(format(MSG_NOT_EMPTY, prefix)); + } else if(object instanceof Map && ((Map)object).isEmpty()) { + throw new IllegalArgumentException(format(MSG_NOT_EMPTY, prefix)); + } + } + + public static void assertNull(Object object, String prefix) throws IllegalArgumentException { + if(object != null) { + throw new IllegalArgumentException(format(MSG_NOT_NULL, prefix)); + } + } + + public static void assertTrue(boolean expression, String message) throws IllegalArgumentException { + if (!expression) { + throw new IllegalArgumentException(format(message)); + } + } + + public static String generateUUID() { + return UUID.randomUUID().toString().replace("-", ""); + } + + // JSON + + public static T JsonToObject(Gson gson, JsonElement elem, String key, Class classType) { + return gson.fromJson(elem.getAsJsonObject().get(key), classType); + } + + /** + * @return A JSON element as a String, or null if not found. + */ + public static String getAsString(JsonObject j, String e) { + return (j.get(e) == null || j.get(e).isJsonNull()) ? null : j.get(e).getAsString(); + } + + /** + * @return A JSON element as long, or 0 if not found. + */ + public static long getAsLong(JsonObject j, String e) { + return (j.get(e) == null || j.get(e).isJsonNull()) ? 0L : j.get(e).getAsLong(); + } + + /** + * @return A JSON element as int, or 0 if not found. + */ + public static int getAsInt(JsonObject j, String e) { + return (j.get(e) == null || j.get(e).isJsonNull()) ? 0 : j.get(e).getAsInt(); + } + + // Files + + private static final String LINE_SEP = System.getProperty("line.separator"); + + private static final String SPRING_BOOT_DIR = "BOOT-INF/classes/"; + + /** + * List directory contents for a resource folder. Not recursive. + * This is basically a brute-force implementation. + * Works for regular files and also JARs. + * + * @author Greg Briggs + * @param path Should end with "/", but not start with one. + * @return Just the name of each member item, not the full paths. + */ + public static List listResources(String path) { + JarFile jar = null; + try { + Class clazz = CouchDbUtil.class; + URL dirURL = clazz.getClassLoader().getResource(path); + if (dirURL != null && dirURL.getProtocol().equals("file")) { + return Arrays.asList(new File(dirURL.toURI()).list()); + } + if (dirURL != null && dirURL.getProtocol().equals("jar")) { + String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")); + jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8")); + Enumeration entries = jar.entries(); + Set result = new HashSet(); + while(entries.hasMoreElements()) { + String name = entries.nextElement().getName(); + if (name.startsWith(SPRING_BOOT_DIR)) { + name = name.substring(SPRING_BOOT_DIR.length()); + } + if (name.startsWith(path)) { + String entry = name.substring(path.length()); + int checkSubdir = entry.indexOf("/"); + if (checkSubdir >= 0) { + entry = entry.substring(0, checkSubdir); + } + if(entry.length() > 0) { + result.add(entry); + } + } + } + return new ArrayList(result); + } + return null; + } catch (Exception e) { + throw new CouchDbException(e); + }finally { + close(jar); + } + } + + public static String readFile(String path) { + InputStream instream = CouchDbUtil.class.getResourceAsStream(path); + StringBuilder content = new StringBuilder(); + Scanner scanner = null; + try { + scanner = new Scanner(instream); + while(scanner.hasNextLine()) { + content.append(scanner.nextLine() + LINE_SEP); + } + } finally { + close(instream); + close(scanner); + } + return content.toString(); + } + + /** + * @return {@link InputStream} of {@link HttpResponse} + */ + public static InputStream getStream(HttpResponse response) { + try { + return response.getEntity().getContent(); + } catch (Exception e) { + throw new CouchDbException("Error reading response. ", e); + } + } + + public static String removeExtension(String fileName) { + return fileName.substring(0, fileName.lastIndexOf('.')); + } + + public static String streamToString(InputStream in) { + Scanner s = new Scanner(in); + s.useDelimiter("\\A"); + String str = s.hasNext() ? s.next() : null; + close(in); + close(s); + return str; + } + + /** + * Closes the response input stream. + * + * @param response The {@link HttpResponse} + */ + public static void close(HttpResponse response) { + try { + close(response.getEntity().getContent()); + } catch (Exception e) {} + } + + /** + * Closes a resource. + * + * @param c The {@link Closeable} resource. + */ + public static void close(Closeable c) { + try { + c.close(); + } catch (Exception e) {} + } +} diff --git a/src/main/java/org/lightcouch/DbUpdates.java b/src/main/java/org/lightcouch/DbUpdates.java new file mode 100644 index 0000000..eac65bb --- /dev/null +++ b/src/main/java/org/lightcouch/DbUpdates.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 indaba.es + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lightcouch; + +import java.util.List; + +import com.google.gson.annotations.SerializedName; + +public class DbUpdates { + + private List results; + + @SerializedName("last_seq") + private String lastSeq; + + public List getResults() { + return results; + } + + public void setResults(List results) { + this.results = results; + } + + public String getLastSeq() { + return lastSeq; + } + + public void setLastSeq(String lastSeq) { + this.lastSeq = lastSeq; + } +} \ No newline at end of file diff --git a/src/main/java/org/lightcouch/DbUpdatesResult.java b/src/main/java/org/lightcouch/DbUpdatesResult.java new file mode 100644 index 0000000..3a55db6 --- /dev/null +++ b/src/main/java/org/lightcouch/DbUpdatesResult.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 indaba.es + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lightcouch; + +import com.google.gson.annotations.SerializedName; + +public class DbUpdatesResult { + + @SerializedName("db_name") + private String dbName; + + private String type; + + private String seq; + + public String getDbName() { + return dbName; + } + + public void setDbName(String dbName) { + this.dbName = dbName; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getSeq() { + return seq; + } + + public void setSeq(String seq) { + this.seq = seq; + } +} diff --git a/src/main/java/org/lightcouch/DocumentNotModifiedException.java b/src/main/java/org/lightcouch/DocumentNotModifiedException.java new file mode 100644 index 0000000..3effa47 --- /dev/null +++ b/src/main/java/org/lightcouch/DocumentNotModifiedException.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2019 Indaba Consultores SL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.lightcouch; + +/** + * Thrown when document has not been modified + */ +public class DocumentNotModifiedException extends CouchDbException { + + private static final long serialVersionUID = 1L; + + public DocumentNotModifiedException(String message) { + super(message); + } + + public DocumentNotModifiedException(Throwable cause) { + super(cause); + } + + public DocumentNotModifiedException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/lightcouch/Local.java b/src/main/java/org/lightcouch/Local.java new file mode 100644 index 0000000..b3ce412 --- /dev/null +++ b/src/main/java/org/lightcouch/Local.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2018 indaba.es + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.lightcouch; + +import static org.lightcouch.CouchDbUtil.assertNotEmpty; +import static org.lightcouch.CouchDbUtil.close; +import static org.lightcouch.CouchDbUtil.getAsString; +import static org.lightcouch.URIBuilder.buildUri; + +import java.net.URI; +import java.util.List; + +import org.apache.http.HttpResponse; + +import com.google.gson.JsonObject; + +public class Local { + + private static final String LOCAL_PATH = "_local"; + + private CouchDbClientBase dbc; + private URI dbURI; + private View localDocsView; + + Local(CouchDbClientBase dbc) { + this.dbc = dbc; + dbURI = buildUri(dbc.getDBUri()).path(LOCAL_PATH).path("/").build(); + localDocsView = new View(dbc, "_local_docs"); + } + + public View localDocs() { + return localDocsView; + } + + public List findAll() { + return findAll(JsonObject.class); + } + + public List findAll(Class classType) { + assertNotEmpty(classType, "Class"); + return (List) localDocsView.includeDocs(true).query(classType); + } + + /** + * Finds JSON Object. + * + * @param id The document id. + * @return An JSON object. + * @throws NoDocumentException If the document is not found in the database. + */ + public JsonObject find(String id) { + return find(JsonObject.class, id); + } + + /** + * 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 = buildURIforLocal(id); + return dbc.get(uri, classType); + } + + /** + * 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 { + final URI uri = buildURIforLocal(id); + response = dbc.head(uri); + } catch (NoDocumentException e) { + return false; + } finally { + close(response); + } + return true; + } + + public Response save(Object object) { + return dbc.put(dbURI, object, true); + } + + public Response update(Object object) { + + final JsonObject json = dbc.getGson().toJsonTree(object).getAsJsonObject(); + String id = CouchDbUtil.getAsString(json, "_id"); + URI baseURI = buildURIforLocal(id, false); + return dbc.put(baseURI, 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 = dbc.getGson().toJsonTree(object).getAsJsonObject(); + final String id = getAsString(jsonObject, "_id"); + return remove(id); + } + + public Response removeWithRev(Object object) { + assertNotEmpty(object, "object"); + JsonObject jsonObject = dbc.getGson().toJsonTree(object).getAsJsonObject(); + final String id = getAsString(jsonObject, "_id"); + final String rev = getAsString(jsonObject, "_rev"); + return remove(id, rev); + } + + public Response remove(String id) { + assertNotEmpty(id, "id"); + final URI docURI = buildURIforLocal(id); + return dbc.delete(docURI); + } + + public Response remove(String id, String rev) { + assertNotEmpty(id, "id"); + assertNotEmpty(id, "rev"); + final URI docURI = buildUri(buildURIforLocal(id)).query("rev", rev).build(); + return dbc.delete(docURI); + } + + private URI buildURIforLocal(String id) { + + return buildURIforLocal(id, true); + } + + private URI buildURIforLocal(String id, boolean includeId) { + URI baseURI = dbURI; + if (id.startsWith(LOCAL_PATH)) { + baseURI = dbc.getDBUri(); + } + if (includeId) { + return buildUri(baseURI).pathEncoded(id).build(); + } else { + return buildUri(baseURI).build(); + } + } +} diff --git a/src/main/java/org/lightcouch/PurgeResponse.java b/src/main/java/org/lightcouch/PurgeResponse.java new file mode 100644 index 0000000..359b813 --- /dev/null +++ b/src/main/java/org/lightcouch/PurgeResponse.java @@ -0,0 +1,31 @@ +package org.lightcouch; + +import java.util.List; +import java.util.Map; + +import com.google.gson.annotations.SerializedName; + +public class PurgeResponse { + + @SerializedName("purge_seq") + private String purgeSeq; + + public String getPurgeSeq() { + return purgeSeq; + } + + public void setPurgeSeq(String purgeSeq) { + this.purgeSeq = purgeSeq; + } + + private Map> purged; + + public Map> getPurged() { + return purged; + } + + public void setPurged(Map> purged) { + this.purged = purged; + } + +} diff --git a/src/main/java/org/lightcouch/URIBuilder.java b/src/main/java/org/lightcouch/URIBuilder.java index 8bc6d81..6ee5530 100644 --- a/src/main/java/org/lightcouch/URIBuilder.java +++ b/src/main/java/org/lightcouch/URIBuilder.java @@ -29,13 +29,15 @@ * @author Ahmed Yehia * */ -class URIBuilder { +public class URIBuilder { private String scheme; private String host; private int port; private String path = ""; private final List params = new ArrayList(); - + private String user; + private String password; + public static URIBuilder buildUri() { return new URIBuilder(); } @@ -94,23 +96,43 @@ public URIBuilder query(Params params) { return this; } + public URIBuilder user(String user) { + this.user = user; + return this; + } + + public URIBuilder password(String password) { + this.password = password; + return this; + } + public URI build() { + return build(false); + } + + public URI buildWithCredentials() { + return build(true); + } + + private URI build(boolean includeCredentials) { final StringBuilder query = new StringBuilder(); - + for (int i = 0; i < params.size(); i++) { String amp = (i != params.size() - 1) ? "&" : ""; query.append(params.get(i) + amp); } - + String q = (query.length() == 0) ? "" : "?" + query; - String uri = String.format("%s://%s:%s%s%s", new Object[] { scheme, host, port, path, q }); - + String uri = ""; + if (includeCredentials && user != null && password != null) { + uri = String.format("%s://%s:%s@%s:%d%s%s", scheme, user, password, host, port, path, q); + } else { + uri = String.format("%s://%s:%d%s%s", scheme, host, port, path, q); + } try { return new URI(uri); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } - } - } diff --git a/src/test/java/org/lightcouch/tests/AttachmentsTest.java b/src/test/java/org/lightcouch/tests/AttachmentsTest.java index d56a42b..11a9d18 100644 --- a/src/test/java/org/lightcouch/tests/AttachmentsTest.java +++ b/src/test/java/org/lightcouch/tests/AttachmentsTest.java @@ -26,27 +26,12 @@ import java.util.UUID; import org.apache.commons.codec.binary.Base64; -import org.junit.AfterClass; -import org.junit.BeforeClass; import org.junit.Test; import org.lightcouch.Attachment; -import org.lightcouch.CouchDbClient; import org.lightcouch.Params; import org.lightcouch.Response; -public class AttachmentsTest { - - private static CouchDbClient dbClient; - - @BeforeClass - public static void setUpClass() { - dbClient = new CouchDbClient(); - } - - @AfterClass - public static void tearDownClass() { - dbClient.shutdown(); - } +public class AttachmentsTest extends CouchDbTestBase { @Test public void attachmentInline() { diff --git a/src/test/java/org/lightcouch/tests/BulkDocumentTest.java b/src/test/java/org/lightcouch/tests/BulkDocumentTest.java index 4d40083..cc0be0b 100644 --- a/src/test/java/org/lightcouch/tests/BulkDocumentTest.java +++ b/src/test/java/org/lightcouch/tests/BulkDocumentTest.java @@ -23,28 +23,13 @@ import java.util.Arrays; import java.util.List; -import org.junit.AfterClass; -import org.junit.BeforeClass; import org.junit.Test; -import org.lightcouch.CouchDbClient; import org.lightcouch.Response; import com.google.gson.JsonObject; -public class BulkDocumentTest { - - private static CouchDbClient dbClient; - - @BeforeClass - public static void setUpClass() { - dbClient = new CouchDbClient(); - } - - @AfterClass - public static void tearDownClass() { - dbClient.shutdown(); - } - +public class BulkDocumentTest extends CouchDbTestBase { + @Test public void bulkModifyDocs() { List newDocs = new ArrayList(); diff --git a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java index 66e1704..6b42777 100644 --- a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java +++ b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java @@ -1,17 +1,14 @@ /* * Copyright (C) 2011 lightcouch.org * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ package org.lightcouch.tests; @@ -20,83 +17,210 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; -import org.junit.AfterClass; -import org.junit.BeforeClass; +import org.junit.Assume; import org.junit.Test; import org.lightcouch.Changes; import org.lightcouch.ChangesResult; import org.lightcouch.ChangesResult.Row; -import org.lightcouch.CouchDbClient; import org.lightcouch.CouchDbInfo; import org.lightcouch.Response; 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(2000) - .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(); - } - } +public class ChangeNotificationsTest extends CouchDbTestBase { + + @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_seqInterval() { + Assume.assumeTrue(isCouchDB2()); + + dbClient.save(new Foo()); + dbClient.save(new Foo()); + dbClient.save(new Foo()); + dbClient.save(new Foo()); + dbClient.save(new Foo()); + + ChangesResult changes = dbClient.changes().includeDocs(true).limit(5).seqInterval(2).getChanges(); + + List rows = changes.getResults(); + + int seqs = 0; + for (Row row : rows) { + List revs = row.getChanges(); + String docId = row.getId(); + JsonObject doc = row.getDoc(); + if (row.getSeq() != null) + seqs++; + + assertNotNull(revs); + assertNotNull(docId); + assertNotNull(doc); + } + + assertThat(rows.size(), is(5)); + assertThat(seqs, is(2)); + } + + @Test + public void changes_normalFeed_selector() { + + Assume.assumeTrue(isCouchDB2()); + + 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_normalFeed_docIds() { + + Assume.assumeTrue(isCouchDB2()); + + dbClient.save(new Foo("test-filter-id-1")); + dbClient.save(new Foo("test-filter-id-2")); + dbClient.save(new Foo("test-filter-id-3")); + List docIds = Arrays.asList("test-filter-id-1", "test-filter-id-2"); + ChangesResult changes = dbClient.changes().includeDocs(true).docIds(docIds).getChanges(); + + List rows = changes.getResults(); + + List returnedIds = new ArrayList(); + for (Row row : rows) { + List revs = row.getChanges(); + String docId = row.getId(); + JsonObject doc = row.getDoc(); + + assertNotNull(revs); + assertNotNull(docId); + assertNotNull(doc); + + returnedIds.add(docId); + + } + + assertTrue(returnedIds.contains("test-filter-id-1")); + assertTrue(returnedIds.contains("test-filter-id-2")); + assertThat(rows.size(), is(2)); + } + + @Test(expected=IllegalArgumentException.class) + public void changes_normalFeed_filters_notcompatible1() { + dbClient.changes().filter("a").selector("{a}"); + } + + @Test(expected=IllegalArgumentException.class) + public void changes_normalFeed_filters_notcompatible2() { + dbClient.changes().filter("a").docIds(Arrays.asList("test-filter-id-1", "test-filter-id-2")); + } + + @Test(expected=IllegalArgumentException.class) + public void changes_normalFeed_filters_notcompatible3() { + dbClient.changes().selector("a").docIds(Arrays.asList("test-filter-id-1", "test-filter-id-2")); + } + + @Test(expected=IllegalArgumentException.class) + public void changes_normalFeed_filters_notcompatible4() { + dbClient.changes().selector("a").filter("a"); + } + + @Test(expected=IllegalArgumentException.class) + public void changes_normalFeed_filters_notcompatible5() { + dbClient.changes().docIds(Arrays.asList("test-filter-id-1", "test-filter-id-2")).selector("a"); + } + + @Test(expected=IllegalArgumentException.class) + public void changes_normalFeed_filters_notcompatible6() { + dbClient.changes().docIds(Arrays.asList("test-filter-id-1", "test-filter-id-2")).filter("a"); + } + + @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(2000).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() { + + Assume.assumeTrue(isCouchDB2()); + + 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(); + } + } } diff --git a/src/test/java/org/lightcouch/tests/CouchDbConfigTest.java b/src/test/java/org/lightcouch/tests/CouchDbConfigTest.java new file mode 100644 index 0000000..57fb87c --- /dev/null +++ b/src/test/java/org/lightcouch/tests/CouchDbConfigTest.java @@ -0,0 +1,82 @@ +package org.lightcouch.tests; + + +import java.io.InputStream; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.lightcouch.CouchDbProperties; + +class CouchDbConfigTest { + private static final Log log = LogFactory.getLog(CouchDbConfigTest.class); + private static final String DEFAULT_FILE = "couchdb.properties"; + + private Properties properties = new Properties(); + private String configFile; + private CouchDbProperties dbProperties; + + public CouchDbConfigTest() { + this(DEFAULT_FILE); + } + + public CouchDbConfigTest(String configFile) { + this.configFile = configFile; + try { + InputStream instream = CouchDbConfigTest.class.getClassLoader().getResourceAsStream(configFile); + properties.load(instream); + } catch (Exception e) { + String msg = "Could not read configuration file from the classpath: " + configFile; + log.error(msg); + throw new IllegalStateException(msg, e); + } + readProperties(); + } + + private void readProperties() { + try { + // required + dbProperties = new CouchDbProperties(); + dbProperties.setDbName(getProperty("couchdb.name", true)); + dbProperties.setCreateDbIfNotExist(new Boolean(getProperty("couchdb.createdb.if-not-exist", true))); + dbProperties.setProtocol(getProperty("couchdb.protocol", true)); + dbProperties.setHost(getProperty("couchdb.host", true)); + dbProperties.setPort(Integer.parseInt(getProperty("couchdb.port", true))); + dbProperties.setUsername(getProperty("couchdb.username", true)); + dbProperties.setPassword(getProperty("couchdb.password", true)); + + // optional + dbProperties.setPath(getProperty("couchdb.path", false)); + dbProperties.setSocketTimeout(getPropertyAsInt("couchdb.http.socket.timeout", false)); + dbProperties.setConnectionTimeout(getPropertyAsInt("couchdb.http.connection.timeout", false)); + dbProperties.setMaxConnections(getPropertyAsInt("couchdb.max.connections", false)); + dbProperties.setProxyHost(getProperty("couchdb.proxy.host", false)); + dbProperties.setProxyPort(getPropertyAsInt("couchdb.proxy.port", false)); + + } catch (Exception e) { + throw new IllegalStateException(e); + } + properties = null; + } + + public CouchDbProperties getProperties() { + return dbProperties; + } + + private String getProperty(String key, boolean isRequired) { + String property = properties.getProperty(key); + if(property == null && isRequired) { + String msg = String.format("A required property is missing. Key: %s, File: %s", key, configFile); + log.error(msg); + throw new IllegalStateException(msg); + } else { + return (property != null && property.length() != 0) ? property.trim() : null; + } + } + + private int getPropertyAsInt(String key, boolean isRequired) { + String prop = getProperty(key, isRequired); + return (prop != null) ? Integer.parseInt(prop) : 0; + } +} \ No newline at end of file diff --git a/src/test/java/org/lightcouch/tests/CouchDbTestBase.java b/src/test/java/org/lightcouch/tests/CouchDbTestBase.java new file mode 100644 index 0000000..d18651f --- /dev/null +++ b/src/test/java/org/lightcouch/tests/CouchDbTestBase.java @@ -0,0 +1,43 @@ +package org.lightcouch.tests; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.lightcouch.CouchDbClient; + +import com.github.zafarkhaja.semver.Version; + +public class CouchDbTestBase { + + protected static CouchDbClient dbClient; + protected static CouchDbConfigTest dbClientConfig; + + @BeforeClass + public static void setUpClass() { + dbClient = new CouchDbClient(); + dbClientConfig = new CouchDbConfigTest(); + } + + @AfterClass + public static void tearDownClass() { + dbClient.context().deleteDB(dbClientConfig.getProperties().getDbName(), "delete database"); + dbClient.shutdown(); + } + + protected boolean isCouchDB23() { + return isCouchDBVersion(">=2.3.0"); + } + + protected boolean isCouchDB2() { + return isCouchDBVersion(">=2.0.0"); + } + + protected boolean isCouchDB1() { + return isCouchDBVersion(">=0.0.0 & <2.0.0"); + } + + protected boolean isCouchDBVersion(String versionExpression) { + String version = dbClient.context().serverVersion(); + Version serverVersion = Version.valueOf(version); + return serverVersion.satisfies(versionExpression); + } +} diff --git a/src/test/java/org/lightcouch/tests/DBServerTest.java b/src/test/java/org/lightcouch/tests/DBServerTest.java index f174bc9..a17f9cc 100644 --- a/src/test/java/org/lightcouch/tests/DBServerTest.java +++ b/src/test/java/org/lightcouch/tests/DBServerTest.java @@ -23,25 +23,11 @@ import java.util.List; -import org.junit.AfterClass; -import org.junit.BeforeClass; import org.junit.Test; -import org.lightcouch.CouchDbClient; import org.lightcouch.CouchDbInfo; -public class DBServerTest { +public class DBServerTest extends CouchDbTestBase { - private static CouchDbClient dbClient; - - @BeforeClass - public static void setUpClass() { - dbClient = new CouchDbClient(); - } - - @AfterClass - public static void tearDownClass() { - dbClient.shutdown(); - } @Test public void dbInfo() { diff --git a/src/test/java/org/lightcouch/tests/DbUpdatesTest.java b/src/test/java/org/lightcouch/tests/DbUpdatesTest.java new file mode 100644 index 0000000..fc178d2 --- /dev/null +++ b/src/test/java/org/lightcouch/tests/DbUpdatesTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2011 lightcouch.org + * Copyright (C) 2018 indaba.es + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lightcouch.tests; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Assume; +import org.junit.Test; +import org.lightcouch.DbUpdates; +import org.lightcouch.Response; + +public class DbUpdatesTest extends CouchDbTestBase { + + private static final String SINCE_PARAM_NOW = "now"; + + @Test + public void testDbUpdates() throws InterruptedException { + + Assume.assumeTrue(isCouchDB2()); + + DbUpdates updates = dbClient.context().dbUpdates(SINCE_PARAM_NOW); + + assertNotNull(updates); + assertTrue(updates.getResults().isEmpty()); + + String lastSeqNow = updates.getLastSeq(); + + Response response = dbClient.save(new Foo()); + assertNull(response.getError()); + + updates = dbClient.context().dbUpdates(lastSeqNow); + String lastSeq = null; + int count = 0; + while((lastSeq == null || lastSeqNow.equals(lastSeq)) && count < 10) { + System.out.println("Equal sequences!"); + Thread.sleep(100); + updates = dbClient.context().dbUpdates(lastSeqNow); + lastSeq = updates.getLastSeq(); + count++; + } + + assertFalse(lastSeqNow.equals(lastSeq)); + assertFalse(updates.getResults().isEmpty()); + } +} diff --git a/src/test/java/org/lightcouch/tests/DesignDocumentsTest.java b/src/test/java/org/lightcouch/tests/DesignDocumentsTest.java index 0e6061f..15dbf11 100644 --- a/src/test/java/org/lightcouch/tests/DesignDocumentsTest.java +++ b/src/test/java/org/lightcouch/tests/DesignDocumentsTest.java @@ -1,69 +1,74 @@ -/* - * Copyright (C) 2011 lightcouch.org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.lightcouch.tests; - -import static org.hamcrest.CoreMatchers.not; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertEquals; - -import java.util.List; - -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; -import org.lightcouch.CouchDbClient; -import org.lightcouch.DesignDocument; - -public class DesignDocumentsTest { - - private static CouchDbClient dbClient; - - @BeforeClass - public static void setUpClass() { - dbClient = new CouchDbClient(); - } - - @AfterClass - public static void tearDownClass() { - dbClient.shutdown(); - } - - @Test - public void designDocSync() { - DesignDocument designDoc = dbClient.design().getFromDesk("example"); - dbClient.design().synchronizeWithDb(designDoc); - } - - @Test - public void designDocCompare() { - DesignDocument designDoc1 = dbClient.design().getFromDesk("example"); - dbClient.design().synchronizeWithDb(designDoc1); - - DesignDocument designDoc11 = dbClient.design().getFromDb("_design/example"); - - assertEquals(designDoc1, designDoc11); - } - - @Test - public void designDocs() { - List designDocs = dbClient.design().getAllFromDesk(); - dbClient.syncDesignDocsWithDb(); - - assertThat(designDocs.size(), not(0)); - } - -} +/* + * Copyright (C) 2011 indaba.es + * Copyright (C) 2011 lightcouch.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.lightcouch.tests; + +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.util.List; + +import org.junit.Test; +import org.lightcouch.DesignDocument; + +public class DesignDocumentsTest extends CouchDbTestBase { + + @Test + public void designDocSync() { + DesignDocument designDoc = dbClient.design().getFromDesk("example"); + dbClient.design().synchronizeWithDb(designDoc); + } + + @Test + public void designDocCompare() { + DesignDocument designDoc1 = dbClient.design().getFromDesk("example"); + dbClient.design().synchronizeWithDb(designDoc1); + + DesignDocument designDoc11 = dbClient.design().getFromDb("_design/example"); + + assertEquals(designDoc1, designDoc11); + } + + @Test + public void designDocs() { + List designDocs = dbClient.design().getAllFromDesk(); + dbClient.syncDesignDocsWithDb(); + + assertThat(designDocs.size(), not(0)); + } + + @Test + public void containsDesignDoc() { + + assertFalse(dbClient.design().contains("_design/test-design-doc-unknown")); + + DesignDocument designDoc1 = dbClient.design().getFromDesk("test-design-doc"); + dbClient.design().synchronizeWithDb(designDoc1); + + assertTrue(dbClient.design().contains("_design/test-design-doc")); + + } + + @Test(expected=IllegalArgumentException.class) + public void containsDesignDocIncorrectId() { + dbClient.design().contains("test-design-doc"); + } + +} diff --git a/src/test/java/org/lightcouch/tests/DocumentsCRUDTest.java b/src/test/java/org/lightcouch/tests/DocumentsCRUDTest.java index c25e8f3..25158f5 100644 --- a/src/test/java/org/lightcouch/tests/DocumentsCRUDTest.java +++ b/src/test/java/org/lightcouch/tests/DocumentsCRUDTest.java @@ -27,10 +27,7 @@ import java.util.Map; import java.util.UUID; -import org.junit.AfterClass; -import org.junit.BeforeClass; import org.junit.Test; -import org.lightcouch.CouchDbClient; import org.lightcouch.DocumentConflictException; import org.lightcouch.NoDocumentException; import org.lightcouch.Params; @@ -39,19 +36,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; -public class DocumentsCRUDTest { - - private static CouchDbClient dbClient; - - @BeforeClass - public static void setUpClass() { - dbClient = new CouchDbClient(); - } - - @AfterClass - public static void tearDownClass() { - dbClient.shutdown(); - } +public class DocumentsCRUDTest extends CouchDbTestBase { // Find diff --git a/src/test/java/org/lightcouch/tests/ETagSupportTest.java b/src/test/java/org/lightcouch/tests/ETagSupportTest.java new file mode 100644 index 0000000..56cfb05 --- /dev/null +++ b/src/test/java/org/lightcouch/tests/ETagSupportTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2019 Indaba Consultores SL + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.lightcouch.tests; + +import org.junit.Assert; +import org.junit.Test; +import org.lightcouch.DocumentNotModifiedException; +import org.lightcouch.Response; + +import java.util.UUID; + +public class ETagSupportTest extends CouchDbTestBase { + + private static String generateUUID() { + return UUID.randomUUID().toString().replace("-", ""); + } + + @Test + public void testFindIfModified() { + Bar doc = new Bar(); + String initialBar = "Initial"; + doc.setBar(initialBar); + doc.setId(generateUUID()); + Response response = dbClient.save(doc); + Bar initial = dbClient.find(Bar.class, response.getId()); + Assert.assertEquals(initialBar, initial.getBar()); + try { + dbClient.findIfModified(Bar.class, initial.getId(), initial.getRevision()); + Assert.fail("Should not return if document has not been modified."); + } catch (DocumentNotModifiedException e) { + Assert.assertTrue(true); + } + String updatedBar = "Updated"; + initial.setBar(updatedBar); + dbClient.update(initial); + try { + Bar updated = dbClient.findIfModified(Bar.class, initial.getId(), initial.getRevision()); + Assert.assertEquals(updatedBar, updated.getBar()); + } catch (DocumentNotModifiedException e) { + Assert.fail("Should correctly return object if document has been modified."); + } + } +} diff --git a/src/test/java/org/lightcouch/tests/LocalDocumentsCRUDTest.java b/src/test/java/org/lightcouch/tests/LocalDocumentsCRUDTest.java new file mode 100644 index 0000000..1348fef --- /dev/null +++ b/src/test/java/org/lightcouch/tests/LocalDocumentsCRUDTest.java @@ -0,0 +1,228 @@ +package org.lightcouch.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.junit.Assume; +import org.junit.Test; +import org.lightcouch.DocumentConflictException; +import org.lightcouch.NoDocumentException; +import org.lightcouch.Response; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +public class LocalDocumentsCRUDTest extends CouchDbTestBase { + + @Test + public void findById() { + Response response = dbClient.local().save(new Foo("test")); + Foo foo = dbClient.local().find(Foo.class, response.getId()); + assertNotNull(foo); + foo = dbClient.local().find(Foo.class, "test"); + assertNotNull(foo); + } + + @Test + public void findByIdContainSlash() { + String generatedId = generateUUID() + "/" + generateUUID(); + Response response = dbClient.local().save(new Foo(generatedId)); + Foo foo = dbClient.local().find(Foo.class, response.getId()); + assertNotNull(foo); + + Foo foo2 = dbClient.local().find(Foo.class, generatedId); + assertNotNull(foo2); + } + + @Test + public void findJsonObject() { + Response response = dbClient.local().save(new Foo()); + JsonObject jsonObject = dbClient.local().find(JsonObject.class, response.getId()); + assertNotNull(jsonObject); + + JsonObject jsonObject2 = dbClient.local().find(response.getId()); + assertNotNull(jsonObject2); + } + + @Test(expected = IllegalArgumentException.class) + public void findWithInvalidId_throwsIllegalArgumentException() { + dbClient.local().find(Foo.class, ""); + } + + @Test(expected = NoDocumentException.class) + public void findWithUnknownId_throwsNoDocumentException() { + dbClient.local().find(Foo.class, generateUUID()); + } + + @Test + public void contains() { + Response response = dbClient.local().save(new Foo()); + boolean found = dbClient.local().contains(response.getId()); + assertTrue(found); + + found = dbClient.local().contains(generateUUID()); + assertFalse(found); + } + + // Save + + @Test + public void savePOJO() { + Response response = dbClient.local().save(new Foo()); + assertNotNull(response.getId()); + } + + @Test + public void saveMap() { + + Map map = new HashMap(); + map.put("_id", generateUUID()); + map.put("field1", "value1"); + Response response = dbClient.local().save(map); + assertNotNull(response.getId()); + } + + @Test + public void saveJsonObject() { + JsonObject json = new JsonObject(); + json.addProperty("_id", generateUUID()); + json.add("json-array", new JsonArray()); + Response response = dbClient.local().save(json); + assertNotNull(response.getId()); + } + + @Test + public void saveWithIdContainSlash() { + String idWithSlash = "a/b/" + generateUUID(); + Response response = dbClient.local().save(new Foo(idWithSlash)); + assertEquals("_local/" + idWithSlash, response.getId()); + } + + @Test(expected = IllegalArgumentException.class) + public void saveInvalidObject_throwsIllegalArgumentException() { + dbClient.save(null); + } + + @Test(expected = IllegalArgumentException.class) + public void saveNewDocWithRevision_throwsIllegalArgumentException() { + Bar bar = new Bar(); + bar.setRevision("unkown"); + dbClient.save(bar); + } + + @Test + public void saveDocWithDuplicateId_allowed() { + Assume.assumeTrue(isCouchDB2()); + String id = generateUUID(); + dbClient.local().save(new Foo(id, "one")); + Foo foo = dbClient.local().find(Foo.class, id); + assertTrue("one".equals(foo.getTitle())); + + dbClient.local().save(new Foo(id, "two")); + foo = dbClient.local().find(Foo.class, id); + assertTrue("two".equals(foo.getTitle())); + } + + @Test + public void saveDocWithDuplicateId_throwsDocumentConflictException() { + Assume.assumeTrue(isCouchDB1()); + boolean exceptionCatched = false; + String id = generateUUID(); + dbClient.save(new Foo(id)); + try { + dbClient.save(new Foo(id)); + } catch (DocumentConflictException e) { + exceptionCatched = true; + } + assertTrue(exceptionCatched); + } + + // Update + + @Test + public void update() { + Response response = dbClient.local().save(new Foo()); + Foo foo = dbClient.local().find(Foo.class, response.getId()); + response = dbClient.local().update(foo); + assertNotNull(response.getId()); + } + + @Test + public void updateWithIdContainSlash() { + String idWithSlash = "a/" + generateUUID(); + Response response = dbClient.local().save(new Bar(idWithSlash)); + + Bar bar = dbClient.local().find(Bar.class, response.getId()); + Response responseUpdate = dbClient.local().update(bar); + assertEquals("_local/" + idWithSlash, responseUpdate.getId()); + } + + // Delete + + @Test + public void deleteObjectV2() { + Assume.assumeTrue(isCouchDB2()); + Response response = dbClient.local().save(new Foo()); + Foo foo = dbClient.local().find(Foo.class, response.getId()); + dbClient.local().remove(foo); + } + + @Test + public void deleteObjectV1() { + Response response = dbClient.local().save(new Foo()); + Foo foo = dbClient.local().find(Foo.class, response.getId()); + dbClient.local().removeWithRev(foo); + } + + @Test + public void deleteById() { + Assume.assumeTrue(isCouchDB2()); + Response response = dbClient.local().save(new Foo()); + response = dbClient.local().remove(response.getId()); + assertNotNull(response); + } + + @Test + public void deleteByIdAndRevValues() { + Response response = dbClient.local().save(new Foo()); + response = dbClient.local().remove(response.getId(), response.getRev()); + assertNotNull(response); + } + + @Test + public void deleteByIdContainSlash() { + Assume.assumeTrue(isCouchDB2()); + String idWithSlash = "a/" + generateUUID(); + Response response = dbClient.local().save(new Bar(idWithSlash)); + + Response responseRemove = dbClient.local().remove(response.getId()); + assertEquals("_local/" + idWithSlash, responseRemove.getId()); + } + + @Test + public void findAllLocalDocs() { + Assume.assumeTrue(isCouchDB2()); + List list = dbClient.local().findAll(); + int intialSize = list.size(); + dbClient.local().save(new Foo("test1")); + dbClient.local().save(new Foo("test2")); + dbClient.save(new Foo("test3")); + + list = dbClient.local().findAll(); + assertTrue(list.size() == intialSize + 2); + } + + + // Helper + private static String generateUUID() { + return UUID.randomUUID().toString().replace("-", ""); + } + +} diff --git a/src/test/java/org/lightcouch/tests/MangoTest.java b/src/test/java/org/lightcouch/tests/MangoTest.java index d6742c0..7ca4e0f 100644 --- a/src/test/java/org/lightcouch/tests/MangoTest.java +++ b/src/test/java/org/lightcouch/tests/MangoTest.java @@ -21,27 +21,16 @@ import java.util.List; -import org.junit.AfterClass; -import org.junit.BeforeClass; +import org.junit.Assume; import org.junit.Test; -import org.lightcouch.CouchDbClient; - -public class MangoTest { - - private static CouchDbClient dbClient; - - @BeforeClass - public static void setUpClass() { - dbClient = new CouchDbClient(); - } - - @AfterClass - public static void tearDownClass() { - dbClient.shutdown(); - } +public class MangoTest extends CouchDbTestBase { + @Test public void findDocs() { + + Assume.assumeTrue(isCouchDB2()); + dbClient.save(new Foo()); String jsonQuery = "{ \"selector\": { \"_id\": { \"$gt\": null } }, \"limit\":2 }"; diff --git a/src/test/java/org/lightcouch/tests/PurgeTest.java b/src/test/java/org/lightcouch/tests/PurgeTest.java new file mode 100644 index 0000000..21c0213 --- /dev/null +++ b/src/test/java/org/lightcouch/tests/PurgeTest.java @@ -0,0 +1,87 @@ +package org.lightcouch.tests; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assume; +import org.junit.Test; +import org.lightcouch.CouchDbException; +import org.lightcouch.PurgeResponse; +import org.lightcouch.Response; + +import junit.framework.Assert; + +public class PurgeTest extends CouchDbTestBase { + + @Test + public void pre23NotsupportedTest() { + Assume.assumeTrue(isCouchDB2() && !isCouchDB23()); + Map> toPurge = new HashMap>(); + toPurge.put("222", Arrays.asList("1-967a00dff5e02add41819138abb3284d")); + try { + dbClient.purge(toPurge); + Assert.fail("Exception is expected"); + } catch (CouchDbException e) { + Assert.assertTrue(e.getMessage().startsWith("Not Implemented")); + } + } + + @Test + public void nullMapNotSupportedTest() { + try { + dbClient.purge(null); + Assert.fail("Exception is expected"); + } catch (IllegalArgumentException e) { + Assert.assertTrue(e.getMessage().equals("to purge map may not be null.")); + } + } + + @Test + public void emptyMapNotSupportedTest() { + Map> toPurge = new HashMap>(); + try { + dbClient.purge(toPurge); + Assert.fail("Exception is expected"); + } catch (IllegalArgumentException e) { + Assert.assertTrue(e.getMessage().equals("to purge map may not be empty.")); + } + } + + @Test + public void purgeTest() { + Assume.assumeTrue(isCouchDB23()); + + Response creation = dbClient.save(new Foo("111")); + String rev = creation.getRev(); + + Assert.assertTrue(dbClient.contains("111")); + + Map> toPurge = new HashMap>(); + + toPurge.put("111", Arrays.asList(rev)); + PurgeResponse response = dbClient.purge(toPurge); + Assert.assertTrue(response.getPurged().containsKey("111")); + List revs = response.getPurged().get("111"); + Assert.assertTrue(revs.contains(rev)); + + Assert.assertTrue(!dbClient.contains("111")); + } + + @Test + public void purgeNonExistingDocTest() { + Assume.assumeTrue(isCouchDB23()); + + Assert.assertTrue(!dbClient.contains("222")); + + Map> toPurge = new HashMap>(); + + toPurge.put("222", Arrays.asList("1-967a00dff5e02add41819138abb3284d")); + PurgeResponse response = dbClient.purge(toPurge); + Assert.assertTrue(response.getPurged().containsKey("222")); + Assert.assertTrue(response.getPurged().get("222").isEmpty()); + + } + +} diff --git a/src/test/java/org/lightcouch/tests/ReplicationTest.java b/src/test/java/org/lightcouch/tests/ReplicationTest.java index e77b4e2..7a2532d 100644 --- a/src/test/java/org/lightcouch/tests/ReplicationTest.java +++ b/src/test/java/org/lightcouch/tests/ReplicationTest.java @@ -20,49 +20,71 @@ import static org.hamcrest.CoreMatchers.not; import static org.junit.Assert.assertThat; +import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.junit.AfterClass; +import org.junit.Assume; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; import org.lightcouch.CouchDbClient; +import org.lightcouch.DocumentConflictException; import org.lightcouch.ReplicationResult; +import org.lightcouch.ReplicationResult.ReplicationHistory; import org.lightcouch.ReplicatorDocument; import org.lightcouch.Response; +import org.lightcouch.URIBuilder; import org.lightcouch.ViewResult; -import org.lightcouch.ReplicationResult.ReplicationHistory; -@Ignore public class ReplicationTest { private static CouchDbClient dbClient; private static CouchDbClient dbClient2; + private static URI dbClientUri; + private static URI dbClient2Uri; + + private static CouchDbConfigTest dbClientConfig; + private static CouchDbConfigTest dbClient2Config; + + private static Log LOG = LogFactory.getLog(ReplicationTest.class); @BeforeClass public static void setUpClass() { dbClient = new CouchDbClient(); dbClient2 = new CouchDbClient("couchdb-2.properties"); + dbClientConfig = new CouchDbConfigTest(); + dbClient2Config = new CouchDbConfigTest("couchdb-2.properties"); + + dbClientUri = buildUri(dbClient.getDBUri()).user(dbClientConfig.getProperties().getUsername()).password(dbClientConfig.getProperties().getPassword()).buildWithCredentials(); + dbClient2Uri = buildUri(dbClient2.getDBUri()).user(dbClient2Config.getProperties().getUsername()).password(dbClient2Config.getProperties().getPassword()).buildWithCredentials(); + dbClient.syncDesignDocsWithDb(); dbClient2.syncDesignDocsWithDb(); } @AfterClass public static void tearDownClass() { + + dbClient.context().deleteDB(dbClientConfig.getProperties().getDbName() , "delete database"); + dbClient.context().deleteDB(dbClient2Config.getProperties().getDbName() , "delete database"); + dbClient.shutdown(); dbClient2.shutdown(); } @Test public void replication() { + dbClient.getDBUri(); ReplicationResult result = dbClient.replication() .createTarget(true) - .source(dbClient.getDBUri().toString()) - .target(dbClient2.getDBUri().toString()) + .source(dbClientUri.toString()) + .target(dbClient2Uri.toString()) .trigger(); List histories = result.getHistories(); @@ -76,24 +98,24 @@ public void replication_filteredWithQueryParams() { dbClient.replication() .createTarget(true) - .source(dbClient.getDBUri().toString()) - .target(dbClient2.getDBUri().toString()) + .source(dbClientUri.toString()) + .target(dbClient2Uri.toString()) .filter("example/example_filter") .queryParams(queryParams) .trigger(); } @Test - public void replicatorDB() { + public void replicatorDB() throws InterruptedException { + String version = dbClient.context().serverVersion(); - if (version.startsWith("0") || version.startsWith("1.0")) { - return; - } - + Assume.assumeTrue(!(version.startsWith("0") || version.startsWith("1.0"))); + // trigger a replication Response response = dbClient.replicator() - .source(dbClient.getDBUri().toString()) - .target(dbClient2.getDBUri().toString()).continuous(true) + .source(dbClientUri.toString()) + .target(dbClient2Uri.toString()) + .continuous(true) .createTarget(true) .save(); @@ -103,15 +125,38 @@ public void replicatorDB() { assertThat(replicatorDocs.size(), is(not(0))); // find replicator doc + String id = response.getId(); ReplicatorDocument replicatorDoc = dbClient.replicator() - .replicatorDocId(response.getId()) + .replicatorDocId(id) .find(); - - // cancel a replication - dbClient.replicator() - .replicatorDocId(replicatorDoc.getId()) - .replicatorDocRev(replicatorDoc.getRevision()) - .remove(); + + LOG.info("Replication state "+replicatorDoc.getReplicationState()); + + // Wait until cancelled + boolean cancelled=false; + while (!cancelled) { + cancelled = tryCancelReplication(id); + Thread.sleep(100); + } + + } + + private boolean tryCancelReplication(String id) { + + ReplicatorDocument replicatorDoc = dbClient.replicator() + .replicatorDocId(id) + .find(); + LOG.info("Replication state "+replicatorDoc.getReplicationState()); + try { + // cancel a replication + dbClient.replicator() + .replicatorDocId(replicatorDoc.getId()) + .replicatorDocRev(replicatorDoc.getRevision()) + .remove(); + } catch (DocumentConflictException e) { + return false; + } + return true; } @Test @@ -124,8 +169,8 @@ public void replication_conflict() { dbClient.save(foodb1); - dbClient.replication().source(dbClient.getDBUri().toString()) - .target(dbClient2.getDBUri().toString()).trigger(); + dbClient.replication().source(dbClientUri.toString()) + .target(dbClient2Uri.toString()).trigger(); foodb2 = dbClient2.find(Foo.class, docId); foodb2.setTitle("titleY"); @@ -135,8 +180,8 @@ public void replication_conflict() { foodb1.setTitle("titleZ"); dbClient.update(foodb1); - dbClient.replication().source(dbClient.getDBUri().toString()) - .target(dbClient2.getDBUri().toString()).trigger(); + dbClient.replication().source(dbClientUri.toString()) + .target(dbClient2Uri.toString()).trigger(); ViewResult conflicts = dbClient2.view("conflicts/conflict") .includeDocs(true).queryView(String[].class, String.class, Foo.class); @@ -147,4 +192,10 @@ public void replication_conflict() { private static String generateUUID() { return UUID.randomUUID().toString().replace("-", ""); } + + public static URIBuilder buildUri(URI uri) { + URIBuilder builder = URIBuilder.buildUri().scheme(uri.getScheme()). + host(uri.getHost()).port(uri.getPort()).path(uri.getPath()); + return builder; + } } diff --git a/src/test/java/org/lightcouch/tests/UpdateHandlerTest.java b/src/test/java/org/lightcouch/tests/UpdateHandlerTest.java index bbc2be2..c89545e 100644 --- a/src/test/java/org/lightcouch/tests/UpdateHandlerTest.java +++ b/src/test/java/org/lightcouch/tests/UpdateHandlerTest.java @@ -19,28 +19,20 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; -import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.lightcouch.CouchDbClient; import org.lightcouch.Params; import org.lightcouch.Response; -public class UpdateHandlerTest { - - private static CouchDbClient dbClient; - - @BeforeClass - public static void setUpClass() { - dbClient = new CouchDbClient(); - dbClient.syncDesignDocsWithDb(); - } - - @AfterClass - public static void tearDownClass() { - dbClient.shutdown(); - } +public class UpdateHandlerTest extends CouchDbTestBase { + @BeforeClass + public static void setUpClass() { + dbClient = new CouchDbClient(); + dbClient.syncDesignDocsWithDb(); + } + @Test public void updateHandler_queryParams() { final String oldValue = "foo"; @@ -51,6 +43,7 @@ public void updateHandler_queryParams() { Params params = new Params() .addParam("field", "title") .addParam("value", newValue); + String output = dbClient.invokeUpdateHandler("example/example_update", response.getId(), params); // retrieve from db to verify diff --git a/src/test/java/org/lightcouch/tests/ViewsTest.java b/src/test/java/org/lightcouch/tests/ViewsTest.java index c9d8e86..94f6b62 100644 --- a/src/test/java/org/lightcouch/tests/ViewsTest.java +++ b/src/test/java/org/lightcouch/tests/ViewsTest.java @@ -27,7 +27,6 @@ import java.util.UUID; import java.util.Vector; -import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.lightcouch.CouchDbClient; @@ -39,23 +38,16 @@ import com.google.gson.JsonObject; -public class ViewsTest { - - private static CouchDbClient dbClient; +public class ViewsTest extends CouchDbTestBase { @BeforeClass public static void setUpClass() { dbClient = new CouchDbClient(); - dbClient.syncDesignDocsWithDb(); init(); } - @AfterClass - public static void tearDownClass() { - dbClient.shutdown(); - } @Test public void queryView() { diff --git a/src/test/resources/design-docs/test-design-doc/filters/example_filter.js b/src/test/resources/design-docs/test-design-doc/filters/example_filter.js new file mode 100644 index 0000000..1efcfd3 --- /dev/null +++ b/src/test/resources/design-docs/test-design-doc/filters/example_filter.js @@ -0,0 +1,7 @@ +function(doc, req) { + if (doc.title == req.query.somekey1) { + return true; + } else { + return false; + } +} \ No newline at end of file