From 44e6cc6477b4fdf2741a079f900fa28bdcb7d07b Mon Sep 17 00:00:00 2001 From: Matthew Buckett Date: Sat, 30 Jan 2016 14:23:16 +0000 Subject: [PATCH 01/70] Initial docker/travis support --- .travis.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3eabd33 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +sudo: required + +language: java + +services: + - docker + +before_install: + - docker pull frodenas/couchdb:1.6 + - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USERNAME=couchdb -e COUCHDB_PASSWORD=couchdb frodenas/couchdb:1.6 + - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb.properties From bd6fb4499b1a9c347ab52bb9dd42606ba13e52de Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Mon, 18 Dec 2017 15:27:31 +0100 Subject: [PATCH 02/70] Add _selector support to _changes operation --- src/main/java/org/lightcouch/Changes.java | 45 +++++++++++++++--- .../java/org/lightcouch/CouchDbClient.java | 1 - .../org/lightcouch/CouchDbClientBase.java | 28 +++++++++++ .../tests/ChangeNotificationsTest.java | 47 +++++++++++++++++++ 4 files changed, 113 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/lightcouch/Changes.java b/src/main/java/org/lightcouch/Changes.java index a544d46..b844fd7 100644 --- a/src/main/java/org/lightcouch/Changes.java +++ b/src/main/java/org/lightcouch/Changes.java @@ -23,6 +23,8 @@ 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; @@ -56,6 +58,14 @@ * 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 @@ -64,7 +74,7 @@ public class Changes { private BufferedReader reader; - private HttpGet httpGet; + private HttpUriRequest httpRequest; private Row nextRow; private boolean stop; @@ -72,6 +82,8 @@ public class Changes { private Gson gson; private URIBuilder uriBuilder; + private String selector; + Changes(CouchDbClientBase dbc) { this.dbc = dbc; this.gson = dbc.getGson(); @@ -85,10 +97,19 @@ public class 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)); + 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; } @@ -121,7 +142,11 @@ public void stop() { */ public ChangesResult getChanges() { final URI uri = uriBuilder.query("feed", "normal").build(); - return dbc.get(uri, ChangesResult.class); + if (selector == null) { + return dbc.get(uri, ChangesResult.class); + } else { + return dbc.post(uri, selector, ChangesResult.class); + } } // Query Params @@ -151,6 +176,12 @@ public Changes filter(String filter) { return this; } + public Changes selector(String json) { + uriBuilder.query("filter", "_selector"); + this.selector = json; + return this; + } + public Changes includeDocs(boolean includeDocs) { uriBuilder.query("include_docs", includeDocs); return this; @@ -206,7 +237,7 @@ private void setNextRow(Row nextRow) { } private void terminate() { - httpGet.abort(); + 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..46c9356 100644 --- a/src/main/java/org/lightcouch/CouchDbClientBase.java +++ b/src/main/java/org/lightcouch/CouchDbClientBase.java @@ -621,6 +621,34 @@ HttpResponse post(URI uri, String 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} diff --git a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java index 66e1704..abe8366 100644 --- a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java +++ b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java @@ -73,6 +73,28 @@ public void changes_normalFeed() { assertThat(rows.size(), is(1)); } + @Test + public void changes_normalFeed_selector() { + dbClient.save(new Foo()); + + ChangesResult changes = dbClient.changes().includeDocs(true).limit(1) + .selector("{\"selector\":{\"_id\": {\"$gt\": null}}}").getChanges(); + + List rows = changes.getResults(); + + for (Row row : rows) { + List revs = row.getChanges(); + String docId = row.getId(); + JsonObject doc = row.getDoc(); + + assertNotNull(revs); + assertNotNull(docId); + assertNotNull(doc); + } + + assertThat(rows.size(), is(1)); + } + @Test public void changes_continuousFeed() { dbClient.save(new Foo()); @@ -99,4 +121,29 @@ public void changes_continuousFeed() { changes.stop(); } } + + @Test + public void changes_continuousFeed_selector() { + dbClient.save(new Foo()); + + CouchDbInfo dbInfo = dbClient.context().info(); + String since = dbInfo.getUpdateSeq(); + + Changes changes = dbClient.changes().includeDocs(true).since(since).heartBeat(1000) + .selector("{\"selector\":{\"_id\": {\"$gt\": null}}}").continuousChanges(); + + Response response = dbClient.save(new Foo()); + + while (changes.hasNext()) { + ChangesResult.Row feed = changes.next(); + final JsonObject feedObject = feed.getDoc(); + final String docId = feed.getId(); + System.out.println("next()=" + docId); + + assertEquals(response.getId(), docId); + assertNotNull(feedObject); + + changes.stop(); + } + } } From 7cdebfe0219a92b5c9f13908018522f193bb248d Mon Sep 17 00:00:00 2001 From: Joseba Urkiri Date: Mon, 19 Feb 2018 12:21:32 +0100 Subject: [PATCH 03/70] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0bc8aa8..73ebca9 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,4 @@ CouchDB Java API 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. From 661176adca407a216e2c76683bb4f8c3e54929be Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Mon, 19 Feb 2018 14:14:03 +0100 Subject: [PATCH 04/70] Restrict tests by CouchDB version --- .../lightcouch/tests/ChangeNotificationsTest.java | 13 ++++++++++++- src/test/java/org/lightcouch/tests/MangoTest.java | 9 +++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java index abe8366..f7c8a87 100644 --- a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java +++ b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java @@ -24,6 +24,7 @@ import java.util.List; import org.junit.AfterClass; +import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Test; import org.lightcouch.Changes; @@ -49,6 +50,11 @@ public static void tearDownClass() { dbClient.shutdown(); } + private boolean isCouchDB2() { + String version = dbClient.context().serverVersion(); + return version.startsWith("2"); + } + @Test public void changes_normalFeed() { dbClient.save(new Foo()); @@ -75,8 +81,10 @@ public void changes_normalFeed() { @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(); @@ -124,6 +132,9 @@ public void changes_continuousFeed() { @Test public void changes_continuousFeed_selector() { + + Assume.assumeTrue(isCouchDB2()); + dbClient.save(new Foo()); CouchDbInfo dbInfo = dbClient.context().info(); diff --git a/src/test/java/org/lightcouch/tests/MangoTest.java b/src/test/java/org/lightcouch/tests/MangoTest.java index d6742c0..6c9f9f1 100644 --- a/src/test/java/org/lightcouch/tests/MangoTest.java +++ b/src/test/java/org/lightcouch/tests/MangoTest.java @@ -22,6 +22,7 @@ import java.util.List; import org.junit.AfterClass; +import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Test; import org.lightcouch.CouchDbClient; @@ -39,9 +40,17 @@ public static void setUpClass() { public static void tearDownClass() { dbClient.shutdown(); } + + private boolean isCouchDB2() { + String version = dbClient.context().serverVersion(); + return version.startsWith("2"); + } @Test public void findDocs() { + + Assume.assumeTrue(isCouchDB2()); + dbClient.save(new Foo()); String jsonQuery = "{ \"selector\": { \"_id\": { \"$gt\": null } }, \"limit\":2 }"; From 945327f34f4954554e44c964c8752e973ca5a079 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Mon, 19 Feb 2018 14:19:48 +0100 Subject: [PATCH 05/70] Reformat --- src/test/java/org/lightcouch/tests/MangoTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/lightcouch/tests/MangoTest.java b/src/test/java/org/lightcouch/tests/MangoTest.java index 6c9f9f1..ec40eff 100644 --- a/src/test/java/org/lightcouch/tests/MangoTest.java +++ b/src/test/java/org/lightcouch/tests/MangoTest.java @@ -41,7 +41,7 @@ public static void tearDownClass() { dbClient.shutdown(); } - private boolean isCouchDB2() { + private boolean isCouchDB2() { String version = dbClient.context().serverVersion(); return version.startsWith("2"); } From 75c3e0df45142037908cdea0cdaa6d5d57479606 Mon Sep 17 00:00:00 2001 From: Joseba Urkiri Date: Mon, 19 Feb 2018 15:03:54 +0100 Subject: [PATCH 06/70] groupId changed and Indaba info added --- pom.xml | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/pom.xml b/pom.xml index b688454..4415595 100644 --- a/pom.xml +++ b/pom.xml @@ -1,14 +1,13 @@ - + 4.0.0 - org.lightcouch + es.indaba lightcouch 0.2.1-SNAPSHOT jar LightCouch CouchDB Java API - 2011 - http://www.lightcouch.org org.sonatype.oss oss-parent @@ -22,17 +21,25 @@ - scm:git:git://github.com/lightcouch/LightCouch.git - scm:git:git@github.com:lightcouch/LightCouch.git - https://lightcouch@github.com/lightcouch/LightCouch.git + scm:git:https://github.com/IndabaConsultores/LightCouch.git + https://github.com/IndabaConsultores/LightCouch + - ahmedyha - Ahmed Yehia - ahmed.yehia.m@gmail.com + 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 @@ -63,9 +70,9 @@ - org.apache.maven.plugins - maven-compiler-plugin - 3.1 + org.apache.maven.plugins + maven-compiler-plugin + 3.1 1.5 1.5 From 21c3273ad13c21e18316fbe9d3f5bd37d33bf419 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Mon, 19 Feb 2018 15:59:30 +0100 Subject: [PATCH 07/70] Add support for CouchDB 1.x/2.x testing and sonar Configure test scenario for CouchDB 1.x and CouchDB 2.x --- .travis.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3eabd33..905c832 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,17 @@ language: java services: - docker -before_install: - - docker pull frodenas/couchdb:1.6 - - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USERNAME=couchdb -e COUCHDB_PASSWORD=couchdb frodenas/couchdb:1.6 - - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb.properties +jobs: + include: + - stage: Build and Test on CouchDB 1.x + language: java + install: + - docker pull couchdb:1.7.1 + - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:1.7.1 + - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb.properties + - stage: Build and Test on CouchDB 2.x + language: java + install: + - docker pull couchdb:2.1.1 + - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.1.1 + - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb.properties From feb3ae0aad818f44d34e3960047b57b6a51d02e3 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Mon, 19 Feb 2018 17:48:45 +0100 Subject: [PATCH 08/70] Add sonarcloud support Add sonarcloud support --- .travis.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 905c832..3074008 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,12 @@ sudo: required language: java +addons: + sonarcloud: + organization: "indaba" + branches: + - master + - travis-work services: - docker @@ -8,14 +14,14 @@ services: jobs: include: - stage: Build and Test on CouchDB 1.x - language: java install: - docker pull couchdb:1.7.1 - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:1.7.1 - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb.properties - stage: Build and Test on CouchDB 2.x - language: java install: - docker pull couchdb:2.1.1 - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.1.1 - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb.properties + script: + - mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent test sonar:sonar From 5c643c4eeb72c40825f8382d0b4f6097a5659a92 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Mon, 19 Feb 2018 22:25:06 +0100 Subject: [PATCH 09/70] Enable replication test --- .travis.yml | 2 +- src/test/java/org/lightcouch/tests/ReplicationTest.java | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3074008..d3696a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ addons: organization: "indaba" branches: - master - - travis-work + - test-replicator services: - docker diff --git a/src/test/java/org/lightcouch/tests/ReplicationTest.java b/src/test/java/org/lightcouch/tests/ReplicationTest.java index e77b4e2..5fc8325 100644 --- a/src/test/java/org/lightcouch/tests/ReplicationTest.java +++ b/src/test/java/org/lightcouch/tests/ReplicationTest.java @@ -27,16 +27,14 @@ import org.junit.AfterClass; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; import org.lightcouch.CouchDbClient; import org.lightcouch.ReplicationResult; +import org.lightcouch.ReplicationResult.ReplicationHistory; import org.lightcouch.ReplicatorDocument; import org.lightcouch.Response; import org.lightcouch.ViewResult; -import org.lightcouch.ReplicationResult.ReplicationHistory; -@Ignore public class ReplicationTest { private static CouchDbClient dbClient; From 69f7f14e8bdff9d2660a21b9dca4a363954f7519 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Mon, 19 Feb 2018 22:58:10 +0100 Subject: [PATCH 10/70] Configure target couchdb for replication tests --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index d3696a8..1637231 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,10 +18,12 @@ jobs: - docker pull couchdb:1.7.1 - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:1.7.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 - stage: Build and Test on CouchDB 2.x install: - docker pull couchdb:2.1.1 - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.1.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 script: - mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent test sonar:sonar From 5073e6dbf37acafb4610ba6283cc4d1bd0fbc56e Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Mon, 19 Feb 2018 23:11:26 +0100 Subject: [PATCH 11/70] Configure target couchdb for replication tests --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1637231..d6f4f7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,4 +26,4 @@ jobs: - 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 script: - - mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent test sonar:sonar + - mvn org.jacoco:jacoco-maven-plugin:prepare-agent test sonar:sonar From ec3be1a94d2ec8639d44f78771bfb8ee7ec7dfe9 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Feb 2018 00:15:14 +0100 Subject: [PATCH 12/70] fix db uri for replicator --- .../org/lightcouch/CouchDbClientBase.java | 30 ++++++++++++++++-- src/main/java/org/lightcouch/URIBuilder.java | 31 +++++++++++++++++-- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/lightcouch/CouchDbClientBase.java b/src/main/java/org/lightcouch/CouchDbClientBase.java index 46c9356..91bb8bc 100644 --- a/src/main/java/org/lightcouch/CouchDbClientBase.java +++ b/src/main/java/org/lightcouch/CouchDbClientBase.java @@ -79,11 +79,13 @@ public abstract class CouchDbClientBase { private URI baseURI; private URI dbURI; + private URI dbURIWithCredentials; private Gson gson; private CouchDbContext context; private CouchDbDesign design; final HttpClient httpClient; final HttpHost host; + CouchDbClientBase() { this(new CouchDbConfig()); @@ -91,13 +93,28 @@ public abstract class CouchDbClientBase { 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(); + if (props.getUsername() != null && props.getPassword()!=null) { + this.dbURIWithCredentials = buildUri().scheme(props.getProtocol()) + .user(props.getUsername()) + .password(props.getPassword()) + .host(props.getHost()) + .port(props.getPort()) + .path("/") + .path(path) + .path(props.getDbName()) + .path("/").buildWithCredentials(); + } else { + this.dbURIWithCredentials = dbURI; + } + + this.httpClient = createHttpClient(props); + this.gson = initGson(new GsonBuilder()); + this.host = new HttpHost(props.getHost(), props.getPort(), props.getProtocol()); this.context = new CouchDbContext(this, props); this.design = new CouchDbDesign(this); @@ -513,6 +530,13 @@ public URI getBaseUri() { public URI getDBUri() { return dbURI; } + + /** + * @return The database URI with credentials info. + */ + public URI getDBUriWithCredentials() { + return dbURIWithCredentials; + } /** * @return The Gson instance. diff --git a/src/main/java/org/lightcouch/URIBuilder.java b/src/main/java/org/lightcouch/URIBuilder.java index 8bc6d81..6a6a8f1 100644 --- a/src/main/java/org/lightcouch/URIBuilder.java +++ b/src/main/java/org/lightcouch/URIBuilder.java @@ -35,7 +35,9 @@ class URIBuilder { 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,7 +96,25 @@ 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++) { @@ -103,7 +123,12 @@ public URI build() { } 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:%s%s%s", scheme, user,password, host, port, path, q ); + } else { + uri = String.format("%s://%s:%s%s%s", scheme, host, port, path, q ); + } try { return new URI(uri); @@ -112,5 +137,5 @@ public URI build() { } } - + } From 19a805fe379460a6d18d173682176db2c7316de8 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Feb 2018 00:20:22 +0100 Subject: [PATCH 13/70] fix db uri for replicator --- .../org/lightcouch/tests/ReplicationTest.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/test/java/org/lightcouch/tests/ReplicationTest.java b/src/test/java/org/lightcouch/tests/ReplicationTest.java index 5fc8325..375dc98 100644 --- a/src/test/java/org/lightcouch/tests/ReplicationTest.java +++ b/src/test/java/org/lightcouch/tests/ReplicationTest.java @@ -57,10 +57,11 @@ public static void tearDownClass() { @Test public void replication() { + dbClient.getDBUri(); ReplicationResult result = dbClient.replication() .createTarget(true) - .source(dbClient.getDBUri().toString()) - .target(dbClient2.getDBUri().toString()) + .source(dbClient.getDBUriWithCredentials().toString()) + .target(dbClient2.getDBUriWithCredentials().toString()) .trigger(); List histories = result.getHistories(); @@ -74,8 +75,8 @@ public void replication_filteredWithQueryParams() { dbClient.replication() .createTarget(true) - .source(dbClient.getDBUri().toString()) - .target(dbClient2.getDBUri().toString()) + .source(dbClient.getDBUriWithCredentials().toString()) + .target(dbClient2.getDBUriWithCredentials().toString()) .filter("example/example_filter") .queryParams(queryParams) .trigger(); @@ -90,8 +91,8 @@ public void replicatorDB() { // trigger a replication Response response = dbClient.replicator() - .source(dbClient.getDBUri().toString()) - .target(dbClient2.getDBUri().toString()).continuous(true) + .source(dbClient.getDBUriWithCredentials().toString()) + .target(dbClient2.getDBUriWithCredentials().toString()).continuous(true) .createTarget(true) .save(); @@ -122,8 +123,8 @@ public void replication_conflict() { dbClient.save(foodb1); - dbClient.replication().source(dbClient.getDBUri().toString()) - .target(dbClient2.getDBUri().toString()).trigger(); + dbClient.replication().source(dbClient.getDBUriWithCredentials().toString()) + .target(dbClient2.getDBUriWithCredentials().toString()).trigger(); foodb2 = dbClient2.find(Foo.class, docId); foodb2.setTitle("titleY"); @@ -133,8 +134,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(dbClient.getDBUriWithCredentials().toString()) + .target(dbClient2.getDBUriWithCredentials().toString()).trigger(); ViewResult conflicts = dbClient2.view("conflicts/conflict") .includeDocs(true).queryView(String[].class, String.class, Foo.class); From fd81af945b2c40b7d3731d0aabe2c5619700b7b1 Mon Sep 17 00:00:00 2001 From: Joseba Urkiri Date: Tue, 20 Feb 2018 07:32:47 +0100 Subject: [PATCH 14/70] Added shard number on database creation --- src/main/java/org/lightcouch/CouchDbContext.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/lightcouch/CouchDbContext.java b/src/main/java/org/lightcouch/CouchDbContext.java index f0b4949..3b508f0 100644 --- a/src/main/java/org/lightcouch/CouchDbContext.java +++ b/src/main/java/org/lightcouch/CouchDbContext.java @@ -75,11 +75,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); From c2a242984f5d46aaeb6a2a926eddf1c8834921e2 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Feb 2018 08:53:14 +0100 Subject: [PATCH 15/70] Configure single node CouchDB 2.x in travis --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index d6f4f7a..2d8c86e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,5 +25,8 @@ jobs: - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.1.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 + - "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 From 3a0736909e3dfecd64d58babb3f06a74aa75d75d Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Feb 2018 09:09:13 +0100 Subject: [PATCH 16/70] Configure single node CouchDB 2.x in travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 2d8c86e..15bd344 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,7 @@ jobs: - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.1.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 10000 - "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" From 46f808d6ac50b4b4e73672cbbc1ae3fe04ade3a6 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Feb 2018 09:14:49 +0100 Subject: [PATCH 17/70] Configure single node CouchDB 2.x in travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 15bd344..3cd4068 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,7 +25,7 @@ jobs: - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.1.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 10000 + - sleep 5 - "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" From 2f5799af949f8ab516d52fc26453398209a032a5 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Feb 2018 09:22:58 +0100 Subject: [PATCH 18/70] Configure single node CouchDB 2.x in travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3cd4068..56066e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ addons: organization: "indaba" branches: - master - - test-replicator + - tests-replicator services: - docker From 7f1dfec641815d6ae744766309109763c0194329 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Feb 2018 09:47:22 +0100 Subject: [PATCH 19/70] Configure single node CouchDB 2.x in travis --- .travis.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 56066e7..c695dc6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,14 +13,14 @@ services: jobs: include: - - stage: Build and Test on CouchDB 1.x - install: + # - stage: Build and Test on CouchDB 1.x + - install: - docker pull couchdb:1.7.1 - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:1.7.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 - - stage: Build and Test on CouchDB 2.x - install: + #- stage: Build and Test on CouchDB 2.x + - install: - docker pull couchdb:2.1.1 - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.1.1 - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb.properties @@ -29,5 +29,5 @@ jobs: - "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: + script: - mvn org.jacoco:jacoco-maven-plugin:prepare-agent test sonar:sonar From ecb4546b7ba363b31938c05f242db704cf23e2c2 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Feb 2018 10:16:09 +0100 Subject: [PATCH 20/70] Adjust replication test --- .travis.yml | 11 ++- .../org/lightcouch/CouchDbClientBase.java | 30 +------ src/main/java/org/lightcouch/URIBuilder.java | 2 +- .../lightcouch/tests/CouchDbConfigTest.java | 82 +++++++++++++++++++ .../org/lightcouch/tests/ReplicationTest.java | 36 +++++--- 5 files changed, 117 insertions(+), 44 deletions(-) create mode 100644 src/test/java/org/lightcouch/tests/CouchDbConfigTest.java diff --git a/.travis.yml b/.travis.yml index c695dc6..b3dc7b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,21 +6,20 @@ addons: organization: "indaba" branches: - master - - tests-replicator services: - docker jobs: include: - # - stage: Build and Test on CouchDB 1.x - - install: + - stage: Build and Test on CouchDB 1.x + install: - docker pull couchdb:1.7.1 - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:1.7.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 - #- stage: Build and Test on CouchDB 2.x - - install: + - stage: Build and Test on CouchDB 2.x + install: - docker pull couchdb:2.1.1 - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.1.1 - sed 's/^couchdb.username.*/couchdb.username=couchdb/;s/^couchdb.password.*/couchdb.password=couchdb/' -i src/test/resources/couchdb.properties @@ -29,5 +28,5 @@ jobs: - "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: + script: - mvn org.jacoco:jacoco-maven-plugin:prepare-agent test sonar:sonar diff --git a/src/main/java/org/lightcouch/CouchDbClientBase.java b/src/main/java/org/lightcouch/CouchDbClientBase.java index 91bb8bc..46c9356 100644 --- a/src/main/java/org/lightcouch/CouchDbClientBase.java +++ b/src/main/java/org/lightcouch/CouchDbClientBase.java @@ -79,13 +79,11 @@ public abstract class CouchDbClientBase { private URI baseURI; private URI dbURI; - private URI dbURIWithCredentials; private Gson gson; private CouchDbContext context; private CouchDbDesign design; final HttpClient httpClient; final HttpHost host; - CouchDbClientBase() { this(new CouchDbConfig()); @@ -93,28 +91,13 @@ public abstract class CouchDbClientBase { 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(); - if (props.getUsername() != null && props.getPassword()!=null) { - this.dbURIWithCredentials = buildUri().scheme(props.getProtocol()) - .user(props.getUsername()) - .password(props.getPassword()) - .host(props.getHost()) - .port(props.getPort()) - .path("/") - .path(path) - .path(props.getDbName()) - .path("/").buildWithCredentials(); - } else { - this.dbURIWithCredentials = dbURI; - } - - this.httpClient = createHttpClient(props); - this.gson = initGson(new GsonBuilder()); - this.host = new HttpHost(props.getHost(), props.getPort(), props.getProtocol()); this.context = new CouchDbContext(this, props); this.design = new CouchDbDesign(this); @@ -530,13 +513,6 @@ public URI getBaseUri() { public URI getDBUri() { return dbURI; } - - /** - * @return The database URI with credentials info. - */ - public URI getDBUriWithCredentials() { - return dbURIWithCredentials; - } /** * @return The Gson instance. diff --git a/src/main/java/org/lightcouch/URIBuilder.java b/src/main/java/org/lightcouch/URIBuilder.java index 6a6a8f1..b6aa32b 100644 --- a/src/main/java/org/lightcouch/URIBuilder.java +++ b/src/main/java/org/lightcouch/URIBuilder.java @@ -29,7 +29,7 @@ * @author Ahmed Yehia * */ -class URIBuilder { +public class URIBuilder { private String scheme; private String host; private int port; 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/ReplicationTest.java b/src/test/java/org/lightcouch/tests/ReplicationTest.java index 375dc98..2f8a64b 100644 --- a/src/test/java/org/lightcouch/tests/ReplicationTest.java +++ b/src/test/java/org/lightcouch/tests/ReplicationTest.java @@ -20,11 +20,13 @@ 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.lightcouch.URIBuilder; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -39,12 +41,20 @@ public class ReplicationTest { private static CouchDbClient dbClient; private static CouchDbClient dbClient2; + private static URI dbClientUri; + private static URI dbClient2Uri; @BeforeClass public static void setUpClass() { dbClient = new CouchDbClient(); dbClient2 = new CouchDbClient("couchdb-2.properties"); + CouchDbConfigTest dbClientConfig = new CouchDbConfigTest(); + 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(); } @@ -60,8 +70,8 @@ public void replication() { dbClient.getDBUri(); ReplicationResult result = dbClient.replication() .createTarget(true) - .source(dbClient.getDBUriWithCredentials().toString()) - .target(dbClient2.getDBUriWithCredentials().toString()) + .source(dbClientUri.toString()) + .target(dbClient2Uri.toString()) .trigger(); List histories = result.getHistories(); @@ -75,8 +85,8 @@ public void replication_filteredWithQueryParams() { dbClient.replication() .createTarget(true) - .source(dbClient.getDBUriWithCredentials().toString()) - .target(dbClient2.getDBUriWithCredentials().toString()) + .source(dbClientUri.toString()) + .target(dbClient2Uri.toString()) .filter("example/example_filter") .queryParams(queryParams) .trigger(); @@ -91,8 +101,8 @@ public void replicatorDB() { // trigger a replication Response response = dbClient.replicator() - .source(dbClient.getDBUriWithCredentials().toString()) - .target(dbClient2.getDBUriWithCredentials().toString()).continuous(true) + .source(dbClientUri.toString()) + .target(dbClient2Uri.toString()).continuous(true) .createTarget(true) .save(); @@ -123,8 +133,8 @@ public void replication_conflict() { dbClient.save(foodb1); - dbClient.replication().source(dbClient.getDBUriWithCredentials().toString()) - .target(dbClient2.getDBUriWithCredentials().toString()).trigger(); + dbClient.replication().source(dbClientUri.toString()) + .target(dbClient2Uri.toString()).trigger(); foodb2 = dbClient2.find(Foo.class, docId); foodb2.setTitle("titleY"); @@ -134,8 +144,8 @@ public void replication_conflict() { foodb1.setTitle("titleZ"); dbClient.update(foodb1); - dbClient.replication().source(dbClient.getDBUriWithCredentials().toString()) - .target(dbClient2.getDBUriWithCredentials().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); @@ -146,4 +156,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; + } } From e3ad60e0654701d54a52900903a03f842fc4961f Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Tue, 20 Feb 2018 10:43:59 +0100 Subject: [PATCH 21/70] Enable replication tests (#4) * Enable replication tests * Configure target couchdb for replication tests * Configure single node CouchDB 2.x in travis --- .travis.yml | 9 +- src/main/java/org/lightcouch/URIBuilder.java | 33 +++++++- .../lightcouch/tests/CouchDbConfigTest.java | 82 +++++++++++++++++++ .../org/lightcouch/tests/ReplicationTest.java | 41 +++++++--- 4 files changed, 146 insertions(+), 19 deletions(-) create mode 100644 src/test/java/org/lightcouch/tests/CouchDbConfigTest.java diff --git a/.travis.yml b/.travis.yml index 3074008..b3dc7b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,6 @@ addons: organization: "indaba" branches: - master - - travis-work services: - docker @@ -18,10 +17,16 @@ jobs: - docker pull couchdb:1.7.1 - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:1.7.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 - stage: Build and Test on CouchDB 2.x install: - docker pull couchdb:2.1.1 - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.1.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 5 + - "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 clean org.jacoco:jacoco-maven-plugin:prepare-agent test sonar:sonar + - mvn org.jacoco:jacoco-maven-plugin:prepare-agent test sonar:sonar diff --git a/src/main/java/org/lightcouch/URIBuilder.java b/src/main/java/org/lightcouch/URIBuilder.java index 8bc6d81..b6aa32b 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,7 +96,25 @@ 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++) { @@ -103,7 +123,12 @@ public URI build() { } 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:%s%s%s", scheme, user,password, host, port, path, q ); + } else { + uri = String.format("%s://%s:%s%s%s", scheme, host, port, path, q ); + } try { return new URI(uri); @@ -112,5 +137,5 @@ public URI build() { } } - + } 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/ReplicationTest.java b/src/test/java/org/lightcouch/tests/ReplicationTest.java index e77b4e2..2f8a64b 100644 --- a/src/test/java/org/lightcouch/tests/ReplicationTest.java +++ b/src/test/java/org/lightcouch/tests/ReplicationTest.java @@ -20,33 +20,41 @@ 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.lightcouch.URIBuilder; import org.junit.AfterClass; import org.junit.BeforeClass; -import org.junit.Ignore; import org.junit.Test; import org.lightcouch.CouchDbClient; import org.lightcouch.ReplicationResult; +import org.lightcouch.ReplicationResult.ReplicationHistory; import org.lightcouch.ReplicatorDocument; import org.lightcouch.Response; 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; @BeforeClass public static void setUpClass() { dbClient = new CouchDbClient(); dbClient2 = new CouchDbClient("couchdb-2.properties"); + CouchDbConfigTest dbClientConfig = new CouchDbConfigTest(); + 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(); } @@ -59,10 +67,11 @@ public static void tearDownClass() { @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,8 +85,8 @@ 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(); @@ -92,8 +101,8 @@ public void replicatorDB() { // 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(); @@ -124,8 +133,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 +144,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 +156,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; + } } From 6bd2f9a1eda493689fd30a58c9f218ac2a31843d Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Tue, 20 Feb 2018 11:42:12 +0100 Subject: [PATCH 22/70] Add Travis CI and Sonarcloud badges --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 73ebca9..ff67a17 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ 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/badges/gate?key=org.lightcouch:lightcouch)](https://sonarcloud.io/dashboard?id=org.lightcouch:lightcouch) + + A Java _client_ for [CouchDB](http://couchdb.apache.org/) database. 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. From 69a4a0d4bfc5cfaeb4555405793d6458b4872289 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Feb 2018 12:38:25 +0100 Subject: [PATCH 23/70] Clean up databases after test, remove order dependency --- .../org/lightcouch/tests/AttachmentsTest.java | 17 +---------- .../lightcouch/tests/BulkDocumentTest.java | 19 ++---------- .../tests/ChangeNotificationsTest.java | 24 ++------------- .../org/lightcouch/tests/CouchDbTestBase.java | 29 +++++++++++++++++++ .../org/lightcouch/tests/DBServerTest.java | 16 +--------- .../lightcouch/tests/DesignDocumentsTest.java | 19 ++---------- .../lightcouch/tests/DocumentsCRUDTest.java | 17 +---------- .../java/org/lightcouch/tests/MangoTest.java | 19 +----------- .../org/lightcouch/tests/ReplicationTest.java | 22 +++++++++----- .../lightcouch/tests/UpdateHandlerTest.java | 23 +++++---------- .../java/org/lightcouch/tests/ViewsTest.java | 10 +------ 11 files changed, 63 insertions(+), 152 deletions(-) create mode 100644 src/test/java/org/lightcouch/tests/CouchDbTestBase.java 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 f7c8a87..5eda426 100644 --- a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java +++ b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java @@ -23,38 +23,18 @@ import java.util.List; -import org.junit.AfterClass; import org.junit.Assume; -import org.junit.BeforeClass; 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(); - } - - private boolean isCouchDB2() { - String version = dbClient.context().serverVersion(); - return version.startsWith("2"); - } - +public class ChangeNotificationsTest extends CouchDbTestBase { + @Test public void changes_normalFeed() { dbClient.save(new Foo()); 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..111c05f --- /dev/null +++ b/src/test/java/org/lightcouch/tests/CouchDbTestBase.java @@ -0,0 +1,29 @@ +package org.lightcouch.tests; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.lightcouch.CouchDbClient; + +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 isCouchDB2() { + String version = dbClient.context().serverVersion(); + return version.startsWith("2"); + } + +} 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/DesignDocumentsTest.java b/src/test/java/org/lightcouch/tests/DesignDocumentsTest.java index 0e6061f..866a54d 100644 --- a/src/test/java/org/lightcouch/tests/DesignDocumentsTest.java +++ b/src/test/java/org/lightcouch/tests/DesignDocumentsTest.java @@ -17,30 +17,15 @@ package org.lightcouch.tests; import static org.hamcrest.CoreMatchers.not; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; 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(); - } +public class DesignDocumentsTest extends CouchDbTestBase { @Test public void designDocSync() { 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/MangoTest.java b/src/test/java/org/lightcouch/tests/MangoTest.java index ec40eff..61fffe0 100644 --- a/src/test/java/org/lightcouch/tests/MangoTest.java +++ b/src/test/java/org/lightcouch/tests/MangoTest.java @@ -27,25 +27,8 @@ 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 { - private boolean isCouchDB2() { - String version = dbClient.context().serverVersion(); - return version.startsWith("2"); - } - @Test public void findDocs() { diff --git a/src/test/java/org/lightcouch/tests/ReplicationTest.java b/src/test/java/org/lightcouch/tests/ReplicationTest.java index 2f8a64b..ad03ce4 100644 --- a/src/test/java/org/lightcouch/tests/ReplicationTest.java +++ b/src/test/java/org/lightcouch/tests/ReplicationTest.java @@ -28,6 +28,7 @@ import org.lightcouch.URIBuilder; import org.junit.AfterClass; +import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Test; import org.lightcouch.CouchDbClient; @@ -44,13 +45,16 @@ public class ReplicationTest { private static URI dbClientUri; private static URI dbClient2Uri; + private static CouchDbConfigTest dbClientConfig; + private static CouchDbConfigTest dbClient2Config; + @BeforeClass public static void setUpClass() { dbClient = new CouchDbClient(); dbClient2 = new CouchDbClient("couchdb-2.properties"); - CouchDbConfigTest dbClientConfig = new CouchDbConfigTest(); - CouchDbConfigTest dbClient2Config = new CouchDbConfigTest("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(); @@ -61,6 +65,10 @@ public static void setUpClass() { @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(); } @@ -94,15 +102,15 @@ public void replication_filteredWithQueryParams() { @Test public void replicatorDB() { + 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(dbClientUri.toString()) - .target(dbClient2Uri.toString()).continuous(true) + .target(dbClient2Uri.toString()) + .continuous(true) .createTarget(true) .save(); 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() { From 843a4fb983c09b4391cedf3e498d2fef5a8eb6a3 Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Tue, 20 Feb 2018 13:05:25 +0100 Subject: [PATCH 24/70] Create CHANGES.MD --- CHANGES.MD | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 CHANGES.MD diff --git a/CHANGES.MD b/CHANGES.MD new file mode 100644 index 0000000..87ee85f --- /dev/null +++ b/CHANGES.MD @@ -0,0 +1,4 @@ +# 0.2.1 (UNRELEASED) +- [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 From ed7ffb5143332a672224070c1ea6263dca9060e3 Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Tue, 20 Feb 2018 13:05:50 +0100 Subject: [PATCH 25/70] Rename CHANGES.MD to CHANGES.md --- CHANGES.MD => CHANGES.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CHANGES.MD => CHANGES.md (100%) diff --git a/CHANGES.MD b/CHANGES.md similarity index 100% rename from CHANGES.MD rename to CHANGES.md From ff0513f83e1afbe0b49032d2b8a44b9a0ccbb37f Mon Sep 17 00:00:00 2001 From: Joseba Urkiri Date: Tue, 20 Feb 2018 13:47:09 +0100 Subject: [PATCH 26/70] sonarcloud revision --- src/main/java/org/lightcouch/CouchDbUtil.java | 9 ++++++--- src/main/java/org/lightcouch/URIBuilder.java | 3 +-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/lightcouch/CouchDbUtil.java b/src/main/java/org/lightcouch/CouchDbUtil.java index 35d03e8..6cdecff 100644 --- a/src/main/java/org/lightcouch/CouchDbUtil.java +++ b/src/main/java/org/lightcouch/CouchDbUtil.java @@ -111,6 +111,7 @@ public static int getAsInt(JsonObject j, String e) { * @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); @@ -119,7 +120,7 @@ public static List listResources(String path) { } 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")); + jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8")); Enumeration entries = jar.entries(); Set result = new HashSet(); while(entries.hasMoreElements()) { @@ -138,12 +139,13 @@ public static List listResources(String path) { } } } - close(jar); return new ArrayList(result); } return null; } catch (Exception e) { throw new CouchDbException(e); + }finally { + close(jar); } } @@ -157,7 +159,8 @@ public static String readFile(String path) { content.append(scanner.nextLine() + LINE_SEP); } } finally { - scanner.close(); + close(instream); + close(scanner); } return content.toString(); } diff --git a/src/main/java/org/lightcouch/URIBuilder.java b/src/main/java/org/lightcouch/URIBuilder.java index 8bc6d81..d63501a 100644 --- a/src/main/java/org/lightcouch/URIBuilder.java +++ b/src/main/java/org/lightcouch/URIBuilder.java @@ -103,7 +103,7 @@ public URI build() { } String q = (query.length() == 0) ? "" : "?" + query; - String uri = String.format("%s://%s:%s%s%s", new Object[] { scheme, host, port, path, q }); + String uri = String.format("%s://%s:%d%s%s", new Object[] { scheme, host, port, path, q }); try { return new URI(uri); @@ -112,5 +112,4 @@ public URI build() { } } - } From 3fd6e398c0fe592c4402736066107a87743c7e1e Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Tue, 20 Feb 2018 14:45:05 +0100 Subject: [PATCH 27/70] Create CONTRIBUTING.md --- CONTRIBUTING.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 CONTRIBUTING.md 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. +``` From 99a50b5010f4792c3efd75f8fa9484f36e56667c Mon Sep 17 00:00:00 2001 From: Joseba Urkiri Date: Tue, 20 Feb 2018 14:49:28 +0100 Subject: [PATCH 28/70] Fix build method broken in previous merge --- src/main/java/org/lightcouch/URIBuilder.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/lightcouch/URIBuilder.java b/src/main/java/org/lightcouch/URIBuilder.java index d355399..6ee5530 100644 --- a/src/main/java/org/lightcouch/URIBuilder.java +++ b/src/main/java/org/lightcouch/URIBuilder.java @@ -128,12 +128,11 @@ private URI build(boolean includeCredentials) { 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); - } } - return null; + try { + return new URI(uri); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } } } From e88d6a14a7d407bc0e298ff9912768773ca9f596 Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Tue, 20 Feb 2018 14:59:53 +0100 Subject: [PATCH 29/70] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ff67a17..9b95cd1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 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/badges/gate?key=org.lightcouch:lightcouch)](https://sonarcloud.io/dashboard?id=org.lightcouch:lightcouch) +[![Sonarcloud](https://sonarcloud.io/api/badges/gate?key=es.indaba:lightcouch)](https://sonarcloud.io/dashboard?id=es.indaba:lightcouch) A Java _client_ for [CouchDB](http://couchdb.apache.org/) database. From 3668628971d3fe8a66fac3d70d20edf4014c878c Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Feb 2018 18:23:04 +0100 Subject: [PATCH 30/70] fix _replicator db tests - Wait until complete --- .travis.yml | 13 +++++++------ src/test/java/org/lightcouch/tests/MangoTest.java | 3 --- .../java/org/lightcouch/tests/ReplicationTest.java | 10 +++++++++- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index b3dc7b4..0c5e2cb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,18 +6,13 @@ addons: organization: "indaba" branches: - master + - travis-work services: - docker jobs: include: - - stage: Build and Test on CouchDB 1.x - install: - - docker pull couchdb:1.7.1 - - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:1.7.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 - stage: Build and Test on CouchDB 2.x install: - docker pull couchdb:2.1.1 @@ -30,3 +25,9 @@ jobs: - "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 1.x + install: + - docker pull couchdb:1.7.1 + - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:1.7.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 diff --git a/src/test/java/org/lightcouch/tests/MangoTest.java b/src/test/java/org/lightcouch/tests/MangoTest.java index 61fffe0..7ca4e0f 100644 --- a/src/test/java/org/lightcouch/tests/MangoTest.java +++ b/src/test/java/org/lightcouch/tests/MangoTest.java @@ -21,11 +21,8 @@ import java.util.List; -import org.junit.AfterClass; import org.junit.Assume; -import org.junit.BeforeClass; import org.junit.Test; -import org.lightcouch.CouchDbClient; public class MangoTest extends CouchDbTestBase { diff --git a/src/test/java/org/lightcouch/tests/ReplicationTest.java b/src/test/java/org/lightcouch/tests/ReplicationTest.java index ad03ce4..da3c914 100644 --- a/src/test/java/org/lightcouch/tests/ReplicationTest.java +++ b/src/test/java/org/lightcouch/tests/ReplicationTest.java @@ -101,7 +101,7 @@ public void replication_filteredWithQueryParams() { } @Test - public void replicatorDB() { + public void replicatorDB() throws InterruptedException { String version = dbClient.context().serverVersion(); Assume.assumeTrue(!(version.startsWith("0") || version.startsWith("1.0"))); @@ -124,6 +124,14 @@ public void replicatorDB() { .replicatorDocId(response.getId()) .find(); + // Wait until complete + while ("triggered".equals(replicatorDoc.getReplicationState())) { + replicatorDoc = dbClient.replicator() + .replicatorDocId(response.getId()) + .find(); + Thread.sleep(10); + } + // cancel a replication dbClient.replicator() .replicatorDocId(replicatorDoc.getId()) From c2d8bd36d7ea24c00e6eda97ffa361b598216d34 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Feb 2018 19:34:00 +0100 Subject: [PATCH 31/70] Retry replica cancellation in replication test --- .../org/lightcouch/tests/ReplicationTest.java | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/test/java/org/lightcouch/tests/ReplicationTest.java b/src/test/java/org/lightcouch/tests/ReplicationTest.java index da3c914..323e942 100644 --- a/src/test/java/org/lightcouch/tests/ReplicationTest.java +++ b/src/test/java/org/lightcouch/tests/ReplicationTest.java @@ -27,11 +27,15 @@ import java.util.UUID; import org.lightcouch.URIBuilder; +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.Test; import org.lightcouch.CouchDbClient; +import org.lightcouch.CouchDbException; +import org.lightcouch.DocumentConflictException; import org.lightcouch.ReplicationResult; import org.lightcouch.ReplicationResult.ReplicationHistory; import org.lightcouch.ReplicatorDocument; @@ -48,6 +52,8 @@ public class ReplicationTest { private static CouchDbConfigTest dbClientConfig; private static CouchDbConfigTest dbClient2Config; + private static Log LOG = LogFactory.getLog(ReplicationTest.class); + @BeforeClass public static void setUpClass() { dbClient = new CouchDbClient(); @@ -120,23 +126,38 @@ public void replicatorDB() throws InterruptedException { assertThat(replicatorDocs.size(), is(not(0))); // find replicator doc + String id = response.getId(); ReplicatorDocument replicatorDoc = dbClient.replicator() - .replicatorDocId(response.getId()) + .replicatorDocId(id) .find(); - - // Wait until complete - while ("triggered".equals(replicatorDoc.getReplicationState())) { - replicatorDoc = dbClient.replicator() - .replicatorDocId(response.getId()) - .find(); - Thread.sleep(10); + + LOG.info("Replication state "+replicatorDoc.getReplicationState()); + + // Wait until cancelled + boolean cancelled=false; + while (!cancelled) { + cancelled = tryCancelReplication(id); + Thread.sleep(100); } - // cancel a replication - dbClient.replicator() - .replicatorDocId(replicatorDoc.getId()) - .replicatorDocRev(replicatorDoc.getRevision()) - .remove(); + } + + 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 From fa672377b0eae5abe56dc252deba1b7c1a0a14ae Mon Sep 17 00:00:00 2001 From: Joseba Urkiri Date: Wed, 21 Feb 2018 12:03:37 +0100 Subject: [PATCH 32/70] Version 0.2.1 release --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4415595..2c2b1fb 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ es.indaba lightcouch - 0.2.1-SNAPSHOT + 0.2.1 jar LightCouch CouchDB Java API From 4bed5bc30dbf88a031294e9c18b9fcd002ff0ac4 Mon Sep 17 00:00:00 2001 From: Joseba Urkiri Date: Wed, 21 Feb 2018 12:08:17 +0100 Subject: [PATCH 33/70] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2c2b1fb..47d30a9 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ es.indaba lightcouch - 0.2.1 + 0.2.2-SNAPSHOT jar LightCouch CouchDB Java API From f6b7a143e799c62674863a58853ca33a88677f06 Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Wed, 21 Feb 2018 21:42:46 +0100 Subject: [PATCH 34/70] Update CHANGES.md --- CHANGES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 87ee85f..38fd67b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,6 @@ -# 0.2.1 (UNRELEASED) +# 0.2.2 (UNRELEASED) + +# 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 From f19036a41d9702519e77a3414aa83e804e8d4892 Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Wed, 21 Feb 2018 21:53:07 +0100 Subject: [PATCH 35/70] Explicit set for LICENSE --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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. From b16b5e67c996b0afb96091c215bd2f59f39bba24 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Fri, 23 Feb 2018 00:28:09 +0100 Subject: [PATCH 36/70] Add explicit API for local documents --- .../org/lightcouch/CouchDbClientBase.java | 6 + src/main/java/org/lightcouch/Local.java | 139 +++++++++++++ .../tests/LocalDocumentsCRUDTest.java | 194 ++++++++++++++++++ .../org/lightcouch/tests/ReplicationTest.java | 3 +- 4 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/lightcouch/Local.java create mode 100644 src/test/java/org/lightcouch/tests/LocalDocumentsCRUDTest.java diff --git a/src/main/java/org/lightcouch/CouchDbClientBase.java b/src/main/java/org/lightcouch/CouchDbClientBase.java index 46c9356..10446f8 100644 --- a/src/main/java/org/lightcouch/CouchDbClientBase.java +++ b/src/main/java/org/lightcouch/CouchDbClientBase.java @@ -82,6 +82,7 @@ public abstract class CouchDbClientBase { private Gson gson; private CouchDbContext context; private CouchDbDesign design; + private Local local; final HttpClient httpClient; final HttpHost host; @@ -101,6 +102,7 @@ public abstract class CouchDbClientBase { this.context = new CouchDbContext(this, props); this.design = new CouchDbDesign(this); + this.local = new Local(this); } // Client(s) provided implementation @@ -146,6 +148,10 @@ public CouchDbDesign design() { public View view(String viewId) { return new View(this, viewId); } + + public Local local() { + return local; + } /** * Provides access to CouchDB replication APIs. diff --git a/src/main/java/org/lightcouch/Local.java b/src/main/java/org/lightcouch/Local.java new file mode 100644 index 0000000..7450fb0 --- /dev/null +++ b/src/main/java/org/lightcouch/Local.java @@ -0,0 +1,139 @@ +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 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 remove(String id) { + assertNotEmpty(id, "id"); + final URI docURI = buildURIforLocal(id); + 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/test/java/org/lightcouch/tests/LocalDocumentsCRUDTest.java b/src/test/java/org/lightcouch/tests/LocalDocumentsCRUDTest.java new file mode 100644 index 0000000..b4a4a43 --- /dev/null +++ b/src/test/java/org/lightcouch/tests/LocalDocumentsCRUDTest.java @@ -0,0 +1,194 @@ +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.Test; +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() { + 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())); + } + + // 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 deleteObject() { + Response response = dbClient.local().save(new Foo()); + Foo foo = dbClient.local().find(Foo.class, response.getId()); + dbClient.local().remove(foo); + } + + @Test + public void deleteByIdAndRevValues() { + Response response = dbClient.local().save(new Foo()); + response = dbClient.local().remove(response.getId()); + assertNotNull(response); + } + + @Test + public void deleteByIdContainSlash() { + 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() { + + 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/ReplicationTest.java b/src/test/java/org/lightcouch/tests/ReplicationTest.java index 323e942..7a2532d 100644 --- a/src/test/java/org/lightcouch/tests/ReplicationTest.java +++ b/src/test/java/org/lightcouch/tests/ReplicationTest.java @@ -26,7 +26,6 @@ import java.util.Map; import java.util.UUID; -import org.lightcouch.URIBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.AfterClass; @@ -34,12 +33,12 @@ import org.junit.BeforeClass; import org.junit.Test; import org.lightcouch.CouchDbClient; -import org.lightcouch.CouchDbException; 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; public class ReplicationTest { From 47ded6ea5fc0c58d9b799126f01934063d2a30d6 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Fri, 23 Feb 2018 10:01:00 +0100 Subject: [PATCH 37/70] Fix test for CouchDB 1.x --- src/main/java/org/lightcouch/Local.java | 15 +++++ .../org/lightcouch/tests/CouchDbTestBase.java | 4 ++ .../tests/LocalDocumentsCRUDTest.java | 64 ++++++++++++++----- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/lightcouch/Local.java b/src/main/java/org/lightcouch/Local.java index 7450fb0..4e42837 100644 --- a/src/main/java/org/lightcouch/Local.java +++ b/src/main/java/org/lightcouch/Local.java @@ -114,12 +114,27 @@ public Response remove(Object object) { 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); diff --git a/src/test/java/org/lightcouch/tests/CouchDbTestBase.java b/src/test/java/org/lightcouch/tests/CouchDbTestBase.java index 111c05f..030c8e7 100644 --- a/src/test/java/org/lightcouch/tests/CouchDbTestBase.java +++ b/src/test/java/org/lightcouch/tests/CouchDbTestBase.java @@ -26,4 +26,8 @@ protected boolean isCouchDB2() { return version.startsWith("2"); } + protected boolean isCouchDB1() { + String version = dbClient.context().serverVersion(); + return version.startsWith("0") || version.startsWith("1") ; + } } diff --git a/src/test/java/org/lightcouch/tests/LocalDocumentsCRUDTest.java b/src/test/java/org/lightcouch/tests/LocalDocumentsCRUDTest.java index b4a4a43..1348fef 100644 --- a/src/test/java/org/lightcouch/tests/LocalDocumentsCRUDTest.java +++ b/src/test/java/org/lightcouch/tests/LocalDocumentsCRUDTest.java @@ -10,7 +10,9 @@ 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; @@ -117,6 +119,7 @@ public void saveNewDocWithRevision_throwsIllegalArgumentException() { @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); @@ -127,7 +130,21 @@ public void saveDocWithDuplicateId_allowed() { assertTrue("two".equals(foo.getTitle())); } - // Update + @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() { @@ -136,53 +153,70 @@ public void update() { 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()); + assertEquals("_local/" + idWithSlash, responseUpdate.getId()); } // Delete @Test - public void deleteObject() { + 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 deleteByIdAndRevValues() { + 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()); + assertEquals("_local/" + idWithSlash, responseRemove.getId()); } - + @Test public void findAllLocalDocs() { - - List list = dbClient.local().findAll(); + 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); + + list = dbClient.local().findAll(); + assertTrue(list.size() == intialSize + 2); } From 93fa07dcc564b40f3bada0c480522de596008fee Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Fri, 23 Feb 2018 12:19:05 +0100 Subject: [PATCH 38/70] Update CHANGES.md --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 38fd67b..a1ef927 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,5 @@ # 0.2.2 (UNRELEASED) +- [NEW] Added explicit API for local document management. # 0.2.1 (21/02/2018) - [NEW] Added API for specifying a mango selector _changes operation From 8ac392e3b487e18f095fd2be50618084696cb03d Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Mon, 5 Mar 2018 08:50:05 +0100 Subject: [PATCH 39/70] Fix final modifier for constant and add license header --- .../java/org/lightcouch/CouchDbClientBase.java | 1 + src/main/java/org/lightcouch/Local.java | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/lightcouch/CouchDbClientBase.java b/src/main/java/org/lightcouch/CouchDbClientBase.java index 10446f8..53728d0 100644 --- a/src/main/java/org/lightcouch/CouchDbClientBase.java +++ b/src/main/java/org/lightcouch/CouchDbClientBase.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"); diff --git a/src/main/java/org/lightcouch/Local.java b/src/main/java/org/lightcouch/Local.java index 4e42837..b3ce412 100644 --- a/src/main/java/org/lightcouch/Local.java +++ b/src/main/java/org/lightcouch/Local.java @@ -1,3 +1,19 @@ +/* + * 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; @@ -14,7 +30,7 @@ public class Local { - private static String LOCAL_PATH = "_local"; + private static final String LOCAL_PATH = "_local"; private CouchDbClientBase dbc; private URI dbURI; From 383da0fd1d120ee0bf3a322984aa3b3323a6e1ec Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Mar 2018 15:51:01 +0100 Subject: [PATCH 40/70] Add seq_interval parameter to Changes API --- src/main/java/org/lightcouch/Changes.java | 5 ++++ .../tests/ChangeNotificationsTest.java | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/main/java/org/lightcouch/Changes.java b/src/main/java/org/lightcouch/Changes.java index b844fd7..6923888 100644 --- a/src/main/java/org/lightcouch/Changes.java +++ b/src/main/java/org/lightcouch/Changes.java @@ -192,6 +192,11 @@ public Changes style(String style) { return this; } + public Changes seqInterval(long batchSize) { + uriBuilder.query("seq_interval", batchSize); + return this; + } + // Helper /** diff --git a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java index 5eda426..2a52bc8 100644 --- a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java +++ b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java @@ -58,6 +58,35 @@ public void changes_normalFeed() { assertThat(rows.size(), is(1)); } + + @Test + public void changes_normalFeed_seqInterval() { + 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() { From 0e6d9196e5907aad15073adf5faa6a64a3cb71f2 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Mar 2018 15:54:36 +0100 Subject: [PATCH 41/70] Updated CHANGES.md --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index a1ef927..12d12a7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ # 0.2.2 (UNRELEASED) - [NEW] Added explicit API for local document management. +- [NEW] Added seq_interval parameter in Changes API # 0.2.1 (21/02/2018) - [NEW] Added API for specifying a mango selector _changes operation From 97f010c1e6d97e3b744f05c7a293d09e30748734 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Mar 2018 16:01:30 +0100 Subject: [PATCH 42/70] Parameter seq_interval is only supported in CouchDB 2.x --- src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java index 2a52bc8..7a05631 100644 --- a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java +++ b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java @@ -61,6 +61,8 @@ public void changes_normalFeed() { @Test public void changes_normalFeed_seqInterval() { + Assume.assumeTrue(isCouchDB2()); + dbClient.save(new Foo()); dbClient.save(new Foo()); dbClient.save(new Foo()); From 86df689e1362a9182d19d2f7ca9df116c2a2b437 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Mar 2018 16:37:52 +0100 Subject: [PATCH 43/70] Reformat source code --- src/main/java/org/lightcouch/Changes.java | 361 +++++++++++----------- 1 file changed, 182 insertions(+), 179 deletions(-) diff --git a/src/main/java/org/lightcouch/Changes.java b/src/main/java/org/lightcouch/Changes.java index 6923888..1ae5bcf 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 * - * 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; @@ -30,8 +27,10 @@ import com.google.gson.Gson; /** - *

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
@@ -67,182 +66,186 @@
  *	.getChanges();
  *
  * 
+ * * @see ChangesResult * @since 0.0.2 * @author Ahmed Yehia */ public class Changes { - - private BufferedReader reader; - private HttpUriRequest httpRequest; - private Row nextRow; - private boolean stop; - - private CouchDbClientBase dbc; - private Gson gson; - private URIBuilder uriBuilder; - - private String selector; - - Changes(CouchDbClientBase dbc) { - this.dbc = dbc; - this.gson = dbc.getGson(); - this.uriBuilder = URIBuilder.buildUri(dbc.getDBUri()).path("_changes"); - } - - /** - * Requests Change notifications of feed type continuous. - *

Feed notifications are accessed in an iterator style. - * @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) { - return dbc.get(uri, ChangesResult.class); - } else { - return dbc.post(uri, selector, ChangesResult.class); - } - } - - // Query Params - - public Changes since(String since) { - uriBuilder.query("since", since); - return this; - } - - public Changes limit(int limit) { - uriBuilder.query("limit", limit); - return this; - } - - public Changes heartBeat(long heartBeat) { - uriBuilder.query("heartbeat", heartBeat); - return this; - } - - public Changes timeout(long timeout) { - uriBuilder.query("timeout", timeout); - return this; - } - - public Changes filter(String filter) { - uriBuilder.query("filter", filter); - return this; - } - - public Changes selector(String json) { - uriBuilder.query("filter", "_selector"); - this.selector = json; - 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) { + + private BufferedReader reader; + private HttpUriRequest httpRequest; + private Row nextRow; + private boolean stop; + + private CouchDbClientBase dbc; + private Gson gson; + private URIBuilder uriBuilder; + + private String selector; + + Changes(CouchDbClientBase dbc) { + this.dbc = dbc; + this.gson = dbc.getGson(); + this.uriBuilder = URIBuilder.buildUri(dbc.getDBUri()).path("_changes"); + } + + /** + * Requests Change notifications of feed type continuous. + *

+ * Feed notifications are accessed in an iterator style. + * + * @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) { + return dbc.get(uri, ChangesResult.class); + } else { + return dbc.post(uri, selector, ChangesResult.class); + } + } + + // Query Params + + public Changes since(String since) { + uriBuilder.query("since", since); + return this; + } + + public Changes limit(int limit) { + uriBuilder.query("limit", limit); + return this; + } + + public Changes heartBeat(long heartBeat) { + uriBuilder.query("heartbeat", heartBeat); + return this; + } + + public Changes timeout(long timeout) { + uriBuilder.query("timeout", timeout); + return this; + } + + public Changes filter(String filter) { + uriBuilder.query("filter", filter); + return this; + } + + public Changes selector(String json) { + uriBuilder.query("filter", "_selector"); + this.selector = json; + 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); - - 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()); - } + + // Helper + + /** + * Reads and sets the next feed in the stream. + */ + private boolean readNextRow() { + boolean hasNext = false; + try { + if (!stop) { + String row = ""; + do { + row = getReader().readLine(); + } while (row.length() == 0); + + if (!row.startsWith("{\"last_seq\":")) { + setNextRow(gson.fromJson(row, Row.class)); + hasNext = true; + } + } + } catch (Exception e) { + terminate(); + throw new CouchDbException("Error reading continuous stream.", e); + } + if (!hasNext) + terminate(); + return hasNext; + } + + private BufferedReader getReader() { + return reader; + } + + private void setReader(BufferedReader reader) { + this.reader = reader; + } + + private Row getNextRow() { + return nextRow; + } + + private void setNextRow(Row nextRow) { + this.nextRow = nextRow; + } + + private void terminate() { + httpRequest.abort(); + CouchDbUtil.close(getReader()); + } } From e3f0bfc16f615d5f65423dc2a0d0d8b6cdd513bb Mon Sep 17 00:00:00 2001 From: Joseba Urkiri Date: Tue, 20 Mar 2018 17:37:57 +0100 Subject: [PATCH 44/70] Added support for _db_updates endpoint (#12) --- .../java/org/lightcouch/CouchDbContext.java | 21 +++++++ src/main/java/org/lightcouch/DbUpdates.java | 44 +++++++++++++ .../java/org/lightcouch/DbUpdatesResult.java | 52 ++++++++++++++++ .../org/lightcouch/tests/DbUpdatesTest.java | 62 +++++++++++++++++++ 4 files changed, 179 insertions(+) create mode 100644 src/main/java/org/lightcouch/DbUpdates.java create mode 100644 src/main/java/org/lightcouch/DbUpdatesResult.java create mode 100644 src/test/java/org/lightcouch/tests/DbUpdatesTest.java diff --git a/src/main/java/org/lightcouch/CouchDbContext.java b/src/main/java/org/lightcouch/CouchDbContext.java index 3b508f0..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. @@ -175,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/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/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()); + } +} From f8cdc649f8740d805cb1e4d08c4e7f3f9a1d889d Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Tue, 20 Mar 2018 18:40:43 +0100 Subject: [PATCH 45/70] Update CHANGES.md --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 12d12a7..9ddb70e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ # 0.2.2 (UNRELEASED) - [NEW] Added explicit API for local document management. - [NEW] Added seq_interval parameter in Changes API +- [NEW] Added _db_updates endpoint support # 0.2.1 (21/02/2018) - [NEW] Added API for specifying a mango selector _changes operation From 2f41ab0b053795c1d95868585d1ab0543b589642 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 20 Mar 2018 19:41:35 +0100 Subject: [PATCH 46/70] Include stop condition check inside reading loop --- src/main/java/org/lightcouch/Changes.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/lightcouch/Changes.java b/src/main/java/org/lightcouch/Changes.java index 1ae5bcf..ade00b7 100644 --- a/src/main/java/org/lightcouch/Changes.java +++ b/src/main/java/org/lightcouch/Changes.java @@ -1,5 +1,6 @@ /* * 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 @@ -212,11 +213,13 @@ private boolean readNextRow() { String row = ""; do { row = getReader().readLine(); - } while (row.length() == 0); + } while (row.length() == 0 && !stop); - if (!row.startsWith("{\"last_seq\":")) { - setNextRow(gson.fromJson(row, Row.class)); - hasNext = true; + if (!stop) { + if (!row.startsWith("{\"last_seq\":")) { + setNextRow(gson.fromJson(row, Row.class)); + hasNext = true; + } } } } catch (Exception e) { From e614259306c8460e45b5d5dd6b1cd4e04f6eaebf Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Wed, 21 Mar 2018 10:13:55 +0100 Subject: [PATCH 47/70] Update CHANGES.md --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 9ddb70e..14c6255 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ - [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 From 41c02d47c3c5c10549baae8089a8883a8056cbf5 Mon Sep 17 00:00:00 2001 From: Joseba Urkiri Date: Wed, 21 Mar 2018 11:11:20 +0100 Subject: [PATCH 48/70] Release v0.2.2 --- CHANGES.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 14c6255..3082da9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -# 0.2.2 (UNRELEASED) +# 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 diff --git a/pom.xml b/pom.xml index 47d30a9..e75a553 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ es.indaba lightcouch - 0.2.2-SNAPSHOT + 0.2.2 jar LightCouch CouchDB Java API From 8a17ab0e714efda60a61a2eb4e1b4317e7237728 Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Mon, 20 Aug 2018 10:36:45 +0200 Subject: [PATCH 49/70] Update README.md --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b95cd1..d108520 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,18 @@ 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/badges/gate?key=es.indaba:lightcouch)](https://sonarcloud.io/dashboard?id=es.indaba: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. 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.2 + +``` From 54b5a9de44d55e873bb7bdbffd59939ea921903e Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Tue, 11 Sep 2018 14:45:49 +0200 Subject: [PATCH 50/70] Add CouchDB 2.2.x as test case in travis (#15) * Update CouchDB versions for testing to 1.7.2,2.1.2 * Add CouchDB 2.2.0 as testing environment --- .travis.yml | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0c5e2cb..1b47925 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,19 +4,19 @@ language: java addons: sonarcloud: organization: "indaba" - branches: - - master - - travis-work - + apt: + packages: + - docker-ce + services: - docker jobs: include: - - stage: Build and Test on CouchDB 2.x + - stage: Build and Test on CouchDB 2.2.x install: - - docker pull couchdb:2.1.1 - - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.1.1 + - 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 5 @@ -25,9 +25,19 @@ jobs: - "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 1.x + - stage: Build and Test on CouchDB 2.1.x + install: + - docker pull couchdb:2.1.2 + - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.1.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 5 + - "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.1 - - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:1.7.1 + - 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 From ea5e32f44069be76e121d7f57f9c4831f99eda14 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 18 Sep 2018 14:54:49 +0200 Subject: [PATCH 51/70] Fix #16 - Add support for _doc_ids filter in changes operation --- src/main/java/org/lightcouch/Changes.java | 39 ++- .../tests/ChangeNotificationsTest.java | 277 +++++++++++------- 2 files changed, 201 insertions(+), 115 deletions(-) diff --git a/src/main/java/org/lightcouch/Changes.java b/src/main/java/org/lightcouch/Changes.java index ade00b7..38faeb6 100644 --- a/src/main/java/org/lightcouch/Changes.java +++ b/src/main/java/org/lightcouch/Changes.java @@ -1,6 +1,5 @@ /* - * Copyright (C) 2011 lightcouch.org - * Copyright (C) 2018 indaba.es + * 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 @@ -18,6 +17,7 @@ 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; @@ -26,6 +26,8 @@ import org.lightcouch.ChangesResult.Row; import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; /** *

@@ -83,7 +85,9 @@ public class Changes { private Gson gson; private URIBuilder uriBuilder; + private String filter; private String selector; + private List docIds; Changes(CouchDbClientBase dbc) { this.dbc = dbc; @@ -146,10 +150,21 @@ public void stop() { */ public ChangesResult getChanges() { final URI uri = uriBuilder.query("feed", "normal").build(); - if (selector == null) { + if (selector == null && docIds == null) { return dbc.get(uri, ChangesResult.class); } else { - return dbc.post(uri, selector, ChangesResult.class); + 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); } } @@ -176,16 +191,32 @@ public Changes timeout(long timeout) { } 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; diff --git a/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java b/src/test/java/org/lightcouch/tests/ChangeNotificationsTest.java index 7a05631..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,7 +17,10 @@ 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.Assume; @@ -34,31 +34,28 @@ import com.google.gson.JsonObject; 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() { + 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()); @@ -90,82 +87,140 @@ public void changes_normalFeed_seqInterval() { 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_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(); - } - } + @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(); + } + } } From e08433f74f660f36bee220129cb23e732b6e3156 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 18 Sep 2018 15:11:53 +0200 Subject: [PATCH 52/70] Add new release info --- CHANGES.md | 3 +++ pom.xml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 3082da9..fa8c1cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +# 0.2.3 (UNRELEASED) +- [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 diff --git a/pom.xml b/pom.xml index e75a553..c3e4fe4 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ es.indaba lightcouch - 0.2.2 + 0.2.3 jar LightCouch CouchDB Java API From 75a144f8f9b90e6519d3087d5bc24a0934c9ba7f Mon Sep 17 00:00:00 2001 From: Joseba Urkiri Date: Tue, 18 Sep 2018 15:36:16 +0200 Subject: [PATCH 53/70] Update CHANGES.md 0.2.3 info added --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index fa8c1cc..053a4f3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -# 0.2.3 (UNRELEASED) +# 0.2.3 (18/09/2018) - [NEW] Added API for specifying docIds filter to _changes operation # 0.2.2 (21/03/2018) From 0615be0bdfc90bb0d17a66ba905a9296974f8c02 Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Tue, 18 Sep 2018 15:53:40 +0200 Subject: [PATCH 54/70] Update maven version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d108520..e2974d3 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,6 @@ The releases of this fork are published via Jitpack com.github.IndabaConsultores lightCouch - 0.2.2 + 0.2.3 ``` From 89344f0346ddc7a97cde8b2e454af1218e2f3a6c Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Mon, 8 Oct 2018 17:05:58 +0200 Subject: [PATCH 55/70] Add method for checking design doc existence (#19) Fix #18 - Add method for checking design doc existence --- CHANGES.md | 29 +- pom.xml | 166 +++---- .../java/org/lightcouch/CouchDbDesign.java | 23 + src/main/java/org/lightcouch/CouchDbUtil.java | 433 +++++++++--------- .../lightcouch/tests/DesignDocumentsTest.java | 128 +++--- .../test-design-doc/filters/example_filter.js | 7 + 6 files changed, 423 insertions(+), 363 deletions(-) create mode 100644 src/test/resources/design-docs/test-design-doc/filters/example_filter.js diff --git a/CHANGES.md b/CHANGES.md index 053a4f3..4511dd6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,13 +1,16 @@ -# 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 +# 0.2.4 (UNRELEASED) +- [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/pom.xml b/pom.xml index c3e4fe4..e26507b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,83 +1,83 @@ - - 4.0.0 - - es.indaba - lightcouch - 0.2.3 - 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 - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - 1.5 - 1.5 - - - - - + + 4.0.0 + + es.indaba + lightcouch + 0.2.4 + 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 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.5 + 1.5 + + + + + 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/CouchDbUtil.java b/src/main/java/org/lightcouch/CouchDbUtil.java index 6cdecff..ec817b6 100644 --- a/src/main/java/org/lightcouch/CouchDbUtil.java +++ b/src/main/java/org/lightcouch/CouchDbUtil.java @@ -1,213 +1,220 @@ -/* - * 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) { - 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) {} - } -} +/* + * 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.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 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/test/java/org/lightcouch/tests/DesignDocumentsTest.java b/src/test/java/org/lightcouch/tests/DesignDocumentsTest.java index 866a54d..15dbf11 100644 --- a/src/test/java/org/lightcouch/tests/DesignDocumentsTest.java +++ b/src/test/java/org/lightcouch/tests/DesignDocumentsTest.java @@ -1,54 +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.assertEquals; -import static org.junit.Assert.assertThat; - -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)); - } - -} +/* + * 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/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 From 59476e1d92e8c5c7d58190ab8666fa59527b988e Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Mon, 8 Oct 2018 17:18:02 +0200 Subject: [PATCH 56/70] Update release date --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 4511dd6..36f755f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -# 0.2.4 (UNRELEASED) +# 0.2.4 (08/10/2018) - [NEW] Added API for checking design doc existence # 0.2.3 (18/09/2018) From 8be5df0bd1f3d924b94376f87f5ba2c93de13049 Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Mon, 8 Oct 2018 17:18:43 +0200 Subject: [PATCH 57/70] Update version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e2974d3..30838f7 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,6 @@ The releases of this fork are published via Jitpack com.github.IndabaConsultores lightCouch - 0.2.3 + 0.2.4 ``` From 93b3f565fbc69d13df171305d2eec90600c05165 Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Fri, 14 Dec 2018 11:52:04 +0100 Subject: [PATCH 58/70] Add testing on CouchDB 2.3 and Fix purge_seq behaviour change (#21) * Test on CouchDB 2.3 * Update .travis.yml * Fix #20 - Change purge_seq from number to string in CouchDB 2.3 --- .travis.yml | 12 +++++++++++- CHANGES.md | 3 +++ src/main/java/org/lightcouch/CouchDbInfo.java | 5 +++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1b47925..2ff0854 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,12 +7,22 @@ addons: apt: packages: - docker-ce - + services: - docker jobs: include: + - stage: Build and Test on CouchDB 2.3.x + install: + - docker pull couchdb:2.3.0 + - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.3.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 5 + - "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 diff --git a/CHANGES.md b/CHANGES.md index 36f755f..d4f3325 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +# 0.2.5 (UNRELEASED) +- [FIX] Support for string purge_seqs in CouchDB 2.3 + # 0.2.4 (08/10/2018) - [NEW] Added API for checking design doc existence 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; } From d7146b77f55b0acf0637eb7303abb89378672429 Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Sun, 23 Dec 2018 23:06:59 +0100 Subject: [PATCH 59/70] Update CHANGES.md --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index d4f3325..2c763ef 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -# 0.2.5 (UNRELEASED) +# 0.2.5 (23/12/2018) - [FIX] Support for string purge_seqs in CouchDB 2.3 # 0.2.4 (08/10/2018) From f3a4599aaa05447d4e7ed5db1fd291b02c0e3128 Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Sun, 23 Dec 2018 23:07:42 +0100 Subject: [PATCH 60/70] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 30838f7..f210aa6 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,6 @@ The releases of this fork are published via Jitpack com.github.IndabaConsultores lightCouch - 0.2.4 + 0.2.5 ``` From 7ae93860d47c0b9aa953579f4b200a436f893e0e Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Sun, 23 Dec 2018 23:08:58 +0100 Subject: [PATCH 61/70] Release 0.2.5 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e26507b..816c673 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ es.indaba lightcouch - 0.2.4 + 0.2.5 jar LightCouch CouchDB Java API From 943a15dfbcb0c0b9e9bce9b9285093fb1bbd02bc Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Mon, 18 Feb 2019 22:28:14 +0100 Subject: [PATCH 62/70] Add support for /db/_purge operation implemented in CouchDB 2.3.0 (#22) * Add support for clustered purge operation * Increase sleep time for couchdb startup in travis * Update CHANGES.md --- .travis.yml | 7 +- CHANGES.md | 3 + .../org/lightcouch/CouchDbClientBase.java | 1565 +++++++++-------- src/main/java/org/lightcouch/CouchDbUtil.java | 20 +- .../java/org/lightcouch/PurgeResponse.java | 31 + .../org/lightcouch/tests/CouchDbTestBase.java | 71 +- .../java/org/lightcouch/tests/PurgeTest.java | 87 + 7 files changed, 993 insertions(+), 791 deletions(-) create mode 100644 src/main/java/org/lightcouch/PurgeResponse.java create mode 100644 src/test/java/org/lightcouch/tests/PurgeTest.java diff --git a/.travis.yml b/.travis.yml index 2ff0854..94637fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ jobs: - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.3.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 5 + - 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" @@ -29,7 +29,7 @@ jobs: - 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 5 + - 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" @@ -41,7 +41,7 @@ jobs: - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.1.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 5 + - 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" @@ -51,3 +51,4 @@ jobs: - 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 index 2c763ef..e4716b1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +# 0.2.6 (UNRELEASED) +- [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 diff --git a/src/main/java/org/lightcouch/CouchDbClientBase.java b/src/main/java/org/lightcouch/CouchDbClientBase.java index 53728d0..5d2dcc0 100644 --- a/src/main/java/org/lightcouch/CouchDbClientBase.java +++ b/src/main/java/org/lightcouch/CouchDbClientBase.java @@ -1,750 +1,815 @@ -/* - * 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 org.lightcouch.CouchDbUtil.assertNotEmpty; -import static org.lightcouch.CouchDbUtil.assertNull; -import static org.lightcouch.CouchDbUtil.close; -import static org.lightcouch.CouchDbUtil.generateUUID; -import static org.lightcouch.CouchDbUtil.getAsString; -import static org.lightcouch.CouchDbUtil.getStream; -import static org.lightcouch.CouchDbUtil.streamToString; -import static org.lightcouch.URIBuilder.buildUri; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.UnsupportedEncodingException; -import java.lang.reflect.Type; -import java.net.URI; -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.codec.Charsets; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.http.HttpHost; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpHead; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.entity.InputStreamEntity; -import org.apache.http.entity.StringEntity; -import org.apache.http.protocol.HttpContext; -import org.apache.http.util.EntityUtils; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonParser; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; -import com.google.gson.reflect.TypeToken; - -/** - * 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; - 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); - } - - /** - * 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 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); - } - 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(); - } -} +/* + * 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 + * + * 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.assertNull; +import static org.lightcouch.CouchDbUtil.close; +import static org.lightcouch.CouchDbUtil.generateUUID; +import static org.lightcouch.CouchDbUtil.getAsString; +import static org.lightcouch.CouchDbUtil.getStream; +import static org.lightcouch.CouchDbUtil.streamToString; +import static org.lightcouch.URIBuilder.buildUri; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Type; +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.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.InputStreamEntity; +import org.apache.http.entity.StringEntity; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.reflect.TypeToken; + +/** + * 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; + 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. + * + * @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 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); + } + default: { // other errors: 400 | 401 | 500 etc. + throw new CouchDbException(reason += EntityUtils.toString(response.getEntity())); + } + } + } + + /** + * @param response The {@link HttpResponse} + * @return {@link Response} + */ + private Response getResponse(HttpResponse response) throws CouchDbException { + InputStreamReader reader = new InputStreamReader(getStream(response), Charsets.UTF_8); + return getGson().fromJson(reader, Response.class); + } + + /** + * @param response The {@link HttpResponse} + * @return {@link Response} + */ + private List getResponseList(HttpResponse response) throws CouchDbException { + InputStream instream = getStream(response); + Reader reader = new InputStreamReader(instream, Charsets.UTF_8); + return getGson().fromJson(reader, new TypeToken>() {}.getType()); + } + + /** + * Sets a JSON String as a request entity. + * + * @param httpRequest The request to set entity. + * @param json The JSON String to set. + */ + private void setEntity(HttpEntityEnclosingRequestBase httpRequest, String json) { + StringEntity entity = new StringEntity(json, "UTF-8"); + entity.setContentType("application/json"); + httpRequest.setEntity(entity); + } + + /** + * Builds {@link Gson} and registers any required serializer/deserializer. + * + * @return {@link Gson} instance + */ + private Gson initGson(GsonBuilder gsonBuilder) { + gsonBuilder.registerTypeAdapter(JsonObject.class, new JsonDeserializer() { + public JsonObject deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return json.getAsJsonObject(); + } + }); + gsonBuilder.registerTypeAdapter(JsonObject.class, new JsonSerializer() { + public JsonElement serialize(JsonObject src, Type typeOfSrc, JsonSerializationContext context) { + return src.getAsJsonObject(); + } + + }); + return gsonBuilder.create(); + } +} diff --git a/src/main/java/org/lightcouch/CouchDbUtil.java b/src/main/java/org/lightcouch/CouchDbUtil.java index ec817b6..f086f82 100644 --- a/src/main/java/org/lightcouch/CouchDbUtil.java +++ b/src/main/java/org/lightcouch/CouchDbUtil.java @@ -26,9 +26,11 @@ 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; @@ -47,21 +49,29 @@ */ 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 } - public static void assertNotEmpty(Object object, String prefix) throws IllegalArgumentException { + @SuppressWarnings("rawtypes") + public static void assertNotEmpty(Object object, String prefix) throws IllegalArgumentException { if(object == null) { - throw new IllegalArgumentException(format("%s may not be null.", prefix)); + throw new IllegalArgumentException(format(MSG_NOT_NULL, prefix)); } else if(object instanceof String && ((String)object).length() == 0) { - throw new IllegalArgumentException(format("%s may not be empty.", prefix)); - } + 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("%s should be null.", prefix)); + throw new IllegalArgumentException(format(MSG_NOT_NULL, prefix)); } } 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/test/java/org/lightcouch/tests/CouchDbTestBase.java b/src/test/java/org/lightcouch/tests/CouchDbTestBase.java index 030c8e7..2840a74 100644 --- a/src/test/java/org/lightcouch/tests/CouchDbTestBase.java +++ b/src/test/java/org/lightcouch/tests/CouchDbTestBase.java @@ -1,33 +1,38 @@ -package org.lightcouch.tests; - -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.lightcouch.CouchDbClient; - -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 isCouchDB2() { - String version = dbClient.context().serverVersion(); - return version.startsWith("2"); - } - - protected boolean isCouchDB1() { - String version = dbClient.context().serverVersion(); - return version.startsWith("0") || version.startsWith("1") ; - } -} +package org.lightcouch.tests; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.lightcouch.CouchDbClient; + +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() { + String version = dbClient.context().serverVersion(); + return version.startsWith("2.3"); + } + + protected boolean isCouchDB2() { + String version = dbClient.context().serverVersion(); + return version.startsWith("2"); + } + + protected boolean isCouchDB1() { + String version = dbClient.context().serverVersion(); + return version.startsWith("0") || version.startsWith("1") ; + } +} 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()); + + } + +} From baa8c7fc709f6c829c1182c2bac1b12c377c8345 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Mon, 18 Feb 2019 22:36:23 +0100 Subject: [PATCH 63/70] Release 0.2.6 --- CHANGES.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e4716b1..31137df 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -# 0.2.6 (UNRELEASED) +# 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) diff --git a/pom.xml b/pom.xml index 816c673..8f060ee 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ es.indaba lightcouch - 0.2.5 + 0.2.6 jar LightCouch CouchDB Java API From 0ae381d2eb3b5ad6f51a80b889dfc3e2e66648f9 Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Wed, 13 Mar 2019 22:09:18 +0100 Subject: [PATCH 64/70] Update testing environment to CouchDB 2.3.1 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 94637fb..43bebb5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,8 +15,8 @@ jobs: include: - stage: Build and Test on CouchDB 2.3.x install: - - docker pull couchdb:2.3.0 - - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.3.0 + - 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 From 48ca10ba984ad60d37c01a047ef777b50340a0ab Mon Sep 17 00:00:00 2001 From: Yongliang Zhan Date: Mon, 13 May 2019 09:35:49 +0200 Subject: [PATCH 65/70] Add ETag management support. (#23) Define method findIfNotModified which sends header "If-None-Match" which throws a "DocumentNotModifiedException" if the document has not changed since specified revision. --- .../org/lightcouch/CouchDbClientBase.java | 1682 +++++++++-------- .../DocumentNotModifiedException.java | 37 + .../org/lightcouch/tests/ETagSupportTest.java | 57 + 3 files changed, 961 insertions(+), 815 deletions(-) create mode 100644 src/main/java/org/lightcouch/DocumentNotModifiedException.java create mode 100644 src/test/java/org/lightcouch/tests/ETagSupportTest.java diff --git a/src/main/java/org/lightcouch/CouchDbClientBase.java b/src/main/java/org/lightcouch/CouchDbClientBase.java index 5d2dcc0..026e4c9 100644 --- a/src/main/java/org/lightcouch/CouchDbClientBase.java +++ b/src/main/java/org/lightcouch/CouchDbClientBase.java @@ -1,815 +1,867 @@ -/* - * 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 - * - * 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.assertNull; -import static org.lightcouch.CouchDbUtil.close; -import static org.lightcouch.CouchDbUtil.generateUUID; -import static org.lightcouch.CouchDbUtil.getAsString; -import static org.lightcouch.CouchDbUtil.getStream; -import static org.lightcouch.CouchDbUtil.streamToString; -import static org.lightcouch.URIBuilder.buildUri; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.UnsupportedEncodingException; -import java.lang.reflect.Type; -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.HttpHost; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpHead; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpRequestBase; -import org.apache.http.entity.InputStreamEntity; -import org.apache.http.entity.StringEntity; -import org.apache.http.protocol.HttpContext; -import org.apache.http.util.EntityUtils; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonParser; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; -import com.google.gson.reflect.TypeToken; - -/** - * 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; - 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. - * - * @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 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); - } - 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(); - } -} +/* + * 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 + * + * 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.assertNull; +import static org.lightcouch.CouchDbUtil.close; +import static org.lightcouch.CouchDbUtil.generateUUID; +import static org.lightcouch.CouchDbUtil.getAsString; +import static org.lightcouch.CouchDbUtil.getStream; +import static org.lightcouch.CouchDbUtil.streamToString; +import static org.lightcouch.URIBuilder.buildUri; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Type; +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; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +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; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.reflect.TypeToken; + +/** + * 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; + 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/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/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."); + } + } +} From ba2c0cae715d28bf13554ef018fbb91461043443 Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Tue, 20 Aug 2019 10:31:54 +0200 Subject: [PATCH 66/70] Update version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f210aa6..e158691 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,6 @@ The releases of this fork are published via Jitpack com.github.IndabaConsultores lightCouch - 0.2.5 + 0.2.6 ``` From c65454f7851681f527d791ac6eefd221c914a06a Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Tue, 11 Feb 2020 22:30:13 +0100 Subject: [PATCH 67/70] Prepare testing support for upcoming CouchDB 3.x release --- pom.xml | 162 +++++++++--------- .../org/lightcouch/tests/CouchDbTestBase.java | 15 +- 2 files changed, 94 insertions(+), 83 deletions(-) diff --git a/pom.xml b/pom.xml index 8f060ee..ffc2511 100644 --- a/pom.xml +++ b/pom.xml @@ -1,83 +1,89 @@ - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 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 - + 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 - - + + + 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 - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - 1.5 - 1.5 - - - - + + 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.5 + 1.5 + + + + diff --git a/src/test/java/org/lightcouch/tests/CouchDbTestBase.java b/src/test/java/org/lightcouch/tests/CouchDbTestBase.java index 2840a74..d18651f 100644 --- a/src/test/java/org/lightcouch/tests/CouchDbTestBase.java +++ b/src/test/java/org/lightcouch/tests/CouchDbTestBase.java @@ -4,6 +4,8 @@ import org.junit.BeforeClass; import org.lightcouch.CouchDbClient; +import com.github.zafarkhaja.semver.Version; + public class CouchDbTestBase { protected static CouchDbClient dbClient; @@ -22,17 +24,20 @@ public static void tearDownClass() { } protected boolean isCouchDB23() { - String version = dbClient.context().serverVersion(); - return version.startsWith("2.3"); + return isCouchDBVersion(">=2.3.0"); } protected boolean isCouchDB2() { - String version = dbClient.context().serverVersion(); - return version.startsWith("2"); + 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(); - return version.startsWith("0") || version.startsWith("1") ; + Version serverVersion = Version.valueOf(version); + return serverVersion.satisfies(versionExpression); } } From c4ebe85c60151ab737942ec4da6f5e01edeced90 Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Sat, 29 Feb 2020 00:09:52 +0100 Subject: [PATCH 68/70] Test using CouchDB 3.0.0 --- .travis.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.travis.yml b/.travis.yml index 43bebb5..c17e52d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,16 @@ services: 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: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.3.x install: - docker pull couchdb:2.3.1 From bc7c25e56be49a545ad1e151b72c08d66ebb09c4 Mon Sep 17 00:00:00 2001 From: Juan Jose Rodriguez Date: Thu, 12 Mar 2020 17:56:22 +0100 Subject: [PATCH 69/70] Move to java 1.6 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ffc2511..df3316e 100644 --- a/pom.xml +++ b/pom.xml @@ -80,8 +80,8 @@ maven-compiler-plugin 3.1 - 1.5 - 1.5 + 1.6 + 1.6 From a3874c2fc266b6850cd84565ab65984e69ae3ade Mon Sep 17 00:00:00 2001 From: Juanjo Rodriguez Date: Tue, 14 Apr 2020 10:22:28 +0200 Subject: [PATCH 70/70] Add support for testing with CouchDB 3.0.0 (#25) * Add Couchdb 3.0.0 test environment Co-authored-by: Juan Jose Rodriguez --- .travis.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 43bebb5..200c26a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,32 +13,32 @@ services: jobs: include: - - stage: Build and Test on CouchDB 2.3.x + - stage: Build and Test on CouchDB 3.0.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 + - 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" - - stage: Build and Test on CouchDB 2.2.x + 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.2.0 - - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.2.0 + - 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" - script: - - mvn org.jacoco:jacoco-maven-plugin:prepare-agent test sonar:sonar - - stage: Build and Test on CouchDB 2.1.x + - stage: Build and Test on CouchDB 2.2.x install: - - docker pull couchdb:2.1.2 - - docker run -d -p 127.0.0.1:5984:5984 -e COUCHDB_USER=couchdb -e COUCHDB_PASSWORD=couchdb couchdb:2.1.2 + - 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