Skip to content
This repository was archived by the owner on Mar 11, 2022. It is now read-only.

Commit 597d0ff

Browse files
authored
Merge pull request #505 from cloudant/416-create-with-history
416 create with history
2 parents 015e6d8 + b02a1ba commit 597d0ff

File tree

20 files changed

+1289
-146
lines changed

20 files changed

+1289
-146
lines changed

CHANGES.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131
- [NOTE] The "CRUD Guide" markdown document (previously located in
3232
`doc/crud.md`) has been migrated to a
3333
[java source file](https://github.com/cloudant/sync-android/blob/2.0.0/doc/CrudSamples.java).
34-
34+
35+
- [NEW] `databaseWithAdvancedAPIs()` getter on `DocumentStore` for specialist advanced use cases.
36+
Adds support for creating specific document revisions with history.
37+
3538
- [FIXED] Issue with double encoding of restricted URL characters in credentials when using
3639
`ReplicatorBuilder`.
3740

cloudant-sync-datastore-core/src/main/java/com/cloudant/sync/documentstore/Database.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,13 @@ DocumentRevision create(DocumentRevision rev) throws AttachmentException,
287287
* @param rev the {@link DocumentRevision} to be updated
288288
* @return a {@link DocumentRevision} - the updated document
289289
* @throws ConflictException if <code>rev</code> is not a current revision for this document
290+
* @throws DocumentNotFoundException if the {@code rev} being updated does not exist
290291
* @throws AttachmentException if there was an error saving any new attachments
291292
* @throws DocumentStoreException if there was an error reading from or writing to the database
292293
* @see Database#getEventBus()
293294
*/
294295
DocumentRevision update(DocumentRevision rev) throws ConflictException,
295-
AttachmentException, DocumentStoreException;
296+
AttachmentException, DocumentStoreException, DocumentNotFoundException;
296297

297298
/**
298299
* <p>Deletes a document from the datastore.</p>

cloudant-sync-datastore-core/src/main/java/com/cloudant/sync/documentstore/DocumentRevision.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ public boolean isDeleted() {
105105
return deleted;
106106
}
107107

108+
/**
109+
* To delete a document it is preferable to call {@link Database#delete(DocumentRevision)}.
110+
* Some use cases need to do a delete via an update in which case it is possible to
111+
* call this method, followed by {@link Database#update(DocumentRevision)}.
112+
*
113+
* This method sets the document revision's {@code _deleted} flag to true and removes the body
114+
* and attachments from the document revision.
115+
*
116+
*/
117+
public void setDeleted() {
118+
this.deleted = true;
119+
this.setBody(DocumentBodyFactory.EMPTY);
120+
}
121+
108122
public Map<String, Attachment> getAttachments() {
109123
return attachments;
110124
}

cloudant-sync-datastore-core/src/main/java/com/cloudant/sync/documentstore/DocumentStore.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@
1616

1717
import com.cloudant.sync.documentstore.encryption.KeyProvider;
1818
import com.cloudant.sync.documentstore.encryption.NullKeyProvider;
19-
import com.cloudant.sync.internal.documentstore.DatabaseImpl;
2019
import com.cloudant.sync.event.EventBus;
2120
import com.cloudant.sync.event.notifications.DocumentStoreClosed;
2221
import com.cloudant.sync.event.notifications.DocumentStoreCreated;
2322
import com.cloudant.sync.event.notifications.DocumentStoreDeleted;
24-
import com.cloudant.sync.event.notifications.DocumentStoreOpened;
2523
import com.cloudant.sync.event.notifications.DocumentStoreModified;
26-
import com.cloudant.sync.query.Query;
24+
import com.cloudant.sync.event.notifications.DocumentStoreOpened;
25+
import com.cloudant.sync.internal.documentstore.DatabaseImpl;
2726
import com.cloudant.sync.internal.query.QueryImpl;
27+
import com.cloudant.sync.query.Query;
2828

2929
import org.apache.commons.io.FileUtils;
3030

@@ -68,7 +68,7 @@ public class DocumentStore {
6868

6969
private static final String EXTENSIONS_LOCATION_NAME = "extensions";
7070

71-
private final Database database;
71+
private final DatabaseImpl database;
7272
private final Query query;
7373
protected final String databaseName; // only used for events
7474
private final File location; // needed for close/delete
@@ -187,6 +187,18 @@ public Database database() {
187187
return database;
188188
}
189189

190+
/**
191+
* WARNING: accessing APIs exposed on the
192+
* {@link com.cloudant.sync.documentstore.advanced.Database} class returned by this method is
193+
* not required for typical use cases. Refer to the javadoc in the
194+
* {@link com.cloudant.sync.documentstore.advanced} package before using this.
195+
*
196+
* @return interface to advanced database methods
197+
*/
198+
public com.cloudant.sync.documentstore.advanced.Database advanced() {
199+
return database;
200+
}
201+
190202
/**
191203
* <p>
192204
* Get a reference to the {@link Query} object.
@@ -208,7 +220,7 @@ public void close() {
208220
synchronized (documentStores) {
209221
DocumentStore ds = documentStores.remove(location);
210222
if (ds != null) {
211-
((DatabaseImpl)database).close();
223+
database.close();
212224
((QueryImpl)query).close();
213225
}
214226
else {
@@ -255,7 +267,7 @@ public static EventBus getEventBus() {
255267

256268
private void closeQuietlyOnException() {
257269
if (database != null) {
258-
((DatabaseImpl) database).close();
270+
database.close();
259271
}
260272
if (query != null) {
261273
((QueryImpl) query).close();
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package com.cloudant.sync.documentstore.advanced;
2+
3+
import com.cloudant.sync.documentstore.DocumentException;
4+
import com.cloudant.sync.documentstore.DocumentRevision;
5+
6+
import java.util.List;
7+
import java.util.Map;
8+
9+
/**
10+
* <P>
11+
* ⚠ Database API methods for advanced use cases.
12+
* </P>
13+
* <P>
14+
* ⚠ Interacting with these methods is not required for the typical use cases. Use with extreme
15+
* caution.
16+
* </P>
17+
*/
18+
public interface Database {
19+
20+
/**
21+
* <P>
22+
* ⚠ Creates a new revision in the database with the specified revision history.
23+
* </P>
24+
* <P>
25+
* This is equivalent to inserting a revision with {@code new_edits: false}, functionality that
26+
* is typically only required by replicators.
27+
* </P>
28+
* <P>
29+
* Note that the revisionsStart and revisionsIDs parameters describe the revision history of the
30+
* document revision being inserted. This history is equivalent to getting the document from
31+
* CouchDB with the {@code revs=true} parameter.
32+
* An example document retrieved with this _revisions history would be:
33+
* </P>
34+
* <pre>
35+
* {@code
36+
* {"_id”:”exampledoc1”,
37+
* "_rev":"4-51aa94e4b0ef37271082033bba52b850",
38+
* "_revisions":
39+
* {"start":4,
40+
* "ids": [“51aa94e4b0ef37271082033bba52b850",
41+
* "f9fb951ca8dadec1459450156b2205cf",
42+
* "617a372bba833d7acf3ccf2e7dece15a",
43+
* "967a00dff5e02add41819138abb3284d"]
44+
* },
45+
* ... // other document properties
46+
* }
47+
* }
48+
* </pre>
49+
* <P>
50+
* Example usage to insert this example document:
51+
* </P>
52+
* <pre>
53+
* {@code
54+
* DocumentRevision documentRevision = new DocumentRevision("exampledoc1",
55+
* "4-51aa94e4b0ef37271082033bba52b850");
56+
* // set document content etc
57+
* documentRevision.setBody(...)
58+
*
59+
* // set attachments if necessary, see note on attachments below.
60+
*
61+
* documentStore.advanced()
62+
* .createWithHistory(documentRevision, 4, Arrays.asList(“51aa94e4b0ef37271082033bba52b850",
63+
* "f9fb951ca8dadec1459450156b2205cf",
64+
* "617a372bba833d7acf3ccf2e7dece15a",
65+
* "967a00dff5e02add41819138abb3284d"))
66+
* }
67+
* </pre>
68+
* <P>
69+
* If the document is deleted then the inserted revision also needs to be made deleted as in
70+
* this example:
71+
* </P>
72+
* <pre>
73+
* {@code
74+
* DocumentRevision documentRevision = new DocumentRevision("exampledoc1",
75+
* "4-51aa94e4b0ef37271082033bba52b850");
76+
* // set document as deleted
77+
* documentRevision.setDeleted()
78+
*
79+
* documentStore.advanced()
80+
* .createWithHistory(documentRevision, 4, Arrays.asList(“51aa94e4b0ef37271082033bba52b850",
81+
* "f9fb951ca8dadec1459450156b2205cf",
82+
* "617a372bba833d7acf3ccf2e7dece15a",
83+
* "967a00dff5e02add41819138abb3284d"))
84+
* }
85+
* </pre>
86+
* <P>
87+
* <B>Attachments</B>
88+
* </P>
89+
* <P>
90+
* Note it is the caller's responsibility to correctly set the attachments on the revision to be
91+
* created. The existing attachments should be compared to the attachments metadata present in
92+
* the JSON of the document revision to be created and an appropriate call made to
93+
* {@link DocumentRevision#setAttachments(Map)} to transfer that metadata and any new attachment
94+
* files to the {@code DocumentRevision} object. If a comparison of the JSON attachment metadata
95+
* to the existing attachments indicates that no new attachments are being added then it is
96+
* possible to simply set the existing attachment map like this:
97+
* </P>
98+
* <pre>
99+
* {@code
100+
* // Get the existing attachments from the current revision
101+
* Map<String, Attachment> atts = database.read("exampledoc1").getAttachments();
102+
* // Set the attachment metadata on the DocumentRevision object that will be passed to
103+
* // createWithHistory.
104+
* documentRevision.setAttachments(atts);
105+
* }
106+
* </pre>
107+
* <P>
108+
* If new attachments need to be added, then the normal CRUD API for creating or updating
109+
* attachments should be used to create the attachments on the revision before calling
110+
* createWithHistory, for example, using
111+
* {@link com.cloudant.sync.documentstore.UnsavedFileAttachment}.
112+
* </P>
113+
* <P>
114+
* If no attachments are set then attachments on ancestor revisions in the existing tree will
115+
* be deleted on the newly created revision, as it is equivalent to performing an update with an
116+
* empty attachment map. It is essential to check for existing attachments and set them
117+
* appropriately on the DocumentRevision if you want to preserve attachments when calling
118+
* createWithHistory.
119+
* </P>
120+
*
121+
* @param documentRevision the revision of the document to create in the database
122+
* @param revisionsStart the generation ID of the revision being inserted, for
123+
* example obtained from the {@code start} property of
124+
* the {@code _revisions} property object of a document
125+
* obtained with {@code revs=true}
126+
* @param revisionsIDs the sequence of revision IDs (excluding the generational prefix)
127+
* starting with the revision being inserted and progressing to the root
128+
* of the document, for example a list of the JSON array content of the
129+
* {@code ids} property of the {@code _revisions} property object of a
130+
* document obtained with {@code revs=true}
131+
* @throws DocumentException if there was an error inserting the revision or its attachments
132+
* into the database
133+
*/
134+
void createWithHistory(DocumentRevision documentRevision, int revisionsStart, List<String>
135+
revisionsIDs) throws DocumentException;
136+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright © 2017 IBM Corp. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5+
* except in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11+
* either express or implied. See the License for the specific language governing permissions
12+
* and limitations under the License.
13+
*/
14+
15+
/**
16+
* <P>
17+
* ⚠ The interfaces in this package are not needed for typical use of the sync library.
18+
* </P>
19+
* <P>
20+
* ⚠ These interfaces are provided for flexibility to allow interactions with the database that
21+
* would otherwise be prevented by the API, but are necessary for some use cases. An example would
22+
* be writing your own replicator for this library's DocumentStore.
23+
* </P>
24+
* <P>
25+
* ⚠ Use of the APIs in this package is beyond the intended use case of the sync library and by
26+
* their nature they may expose untested edge cases. Incorrect use of the APIs could result in data
27+
* loss or corruption. You should only use these interfaces if you really know what you are doing!
28+
* </P>
29+
*/
30+
package com.cloudant.sync.documentstore.advanced;

cloudant-sync-datastore-core/src/main/java/com/cloudant/sync/internal/common/CouchUtils.java

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818

1919
import com.cloudant.sync.internal.util.Misc;
2020

21+
import java.util.ArrayList;
22+
import java.util.Collections;
2123
import java.util.List;
22-
import java.util.Map;
2324
import java.util.UUID;
2425

2526
public class CouchUtils {
@@ -48,23 +49,23 @@ public static boolean isValidDocumentId(String docId) {
4849
return true;
4950
}
5051

51-
/*
52-
* Parses the _revisions dict from a document into an array of revision ID strings
52+
/**
53+
* Get an ascending order revision list from the couch style {@code _revisions} history of
54+
* {@code start} and hash {@code ids}.
55+
*
56+
* @param start the starting generation
57+
* @param ids the list of ID hashes
58+
* @return
5359
*/
54-
public static List<String> parseCouchDBRevisionHistory(Map<String, Object> docProperties) {
55-
Map<String, Object> revisions = (Map<String, Object>) docProperties.get(CouchConstants._revisions);
56-
if (revisions == null) {
57-
return null;
60+
public static List<String> couchStyleRevisionHistoryToFullRevisionIDs(final int start, List<String> ids) {
61+
List<String> revisionHistory = new ArrayList<String>();
62+
int generation = start;
63+
for (String revIdHash : ids) {
64+
revisionHistory.add(generation + "-" + revIdHash);
65+
generation--;
5866
}
59-
List<String> revIDs = (List<String>) revisions.get(CouchConstants.ids);
60-
Integer start = (Integer) revisions.get(CouchConstants.start);
61-
if (start != null) {
62-
for (int i = 0; i < revIDs.size(); i++) {
63-
String revID = revIDs.get(i);
64-
revIDs.set(i, Integer.toString(start--) + "-" + revID);
65-
}
66-
}
67-
return revIDs;
67+
Collections.reverse(revisionHistory);
68+
return revisionHistory;
6869
}
6970

7071
public static String getFirstLocalDocRevisionId() {

0 commit comments

Comments
 (0)