com.couchbase.lite.Document Maven / Gradle / Ivy
package com.couchbase.lite;
import com.couchbase.lite.internal.InterfaceAudience;
import com.couchbase.lite.internal.RevisionInternal;
import com.couchbase.lite.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A CouchbaseLite document.
*/
public class Document {
/**
* The document's owning database.
*/
private Database database;
/**
* The document's ID.
*/
private String documentId;
/**
* The current/latest revision. This object is cached.
*/
private SavedRevision currentRevision;
/**
* Change Listeners
*/
private List changeListeners = Collections.synchronizedList(new ArrayList());
/**
* Constructor
*
* @param database The document's owning database
* @param documentId The document's ID
* @exclude
*/
@InterfaceAudience.Private
public Document(Database database, String documentId) {
this.database = database;
this.documentId = documentId;
}
@InterfaceAudience.Private
public static boolean isValidDocumentId(String id) {
// http://wiki.apache.org/couchdb/HTTP_Document_API#Documents
if (id == null || id.length() == 0) {
return false;
}
if (id.charAt(0) == '_') {
return (id.startsWith("_design/"));
}
return true;
// "_local/*" is not a valid document ID. Local docs have their own API and shouldn't get here.
}
/**
* Get the document's owning database.
*/
@InterfaceAudience.Public
public Database getDatabase() {
return database;
}
/**
* Get the document's ID
*/
@InterfaceAudience.Public
public String getId() {
return documentId;
}
/**
* Is this document deleted? (That is, does its current revision have the '_deleted' property?)
*
* @return boolean to indicate whether deleted or not
*/
@InterfaceAudience.Public
public boolean isDeleted() {
try {
return getCurrentRevision() == null && getLeafRevisions().size() > 0;
} catch (CouchbaseLiteException e) {
throw new RuntimeException(e);
}
}
/**
* Get the ID of the current revision
*/
@InterfaceAudience.Public
public String getCurrentRevisionId() {
SavedRevision rev = getCurrentRevision();
if (rev == null) {
return null;
}
return rev.getId();
}
/**
* Get the current revision
*/
@InterfaceAudience.Public
public SavedRevision getCurrentRevision() {
if (currentRevision == null)
currentRevision = getRevision(null);
return currentRevision;
}
/**
* Returns the document's history as an array of CBLRevisions. (See SavedRevision's method.)
*
* @return document's history
* @throws CouchbaseLiteException
*/
@InterfaceAudience.Public
public List getRevisionHistory() throws CouchbaseLiteException {
if (getCurrentRevision() == null) {
Log.w(Database.TAG, "getRevisionHistory() called but no currentRevision");
return null;
}
return getCurrentRevision().getRevisionHistory();
}
/**
* Returns all the current conflicting revisions of the document. If the document is not
* in conflict, only the single current revision will be returned.
*
* @return all current conflicting revisions of the document
* @throws CouchbaseLiteException
*/
@InterfaceAudience.Public
public List getConflictingRevisions() throws CouchbaseLiteException {
return getLeafRevisions(false);
}
/**
* Returns all the leaf revisions in the document's revision tree,
* including deleted revisions (i.e. previously-resolved conflicts.)
*
* @return all the leaf revisions in the document's revision tree
* @throws CouchbaseLiteException
*/
@InterfaceAudience.Public
public List getLeafRevisions() throws CouchbaseLiteException {
return getLeafRevisions(true);
}
/**
* The contents of the current revision of the document.
* This is shorthand for self.currentRevision.properties.
* Any keys in the dictionary that begin with "_", such as "_id" and "_rev", contain CouchbaseLite metadata.
*
* @return contents of the current revision of the document.
* null if currentRevision is null
*/
@InterfaceAudience.Public
public Map getProperties() {
SavedRevision currentRevision = getCurrentRevision();
return currentRevision == null ? null : currentRevision.getProperties();
}
/**
* The user-defined properties, without the ones reserved by CouchDB.
* This is based on -properties, with every key whose name starts with "_" removed.
*
* @return user-defined properties, without the ones reserved by CouchDB.
*/
@InterfaceAudience.Public
public Map getUserProperties() {
return getCurrentRevision().getUserProperties();
}
/**
* Deletes this document by adding a deletion revision.
* This will be replicated to other databases.
*
* @return boolean to indicate whether deleted or not
* @throws CouchbaseLiteException
*/
@InterfaceAudience.Public
public boolean delete() throws CouchbaseLiteException {
return getCurrentRevision().deleteDocument() != null;
}
/**
* Purges this document from the database; this is more than deletion, it forgets entirely about it.
* The purge will NOT be replicated to other databases.
*
* @throws CouchbaseLiteException
*/
@InterfaceAudience.Public
public void purge() throws CouchbaseLiteException {
Map> docsToRevs = new HashMap>();
List revs = new ArrayList();
revs.add("*");
docsToRevs.put(documentId, revs);
database.purgeRevisions(docsToRevs);
database.removeDocumentFromCache(this);
}
/**
* The revision with the specified ID.
*
* @param revID the revision ID
* @return the SavedRevision object
*/
@InterfaceAudience.Public
public SavedRevision getRevision(String revID) {
if (revID != null && currentRevision != null && revID.equals(currentRevision.getId()))
return currentRevision;
RevisionInternal revisionInternal = database.getDocument(getId(), revID, true);
return getRevisionFromRev(revisionInternal);
}
/**
* Creates an unsaved new revision whose parent is the currentRevision,
* or which will be the first revision if the document doesn't exist yet.
* You can modify this revision's properties and attachments, then save it.
* No change is made to the database until/unless you save the new revision.
*
* @return the newly created revision
*/
@InterfaceAudience.Public
public UnsavedRevision createRevision() {
return new UnsavedRevision(this, getCurrentRevision());
}
/**
* Shorthand for getProperties().get(key)
*/
@InterfaceAudience.Public
public Object getProperty(String key) {
if (getCurrentRevision() != null &&
getCurrentRevision().getProperties().containsKey(key)) {
return getCurrentRevision().getProperties().get(key);
}
return null;
}
/**
* Saves a new revision. The properties dictionary must have a "_rev" property
* whose ID matches the current revision's (as it will if it's a modified
* copy of this document's .properties property.)
*
* @param properties the contents to be saved in the new revision
* @return a new SavedRevision
*/
@InterfaceAudience.Public
public SavedRevision putProperties(Map properties) throws CouchbaseLiteException {
String prevID = (String) properties.get("_rev");
boolean allowConflict = false;
return putProperties(properties, prevID, allowConflict);
}
/**
* Saves a new revision by letting the caller update the existing properties.
* This method handles conflicts by retrying (calling the block again).
* The DocumentUpdater implementation should modify the properties of the new revision and return YES to save or
* NO to cancel. Be careful: the DocumentUpdater can be called multiple times if there is a conflict!
*
* @param updater the callback DocumentUpdater implementation. Will be called on each
* attempt to save. Should update the given revision's properties and then
* return YES, or just return NO to cancel.
* @return The new saved revision, or null on error or cancellation.
* @throws CouchbaseLiteException
*/
@InterfaceAudience.Public
public SavedRevision update(DocumentUpdater updater) throws CouchbaseLiteException {
int lastErrorCode = Status.UNKNOWN;
do {
// if there is a conflict error, get the latest revision from db instead of cache
if (lastErrorCode == Status.CONFLICT) {
forgetCurrentRevision();
}
UnsavedRevision newRev = createRevision();
if (updater.update(newRev) == false) {
break;
}
try {
SavedRevision savedRev = newRev.save();
if (savedRev != null) {
return savedRev;
}
} catch (CouchbaseLiteException e) {
lastErrorCode = e.getCBLStatus().getCode();
}
} while (lastErrorCode == Status.CONFLICT);
return null;
}
@InterfaceAudience.Public
public void addChangeListener(ChangeListener changeListener) {
changeListeners.add(changeListener);
}
@InterfaceAudience.Public
public void removeChangeListener(ChangeListener changeListener) {
changeListeners.remove(changeListener);
}
/**
* A delegate that can be used to update a Document.
*/
@InterfaceAudience.Public
public static interface DocumentUpdater {
/**
* Document update delegate
*
* @param newRevision the unsaved revision about to be saved
* @return True if the UnsavedRevision should be saved, otherwise false.
*/
public boolean update(UnsavedRevision newRevision);
}
/**
* The type of event raised when a Document changes. This event is not raised in response
* to local Document changes.
*/
@InterfaceAudience.Public
public static class ChangeEvent {
private Document source;
private DocumentChange change;
public ChangeEvent(Document source, DocumentChange documentChange) {
this.source = source;
this.change = documentChange;
}
public Document getSource() {
return source;
}
public DocumentChange getChange() {
return change;
}
}
/**
* A delegate that can be used to listen for Document changes.
*/
@InterfaceAudience.Public
public interface ChangeListener {
void changed(ChangeEvent event);
}
/**
* Get the document's abbreviated ID
*
* @exclude
*/
@InterfaceAudience.Private
public String getAbbreviatedId() {
String abbreviated = documentId;
if (documentId.length() > 10) {
String firstFourChars = documentId.substring(0, 4);
String lastFourChars = documentId.substring(abbreviated.length() - 4);
return String.format("%s..%s", firstFourChars, lastFourChars);
}
return documentId;
}
/**
* @exclude
*/
@InterfaceAudience.Private
protected List getLeafRevisions(boolean includeDeleted) throws CouchbaseLiteException {
List result = new ArrayList();
RevisionList revs = database.getStore().getAllRevisions(documentId, true);
if (revs != null) {
for (RevisionInternal rev : revs) {
// add it to result, unless we are not supposed to include deleted and it's deleted
if (!includeDeleted && rev.isDeleted()) {
// don't add it
} else {
result.add(getRevisionFromRev(rev));
}
}
}
return Collections.unmodifiableList(result);
}
/**
* @exclude
*/
@InterfaceAudience.Private
protected SavedRevision putProperties(Map properties, String prevID, boolean allowConflict) throws CouchbaseLiteException {
String newId = null;
if (properties != null && properties.containsKey("_id")) {
newId = (String) properties.get("_id");
}
if (newId != null && !newId.equalsIgnoreCase(getId())) {
Log.w(Database.TAG, "Trying to put wrong _id to this: %s properties: %s", this, properties);
}
// Process _attachments dict, converting CBLAttachments to dicts:
Map attachments = null;
if (properties != null && properties.containsKey("_attachments")) {
attachments = (Map) properties.get("_attachments");
}
if (attachments != null && attachments.size() > 0) {
Map updatedAttachments = Attachment.installAttachmentBodies(attachments, database);
properties.put("_attachments", updatedAttachments);
}
boolean hasTrueDeletedProperty = false;
if (properties != null) {
hasTrueDeletedProperty = properties.get("_deleted") != null && ((Boolean) properties.get("_deleted")).booleanValue();
}
boolean deleted = (properties == null) || hasTrueDeletedProperty;
RevisionInternal rev = new RevisionInternal(documentId, null, deleted);
if (properties != null) {
rev.setProperties(properties);
}
RevisionInternal newRev = database.putRevision(rev, prevID, allowConflict);
if (newRev == null) {
return null;
}
return new SavedRevision(this, newRev);
}
/**
* @exclude
*/
@InterfaceAudience.Private
protected SavedRevision getRevisionFromRev(RevisionInternal internalRevision) {
if (internalRevision == null) {
return null;
} else if (currentRevision != null && internalRevision.getRevID().equals(currentRevision.getId())) {
return currentRevision;
} else {
return new SavedRevision(this, internalRevision);
}
}
/**
* @exclude
*/
@InterfaceAudience.Private
protected void loadCurrentRevisionFrom(QueryRow row) {
if (row.getDocumentRevisionId() == null) {
return;
}
String revId = row.getDocumentRevisionId();
if (currentRevision == null || revIdGreaterThanCurrent(revId)) {
forgetCurrentRevision();
Map properties = row.getDocumentProperties();
if (properties != null) {
RevisionInternal rev = new RevisionInternal(properties);
currentRevision = new SavedRevision(this, rev);
}
}
}
/**
* @exclude
*/
@InterfaceAudience.Private
private boolean revIdGreaterThanCurrent(String revId) {
return (RevisionInternal.CBLCompareRevIDs(revId, currentRevision.getId()) > 0);
}
/**
* @exclude
*/
@InterfaceAudience.Private
protected void revisionAdded(DocumentChange change, boolean notify) {
String revID = change.getWinningRevisionID();
if (revID == null) {
return; // current revision didn't change
}
if (currentRevision != null && !revID.equals(currentRevision.getId())) {
RevisionInternal rev = change.getWinningRevisionIfKnown();
if (rev == null)
forgetCurrentRevision();
else if (rev.isDeleted())
currentRevision = null;
else
currentRevision = new SavedRevision(this, rev);
}
if (notify) {
synchronized (changeListeners) {
for (ChangeListener listener : changeListeners) {
listener.changed(new ChangeEvent(this, change));
}
}
}
}
@InterfaceAudience.Private
protected void forgetCurrentRevision() {
currentRevision = null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy