Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/**
* Original iOS version by Jens Alfke
* Ported to Android by Marty Schoch
*
* Copyright (c) 2012 Couchbase, Inc. All rights reserved.
*
* 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 com.couchbase.lite;
import com.couchbase.lite.internal.AttachmentInternal;
import com.couchbase.lite.internal.Body;
import com.couchbase.lite.internal.InterfaceAudience;
import com.couchbase.lite.internal.RevisionInternal;
import com.couchbase.lite.replicator.Replication;
import com.couchbase.lite.replicator.ReplicationState;
import com.couchbase.lite.replicator.ReplicationStateTransition;
import com.couchbase.lite.storage.SQLException;
import com.couchbase.lite.store.EncryptableStore;
import com.couchbase.lite.store.StorageValidation;
import com.couchbase.lite.store.Store;
import com.couchbase.lite.store.StoreDelegate;
import com.couchbase.lite.support.Base64;
import com.couchbase.lite.support.FileDirUtils;
import com.couchbase.lite.support.HttpClientFactory;
import com.couchbase.lite.support.PersistentCookieStore;
import com.couchbase.lite.support.RevisionUtils;
import com.couchbase.lite.support.action.Action;
import com.couchbase.lite.support.action.ActionBlock;
import com.couchbase.lite.support.action.ActionException;
import com.couchbase.lite.support.security.SymmetricKey;
import com.couchbase.lite.support.security.SymmetricKeyException;
import com.couchbase.lite.util.CollectionUtils;
import com.couchbase.lite.util.CollectionUtils.Functor;
import com.couchbase.lite.util.Log;
import com.couchbase.lite.util.StreamUtils;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A CouchbaseLite Database.
*/
public class Database implements StoreDelegate {
public static final String TAG = Log.TAG_DATABASE;
// When this many changes pile up in _changesToNotify, start removing their bodies to save RAM
private static final int MANY_CHANGES_TO_NOTIFY = 5000;
private static final String DEFAULT_PBKDF2_KEY_SALT = "Salty McNaCl";
private static final int DEFAULT_PBKDF2_KEY_ROUNDS = 64000;
private static final String DEFAULT_STORAGE = Manager.SQLITE_STORAGE;
private static final String SQLITE_STORE_CLASS = "com.couchbase.lite.store.SQLiteStore";
private static final String FORESTDB_STORE_CLASS = "com.couchbase.lite.store.ForestDBStore";
private static ReplicationFilterCompiler filterCompiler;
// Length that constitutes a 'big' attachment
public static int kBigAttachmentLength = (2 * 1024);
// Default value for maxRevTreeDepth, the max rev depth to preserve in a prune operation
public static int DEFAULT_MAX_REVS = 20;
private Store store = null;
private String path;
private String name;
final private AtomicBoolean open = new AtomicBoolean(false);
private Map views;
private Map viewDocTypes;
private Map filters;
private Map validations;
private Map pendingAttachmentsByDigest;
private Set activeReplicators;
private Set allReplicators;
private BlobStore attachments;
private Manager manager;
final private Set changeListeners;
final private Set databaseListeners;
private Cache docCache;
final private List changesToNotify;
private boolean postingChangeNotifications;
final private Object lockPostingChangeNotifications = new Object();
private long startTime;
/**
* Each database can have an associated PersistentCookieStore,
* where the persistent cookie store uses the database to store
* its cookies.
*
* There are two reasons this has been made an instance variable
* of the Database, rather than of the Replication:
*
* - The PersistentCookieStore needs to span multiple replications.
* For example, if there is a "push" and a "pull" replication for
* the same DB, they should share a cookie store.
*
* - PersistentCookieStore lifecycle should be tied to the Database
* lifecycle, since it needs to cease to exist if the underlying
* Database ceases to exist.
*
* REF: https://github.com/couchbase/couchbase-lite-android/issues/269
*/
private PersistentCookieStore persistentCookieStore;
/**
* Constructor
*/
@InterfaceAudience.Private
public Database(String path, String name, Manager manager, boolean readOnly) {
assert (new File(path).isAbsolute()); //path must be absolute
this.path = path;
this.name = name != null ? name : FileDirUtils.getDatabaseNameFromPath(path);
this.manager = manager;
this.startTime = System.currentTimeMillis();
this.changeListeners = Collections.synchronizedSet(new HashSet());
this.databaseListeners = Collections.synchronizedSet(new HashSet());
this.docCache = new Cache();
this.changesToNotify = Collections.synchronizedList(new ArrayList());
this.activeReplicators = Collections.synchronizedSet(new HashSet());
this.allReplicators = Collections.synchronizedSet(new HashSet());
this.postingChangeNotifications = false;
}
///////////////////////////////////////////////////////////////////////////
// APIs
// https://github.com/couchbaselabs/couchbase-lite-api/blob/master/gen/md/Database.md
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
// Constants
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
// Class Members - Properties
///////////////////////////////////////////////////////////////////////////
/**
* Returns the currently registered filter compiler (nil by default).
*/
@InterfaceAudience.Public
public static ReplicationFilterCompiler getFilterCompiler() {
return filterCompiler;
}
/**
* Registers an object that can compile source code into executable filter blocks.
*/
@InterfaceAudience.Public
public static void setFilterCompiler(ReplicationFilterCompiler filterCompiler) {
Database.filterCompiler = filterCompiler;
}
///////////////////////////////////////////////////////////////////////////
// Instance Members - Properties
///////////////////////////////////////////////////////////////////////////
/**
* Get all the replicators associated with this database.
*/
@InterfaceAudience.Public
public List getAllReplications() {
List allReplicatorsList = new ArrayList();
allReplicatorsList.addAll(allReplicators);
return allReplicatorsList;
}
/**
* The number of documents in the database.
*/
@InterfaceAudience.Public
public int getDocumentCount() {
return store.getDocumentCount();
}
/**
* The latest sequence number used. Every new revision is assigned a new sequence number,
* so this property increases monotonically as changes are made to the database. It can be
* used to check whether the database has changed between two points in time.
*/
@InterfaceAudience.Public
public long getLastSequenceNumber() {
return store.getLastSequence();
}
/**
* The database manager that owns this database.
*/
@InterfaceAudience.Public
public Manager getManager() {
return manager;
}
/**
* Get the database's name.
*/
@InterfaceAudience.Public
public String getName() {
return name;
}
///////////////////////////////////////////////////////////////////////////
// Instance Members - Methods
///////////////////////////////////////////////////////////////////////////
/**
* Adds a Database change delegate that will be called whenever a Document
* within the Database changes.
*/
@InterfaceAudience.Public
public void addChangeListener(ChangeListener listener) {
changeListeners.add(listener);
}
/**
* Compacts the database file by purging non-current JSON bodies, pruning revisions older than
* the maxRevTreeDepth, deleting unused attachment files, and vacuuming the SQLite database.
*/
@InterfaceAudience.Public
public void compact() throws CouchbaseLiteException {
store.compact();
garbageCollectAttachments();
}
/**
* Changes the database's encryption key, or removes encryption if the new key is null.
*
* To use this API, the database storage engine must support encryption, and the
* ManagerOptions.EnableStorageEncryption property must be set to true.
* @param newKeyOrPassword The encryption key in the form of an String (a password) or an
* byte[] object exactly 32 bytes in length (a raw AES key.)
* If a string is given, it will be internally converted to a raw key
* using 64,000 rounds of PBKDF2 hashing.
* A null value is legal, and clears a previously-registered key.
* @throws CouchbaseLiteException
*/
@InterfaceAudience.Public
public void changeEncryptionKey(final Object newKeyOrPassword) throws CouchbaseLiteException {
if (!(store instanceof EncryptableStore))
throw new CouchbaseLiteException(Status.NOT_IMPLEMENTED);
SymmetricKey newKey = null;
if (newKeyOrPassword != null)
newKey = createSymmetricKey(newKeyOrPassword);
try {
Action action = ((EncryptableStore) store).actionToChangeEncryptionKey(newKey);
action.add(attachments.actionToChangeEncryptionKey(newKey));
action.add(new ActionBlock() {
@Override
public void execute() throws ActionException {
manager.registerEncryptionKey(newKeyOrPassword, name);
}
}, null, null);
action.run();
} catch (ActionException e) {
throw new CouchbaseLiteException(e, Status.INTERNAL_SERVER_ERROR);
}
}
/**
* Returns a query that matches all documents in the database.
* This is like querying an imaginary view that emits every document's ID as a key.
*/
@InterfaceAudience.Public
public Query createAllDocumentsQuery() {
return new Query(this, (View) null);
}
/**
* Creates a new Document object with no properties and a new (random) UUID.
* The document will be saved to the database when you call -createRevision: on it.
*/
@InterfaceAudience.Public
public Document createDocument() {
return getDocument(Misc.CreateUUID());
}
/**
* Creates a new Replication that will pull from the source Database at the given url.
*
* @param remote the remote URL to pull from
* @return A new Replication that will pull from the source Database at the given url.
*/
@InterfaceAudience.Public
public Replication createPullReplication(URL remote) {
return new Replication(this, remote, Replication.Direction.PULL, null,
manager.getWorkExecutor());
}
/**
* Creates a new Replication that will push to the target Database at the given url.
*
* @param remote the remote URL to push to
* @return A new Replication that will push to the target Database at the given url.
*/
@InterfaceAudience.Public
public Replication createPushReplication(URL remote) {
return new Replication(this, remote, Replication.Direction.PUSH, null,
manager.getWorkExecutor());
}
/**
* Deletes the database.
*/
@InterfaceAudience.Public
public void delete() throws CouchbaseLiteException {
if (open.get()) {
if (!close()) {
throw new CouchbaseLiteException("The database was open, and could not be closed",
Status.INTERNAL_SERVER_ERROR);
}
}
manager.forgetDatabase(this);
if (!exists()) {
return;
}
File dir = new File(path);
if (!FileDirUtils.deleteRecursive(dir))
throw new CouchbaseLiteException("Was not able to delete the database directory",
Status.INTERNAL_SERVER_ERROR);
}
/**
* Deletes the local document with the given ID.
*/
@InterfaceAudience.Public
public boolean deleteLocalDocument(String localDocID) throws CouchbaseLiteException {
return putLocalDocument(localDocID, null);
}
/**
* Instantiates a Document object with the given ID.
* Doesn't touch the on-disk sqliteDb; a document with that ID doesn't
* even need to exist yet. CBLDocuments are cached, so there will
* never be more than one instance (in this sqliteDb) at a time with
* the same documentID.
* NOTE: the caching described above is not implemented yet
*/
@InterfaceAudience.Public
public Document getDocument(String documentId) {
if (documentId == null || documentId.length() == 0) {
return null;
}
Document doc = docCache.get(documentId);
if (doc == null) {
doc = new Document(this, documentId);
if (doc == null) {
return null;
}
docCache.put(documentId, doc);
}
return doc;
}
/**
* Gets the Document with the given id, or null if it does not exist.
*/
@InterfaceAudience.Public
public Document getExistingDocument(String docID) {
// TODO: Needs to review this implementation
if (docID == null || docID.length() == 0) {
return null;
}
RevisionInternal revisionInternal = getDocument(docID, null, true);
if (revisionInternal == null) {
return null;
}
return getDocument(docID);
}
/**
* Returns the contents of the local document with the given ID, or nil if none exists.
*/
@InterfaceAudience.Public
public Map getExistingLocalDocument(String documentId) {
RevisionInternal revInt = getLocalDocument(makeLocalDocumentId(documentId), null);
if (revInt == null) {
return null;
}
return revInt.getProperties();
}
/**
* Returns the existing View with the given name, or nil if none.
*/
@InterfaceAudience.Public
public View getExistingView(String name) {
View view = views != null ? views.get(name) : null;
if (view != null)
return view;
try {
return registerView(new View(this, name, false));
} catch (CouchbaseLiteException e) {
// View is not exist.
return null;
}
}
/**
* Returns the existing filter function (block) registered with the given name.
* Note that filters are not persistent -- you have to re-register them on every launch.
*/
@InterfaceAudience.Public
public ReplicationFilter getFilter(String filterName) {
ReplicationFilter result = null;
if (filters != null) {
result = filters.get(filterName);
}
if (result == null) {
ReplicationFilterCompiler filterCompiler = getFilterCompiler();
if (filterCompiler == null) {
return null;
}
List outLanguageList = new ArrayList();
String sourceCode = getDesignDocFunction(filterName, "filters", outLanguageList);
if (sourceCode == null) {
return null;
}
String language = outLanguageList.get(0);
ReplicationFilter filter = filterCompiler.compileFilterFunction(sourceCode, language);
if (filter == null) {
Log.w(Database.TAG, "Filter %s failed to compile", filterName);
return null;
}
setFilter(filterName, filter);
return filter;
}
return result;
}
/**
* Returns the existing document validation function (block) registered with the given name.
* Note that validations are not persistent -- you have to re-register them on every launch.
*/
@InterfaceAudience.Public
public Validator getValidation(String name) {
Validator result = null;
if (validations != null) {
result = validations.get(name);
}
return result;
}
/**
* Returns a View object for the view with the given name.
* (This succeeds even if the view doesn't already exist, but the view won't be added to
* the database until the View is assigned a map function.)
*/
@InterfaceAudience.Public
public View getView(String name) {
View view = null;
if (views != null) {
view = views.get(name);
}
if (view != null) {
return view;
}
try {
return registerView(new View(this, name, true));
} catch (CouchbaseLiteException e) {
Log.e(TAG, "Error in registerView", e);
return null;
}
}
/**
* Sets the contents of the local document with the given ID. Unlike CouchDB, no revision-ID
* checking is done; the put always succeeds. If the properties dictionary is nil, the document
* will be deleted.
*/
@InterfaceAudience.Public
public boolean putLocalDocument(String localDocID, Map properties)
throws CouchbaseLiteException {
localDocID = makeLocalDocumentId(localDocID);
RevisionInternal rev = new RevisionInternal(localDocID, null, properties == null);
if (properties != null)
rev.setProperties(properties);
return store.putLocalRevision(rev, null, false) != null;
}
/**
* Removes the specified delegate as a listener for the Database change event.
*/
@InterfaceAudience.Public
public void removeChangeListener(ChangeListener listener) {
changeListeners.remove(listener);
}
/**
* Runs the delegate asynchronously.
*/
@InterfaceAudience.Public
public Future runAsync(final AsyncTask asyncTask) {
return getManager().runAsync(new Runnable() {
@Override
public void run() {
asyncTask.run(Database.this);
}
});
}
/**
* Runs the block within a transaction. If the block returns NO, the transaction is rolled back.
* Use this when performing bulk write operations like multiple inserts/updates;
* it saves the overhead of multiple SQLite commits, greatly improving performance.
*
* Does not commit the transaction if the code throws an Exception.
*
* TODO: the iOS version has a retry loop, so there should be one here too
*/
@InterfaceAudience.Public
public boolean runInTransaction(TransactionalTask task) {
return store.runInTransaction(task);
}
/**
* Define or clear a named filter function.
*
* Filters are used by push replications to choose which documents to send.
*/
@InterfaceAudience.Public
public void setFilter(String filterName, ReplicationFilter filter) {
if (filters == null) {
filters = new HashMap();
}
if (filter != null) {
filters.put(filterName, filter);
} else {
filters.remove(filterName);
}
}
/**
* Defines or clears a named document validation function.
* Before any change to the database, all registered validation functions are called and given
* a chance to reject it. (This includes incoming changes from a pull replication.)
*/
@InterfaceAudience.Public
public void setValidation(String name, Validator validator) {
if (validations == null) {
validations = new HashMap();
}
if (validator != null) {
validations.put(name, validator);
} else {
validations.remove(name);
}
}
/**
* Set the maximum depth of a document's revision tree (or, max length of its revision history.)
* Revisions older than this limit will be deleted during a -compact: operation.
* Smaller values save space, at the expense of making document conflicts somewhat more likely.
*/
@InterfaceAudience.Public
public void setMaxRevTreeDepth(int maxRevTreeDepth) {
if (store != null)
store.setMaxRevTreeDepth(maxRevTreeDepth);
}
/**
* Get the maximum depth of a document's revision tree (or, max length of its revision history.)
* Revisions older than this limit will be deleted during a -compact: operation.
* Smaller values save space, at the expense of making document conflicts somewhat more likely.
*/
@InterfaceAudience.Public
public int getMaxRevTreeDepth() {
return store.getMaxRevTreeDepth();
}
///////////////////////////////////////////////////////////////////////////
// Events
///////////////////////////////////////////////////////////////////////////
/**
* The type of event raised when a Database changes.
*/
@InterfaceAudience.Public
public static class ChangeEvent {
private Database source;
private boolean isExternal;
private List changes;
public ChangeEvent(Database source, boolean isExternal, List changes) {
this.source = source;
this.isExternal = isExternal;
this.changes = changes;
}
public Database getSource() {
return source;
}
public boolean isExternal() {
return isExternal;
}
public List getChanges() {
return changes;
}
}
///////////////////////////////////////////////////////////////////////////
// Delegates
///////////////////////////////////////////////////////////////////////////
/**
* A delegate that can be used to listen for Database changes.
*/
@InterfaceAudience.Public
public interface ChangeListener {
void changed(ChangeEvent event);
}
// ReplicationFilterCompiler -> ReplicationFilterCompiler.java
// ReplicationFilter -> ReplicationFilter.java
// AsyncTask -> AsyncTask.java
// TransactionalTask -> TransactionalTask.java
// Validator -> Validator.java
///////////////////////////////////////////////////////////////////////////
// End of APIs
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
// Override Methods
///////////////////////////////////////////////////////////////////////////
/**
* Returns a string representation of this database.
*/
@InterfaceAudience.Public
public String toString() {
return this.getClass().getName() + '[' + path + ']';
}
///////////////////////////////////////////////////////////////////////////
// Implementation of StorageDelegate
///////////////////////////////////////////////////////////////////////////
/**
* in CBLDatabase+Internal.m
* - (void) storageExitedTransaction: (BOOL)committed
*/
@InterfaceAudience.Private
public void storageExitedTransaction(boolean committed) {
if (!committed) {
// I already told cached CBLDocuments about these new revisions. Back that out:
synchronized (changesToNotify) {
for (DocumentChange change : changesToNotify) {
Document doc = cachedDocumentWithID(change.getDocumentId());
if (doc != null)
doc.forgetCurrentRevision();
}
changesToNotify.clear();
}
}
postChangeNotifications();
}
/**
* in CBLDatabase+Internal.m
* - (void) databaseStorageChanged:(CBLDatabaseChange *)change
*/
@InterfaceAudience.Private
public void databaseStorageChanged(DocumentChange change) {
Log.v(Log.TAG_DATABASE, "Added: " + change.getAddedRevision());
changesToNotify.add(change);
if (!postChangeNotifications()) {
// The notification wasn't posted yet, probably because a transaction is open.
// But the CBLDocument, if any, needs to know right away so it can update its
// currentRevision.
Document doc = cachedDocumentWithID(change.getDocumentId());
if (doc != null) {
doc.revisionAdded(change, false);
}
}
// Squish the change objects if too many of them are piling up:
if (changesToNotify.size() >= MANY_CHANGES_TO_NOTIFY) {
if (changesToNotify.size() == MANY_CHANGES_TO_NOTIFY) {
synchronized (changesToNotify) {
for (DocumentChange c : changesToNotify)
c.reduceMemoryUsage();
}
} else {
change.reduceMemoryUsage();
}
}
}
/**
* Generates a revision ID for a new revision.
*
* @param json The canonical JSON of the revision (with metadata properties removed.)
* @param deleted YES if this revision is a deletion
* @param prevRevID The parent's revision ID, or nil if this is a new document.
*/
@InterfaceAudience.Private
public String generateRevID(byte[] json, boolean deleted, String prevRevID) {
return RevisionUtils.generateRevID(json, deleted, prevRevID);
}
@InterfaceAudience.Private
public BlobStore getAttachmentStore() {
return attachments;
}
private void validateRevision(RevisionInternal newRev,
RevisionInternal oldRev,
String parentRevID)
throws CouchbaseLiteException {
if (validations == null || validations.size() == 0) {
return;
}
SavedRevision publicRev = new SavedRevision(this, newRev, parentRevID);
publicRev.setParentRevisionID(parentRevID);
ValidationContextImpl context = new ValidationContextImpl(this, oldRev, newRev);
for (String validationName : validations.keySet()) {
Validator validation = getValidation(validationName);
validation.validate(publicRev, context);
if (context.getRejectMessage() != null) {
throw new CouchbaseLiteException(context.getRejectMessage(), Status.FORBIDDEN);
}
}
}
// #pragma mark - UPDATING _attachments DICTS:
private static long smallestLength(Map attachment) {
long length = 0;
Number explicitLength = (Number) attachment.get("length");
if (explicitLength != null)
length = explicitLength.longValue();
explicitLength = (Number) attachment.get("encoded_length");
if (explicitLength != null)
length = explicitLength.longValue();
return length;
}
/**
* Modifies a CBL_Revision's _attachments dictionary by adding the "data" property to all
* attachments (and removing "stub" and "follows".) GZip-encoded attachments will be unzipped
* unless options contains the flag kCBLLeaveAttachmentsEncoded.
*
* @param rev The revision to operate on. Its _attachments property may be altered.
* @param minRevPos Attachments with a "revpos" less than this will remain stubs.
* @param allowFollows If YES, non-small attachments will get a "follows" key instead of data.
* @param decodeAttachments If YES, attachments with "encoding" properties will be decoded.
* @param outStatus On failure, will be set to the error status.
* @return YES on success, NO on failure.
*/
@InterfaceAudience.Private
public boolean expandAttachments(final RevisionInternal rev,
final int minRevPos,
final boolean allowFollows,
final boolean decodeAttachments,
final Status outStatus) {
outStatus.setCode(Status.OK);
rev.mutateAttachments(new Functor