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

Commit 5f3f2c1

Browse files
authored
read, contains, create, delete for local docs (#591)
* `read`, `contains`, `create`, `delete` for local docs * Fix markup * Fix NPE * `delete(final String id)` deletes local documents Also fix bug in `delete(final DocumentRevision rev)` where `DocumentNotFoundException` was not being thrown for non-existent local documents. * Fix javadoc * Clarify javadoc in deprecation notics * `Update` also creates/updates local documents
1 parent 32c98a2 commit 5f3f2c1

File tree

5 files changed

+390
-26
lines changed

5 files changed

+390
-26
lines changed

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 2.4.0 (Unreleased)
2+
- [NEW] `Database` methods `read`, `contains`, `create`, and `delete` now accept local
3+
(non-replicating documents). These documents must have their document ID prefixed with `_local/`
4+
and must have their revision ID set to `null` (where applicable).
5+
16
# 2.3.0 (2018-08-14)
27
- [NEW] Added API for specifying a list of document IDs in the filtered pull replicator.
38
- [IMPROVED] Forced a TLS1.2 `SSLSocketFactory` where possible on Android API versions < 20 (it is

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

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2016 IBM Corp. All rights reserved.
2+
* Copyright © 2016, 2018 IBM Corp. All rights reserved.
33
*
44
* Original iOS version by Jens Alfke, ported to Android by Marty Schoch
55
* Copyright © 2012 Couchbase, Inc. All rights reserved.
@@ -73,7 +73,10 @@ public interface Database {
7373
File getPath();
7474

7575
/**
76-
* <p>Returns the current winning revision of a document.</p>
76+
* <p>Returns the current winning revision of a document; or returns the given
77+
* <a href="https://couchdb.readthedocs.io/en/stable/api/local.html">local document</a>,
78+
* if {@code documentId} is prefixed with {@code _local/}.
79+
* </p>
7780
*
7881
* <p>Previously deleted documents can be retrieved
7982
* (via tombstones, see {@link Database#delete(DocumentRevision)})
@@ -109,7 +112,9 @@ DocumentRevision read(String documentId, String revisionId) throws
109112

110113
/**
111114
* <p>Returns whether this DocumentStore contains a particular revision of
112-
* a document.</p>
115+
* a document; or contains the given
116+
* <a href="https://couchdb.readthedocs.io/en/stable/api/local.html">local document</a>,
117+
* if {@code documentId} is prefixed with {@code _local/}.</p>
113118
*
114119
* <p>{@code true} will still be returned if the document is deleted.</p>
115120
*
@@ -122,7 +127,10 @@ DocumentRevision read(String documentId, String revisionId) throws
122127
boolean contains(String documentId, String revisionId) throws DocumentStoreException;
123128

124129
/**
125-
* <p>Returns whether this DocumentStore contains any revisions of a document.
130+
* <p>Returns whether this DocumentStore contains any revisions of a document; or contains the
131+
* given
132+
* <a href="https://couchdb.readthedocs.io/en/stable/api/local.html">local document</a>,
133+
* if {@code documentId} is prefixed with {@code _local/}.
126134
* </p>
127135
*
128136
* <p>{@code true} will still be returned if the document is deleted.</p>
@@ -195,6 +203,7 @@ DocumentRevision read(String documentId, String revisionId) throws
195203

196204
/**
197205
* <p>Return the number of documents in the DocumentStore</p>
206+
* <p><b>Note:</b> this excludes local documents.</p>
198207
*
199208
* @return number of non-deleted documents in DocumentStore
200209
* @throws DocumentStoreException if there was an error reading from the database.
@@ -253,7 +262,10 @@ void resolveConflicts(String docId, ConflictResolver resolver)
253262
throws ConflictException;
254263

255264
/**
256-
* <p>Adds a new document with body and attachments from <code>rev</code>.</p>
265+
* <p>Adds a new document with body and attachments from <code>rev</code>; or creates or updates
266+
* a <a href="https://couchdb.readthedocs.io/en/stable/api/local.html">local document</a>,
267+
* if {@code documentId} is prefixed with {@code _local/}.
268+
* </p>
257269
*
258270
* <p>If the ID in <code>rev</code> is null, the document's ID will be auto-generated,
259271
* and can be found by inspecting the returned {@code DocumentRevision}.</p>
@@ -275,8 +287,9 @@ DocumentRevision create(DocumentRevision rev) throws AttachmentException,
275287

276288
/**
277289
* <p>Updates a document that exists in the DocumentStore with with body and attachments
278-
* from <code>rev</code>.
279-
* </p>
290+
* from <code>rev</code>; or creates or updates
291+
* a <a href="https://couchdb.readthedocs.io/en/stable/api/local.html">local document</a>,
292+
* if {@code documentId} is prefixed with {@code _local/}.
280293
*
281294
* <p>{@code rev} must be a current revision for this document.</p>
282295
*
@@ -296,10 +309,14 @@ DocumentRevision update(DocumentRevision rev) throws ConflictException,
296309
AttachmentException, DocumentStoreException, DocumentNotFoundException;
297310

298311
/**
299-
* <p>Deletes a document from the DocumentStore.</p>
312+
* <p>Deletes a document from the DocumentStore; or delete a
313+
* <a href="https://couchdb.readthedocs.io/en/stable/api/local.html">local document</a>,
314+
* if {@code documentId} is prefixed with {@code _local/}.</p>
300315
*
301316
* <p>This operation leaves a "tombstone" for the deleted document, so that
302-
* future replication operations can successfully replicate the deletion.
317+
* future replication operations can successfully replicate the deletion. Note that local
318+
* documents do not have tombstones since they do not have a revision history; they are
319+
* immediately removed from the database.
303320
* </p>
304321
*
305322
* <p>If the document is successfully deleted, a
@@ -326,7 +343,15 @@ DocumentRevision delete(DocumentRevision rev) throws ConflictException, Document
326343
DocumentStoreException;
327344

328345
/**
329-
* <p>Delete all leaf revisions for the document</p>
346+
* <p>Delete all leaf revisions for the document; or delete a
347+
* <a href="https://couchdb.readthedocs.io/en/stable/api/local.html">local document</a>,
348+
* if {@code documentId} is prefixed with {@code _local/}.</p>
349+
*
350+
* <p>This operation leaves a "tombstone" for each deleted document, so that
351+
* future replication operations can successfully replicate the deletion. Note that local
352+
* documents do not have tombstones since they do not have a revision history; they are
353+
* immediately removed from the database.
354+
* </p>
330355
*
331356
* <p>This is equivalent to calling
332357
* {@link Database#delete(DocumentRevision)
@@ -335,10 +360,9 @@ DocumentRevision delete(DocumentRevision rev) throws ConflictException, Document
335360
* @param id the ID of the document to delete leaf nodes for
336361
* @return a List of a {@link DocumentRevision}s - the deleted or "tombstone" documents
337362
* @throws DocumentStoreException if there was an error reading from or writing to the database
338-
* @see Database#getEventBus()
339363
* @see Database#delete(DocumentRevision)
340364
*/
341-
List<DocumentRevision> delete(String id) throws DocumentStoreException;
365+
List<DocumentRevision> delete(String id) throws DocumentNotFoundException, DocumentStoreException;
342366

343367
/**
344368
* Compacts the SQL database and disk storage by removing the bodies and attachments of obsolete revisions.

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2015 IBM Corp. All rights reserved.
2+
* Copyright © 2015, 2018 IBM Corp. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
55
* except in compliance with the License. You may obtain a copy of the License at
@@ -15,8 +15,16 @@
1515
package com.cloudant.sync.documentstore;
1616

1717
/**
18-
* A local Document. {@code LocalDocument}s do not have a history, or the concept of revisions
18+
* <p>
19+
* <b>Note:</b> this class is deprecated and will be moved to an internal package in a future
20+
* release. For local documents use a {@link DocumentRevision} with an {@code id} prefixed with
21+
* {@code _local} and a {@code rev} set to {@code null}.
22+
* </p>
23+
* <p>
24+
* A local Document. {@code LocalDocument}s do not have a history, or the concept of revisions
25+
* </p>
1926
*/
27+
@Deprecated
2028
public class LocalDocument {
2129

2230
/**

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

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2016, 2017 IBM Corp. All rights reserved.
2+
* Copyright © 2016, 2018 IBM Corp. All rights reserved.
33
*
44
* Original iOS version by Jens Alfke, ported to Android by Marty Schoch
55
* Copyright © 2012 Couchbase, Inc. All rights reserved.
@@ -38,6 +38,7 @@
3838
import com.cloudant.sync.event.notifications.DocumentDeleted;
3939
import com.cloudant.sync.event.notifications.DocumentModified;
4040
import com.cloudant.sync.event.notifications.DocumentUpdated;
41+
import com.cloudant.sync.internal.common.CouchConstants;
4142
import com.cloudant.sync.internal.common.CouchUtils;
4243
import com.cloudant.sync.internal.common.ValueListMap;
4344
import com.cloudant.sync.internal.documentstore.callables.ChangesCallable;
@@ -242,9 +243,16 @@ public InternalDocumentRevision read(final String id, final String rev) throws
242243
DocumentNotFoundException, DocumentStoreException {
243244
Misc.checkState(this.isOpen(), "Database is closed");
244245
Misc.checkNotNullOrEmpty(id, "Document id");
245-
246246
try {
247-
return get(queue.submit(new GetDocumentCallable(id, rev, this.attachmentsDir, this.attachmentStreamFactory)));
247+
if (id.startsWith(CouchConstants._local_prefix)) {
248+
Misc.checkArgument(rev == null, "Local documents must have a null revision ID");
249+
String localId = id.substring(CouchConstants._local_prefix.length());
250+
LocalDocument ld = get(queue.submit(new GetLocalDocumentCallable(localId)));
251+
// convert to DocumentRevision, adding back "_local/" prefix which was stripped off when document was written
252+
return new DocumentRevisionBuilder().setDocId(CouchConstants._local_prefix + ld.docId).setBody(ld.body).build();
253+
} else {
254+
return get(queue.submit(new GetDocumentCallable(id, rev, this.attachmentsDir, this.attachmentStreamFactory)));
255+
}
248256
} catch (ExecutionException e) {
249257
throwCauseAs(e, DocumentNotFoundException.class);
250258
String message = String.format(Locale.ENGLISH, "Failed to get document id %s at revision %s", id, rev);
@@ -879,13 +887,28 @@ public DocumentRevision create(final DocumentRevision rev)
879887
Misc.checkArgument(rev.isFullRevision(), "Projected revisions cannot be used to " +
880888
"create documents");
881889
final String docId;
890+
882891
// create docid if docid is null
883892
if (rev.getId() == null) {
884893
docId = CouchUtils.generateDocumentId();
885894
} else {
886895
docId = rev.getId();
887896
}
888897

898+
// check to see if we are creating a local (non-replicating) document
899+
if (docId.startsWith(CouchConstants._local_prefix)) {
900+
String localId = docId.substring(CouchConstants._local_prefix.length());
901+
try {
902+
insertLocalDocument(localId, rev.getBody());
903+
// we can return the input document as-is since there was no doc id or rev id to generate
904+
return rev;
905+
} catch (DocumentException e) {
906+
throw new DocumentStoreException(e.getMessage(), e.getCause());
907+
} finally {
908+
eventBus.post(new DocumentCreated(rev));
909+
}
910+
}
911+
889912
// We need to work out which of the attachments for the revision are ones
890913
// we can copy over because they exist in the attachment store already and
891914
// which are new, that we need to prepare for insertion.
@@ -942,6 +965,11 @@ public DocumentRevision update(final DocumentRevision rev)
942965
Misc.checkArgument(rev.isFullRevision(), "Projected revisions cannot be used to " +
943966
"create documents");
944967

968+
// Shortcut if this is a create/update of local doc
969+
if (rev.getId().startsWith(CouchConstants._local_prefix)) {
970+
return create(rev);
971+
}
972+
945973
// Shortcut if this is a deletion
946974
if (rev.isDeleted()) {
947975
return delete(rev);
@@ -993,17 +1021,28 @@ public DocumentRevision delete(final DocumentRevision rev) throws
9931021
ConflictException, DocumentNotFoundException, DocumentStoreException {
9941022
Misc.checkNotNull(rev, "DocumentRevision");
9951023
Misc.checkState(isOpen(), "Datastore is closed");
996-
9971024
try {
998-
InternalDocumentRevision deletedRevision = get(queue.submit(new DeleteDocumentCallable(rev.getId(), rev.getRevision())));
999-
if (deletedRevision != null) {
1000-
eventBus.post(new DocumentDeleted(rev, deletedRevision));
1025+
// local documents
1026+
if (rev.getId().startsWith(CouchConstants._local_prefix)) {
1027+
Misc.checkArgument(rev.getRevision() == null, "Local documents must have a null revision ID");
1028+
String localId = rev.getId().substring(CouchConstants._local_prefix.length());
1029+
deleteLocalDocument(localId);
1030+
// for local documents there is no "new document" to post on the event bus or return as
1031+
// the document is removed rather than updated with a tombstone
1032+
eventBus.post(new DocumentDeleted(rev, null));
1033+
return null;
1034+
} else {
1035+
// "normal" documents
1036+
InternalDocumentRevision deletedRevision = get(queue.submit(new DeleteDocumentCallable(rev.getId(), rev.getRevision())));
1037+
if (deletedRevision != null) {
1038+
eventBus.post(new DocumentDeleted(rev, deletedRevision));
1039+
}
1040+
return deletedRevision;
10011041
}
1002-
return deletedRevision;
10031042
} catch (ExecutionException e) {
10041043
// conflictexception if source revision isn't current rev
10051044
throwCauseAs(e, ConflictException.class);
1006-
// documentnotfoundexception if it's already deleted
1045+
// documentnotfoundexception if it's already deleted, or was a non-existent local document
10071046
throwCauseAs(e, DocumentNotFoundException.class);
10081047
String message = "Failed to delete document";
10091048
logger.log(Level.SEVERE, message, e);
@@ -1014,11 +1053,21 @@ public DocumentRevision delete(final DocumentRevision rev) throws
10141053
// delete all leaf nodes
10151054
@Override
10161055
public List<DocumentRevision> delete(final String id)
1017-
throws DocumentStoreException {
1056+
throws DocumentNotFoundException, DocumentStoreException {
10181057
Misc.checkNotNull(id, "ID");
10191058
try {
1020-
return get(queue.submitTransaction(new DeleteAllRevisionsCallable(id)));
1059+
if (id.startsWith(CouchConstants._local_prefix)) {
1060+
String localId = id.substring(CouchConstants._local_prefix.length());
1061+
deleteLocalDocument(localId);
1062+
// for local documents there is no "new document" to return as the document is
1063+
// removed rather than updated with a tombstone
1064+
return Collections.singletonList(null);
1065+
} else {
1066+
return get(queue.submitTransaction(new DeleteAllRevisionsCallable(id)));
1067+
}
10211068
} catch (ExecutionException e) {
1069+
// documentnotfoundexception if it was a non-existent local document
1070+
throwCauseAs(e, DocumentNotFoundException.class);
10221071
String message = "Failed to delete document";
10231072
logger.log(Level.SEVERE, message, e);
10241073
throw new DocumentStoreException(message, e.getCause());
@@ -1085,4 +1134,4 @@ public void createWithHistory(DocumentRevision revision, int revisionsStart, Lis
10851134
forceInsert(Collections.singletonList(new ForceInsertItem(internalRev,
10861135
revIDs, null, preparedAttachments, false)));
10871136
}
1088-
}
1137+
}

0 commit comments

Comments
 (0)