
com.couchbase.lite.AbstractDatabase Maven / Gradle / Ivy
//
// Copyright (c) 2020, 2017 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 android.support.annotation.GuardedBy;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.json.JSONException;
import com.couchbase.lite.internal.CBLInternalException;
import com.couchbase.lite.internal.CBLStatus;
import com.couchbase.lite.internal.CouchbaseLiteInternal;
import com.couchbase.lite.internal.ExecutionService;
import com.couchbase.lite.internal.SocketFactory;
import com.couchbase.lite.internal.core.C4BlobStore;
import com.couchbase.lite.internal.core.C4Constants;
import com.couchbase.lite.internal.core.C4Database;
import com.couchbase.lite.internal.core.C4DatabaseChange;
import com.couchbase.lite.internal.core.C4DatabaseObserver;
import com.couchbase.lite.internal.core.C4Document;
import com.couchbase.lite.internal.core.C4DocumentObserver;
import com.couchbase.lite.internal.core.C4DocumentObserverListener;
import com.couchbase.lite.internal.core.C4Query;
import com.couchbase.lite.internal.core.C4ReplicationFilter;
import com.couchbase.lite.internal.core.C4Replicator;
import com.couchbase.lite.internal.core.C4ReplicatorListener;
import com.couchbase.lite.internal.core.SharedKeys;
import com.couchbase.lite.internal.fleece.FLEncoder;
import com.couchbase.lite.internal.fleece.FLSliceResult;
import com.couchbase.lite.internal.support.Log;
import com.couchbase.lite.internal.utils.ClassUtils;
import com.couchbase.lite.internal.utils.FileUtils;
import com.couchbase.lite.internal.utils.Fn;
import com.couchbase.lite.internal.utils.JsonUtils;
import com.couchbase.lite.internal.utils.PlatformUtils;
import com.couchbase.lite.internal.utils.Preconditions;
/**
* AbstractDatabase is a base class of A Couchbase Lite Database.
*/
@SuppressWarnings({"PMD.GodClass", "PMD.CyclomaticComplexity", "PMD.TooManyMethods"})
abstract class AbstractDatabase {
/**
* Gets the logging controller for the Couchbase Lite library to configure the
* logging settings and add custom logging.
*
*/
// Public API. Do not fix the name.
@SuppressWarnings({"ConstantName", "PMD.FieldNamingConventions"})
@NonNull
public static final com.couchbase.lite.Log log = new com.couchbase.lite.Log();
//---------------------------------------------
// Constants
//---------------------------------------------
private static final String ERROR_RESOLVER_FAILED = "Conflict resolution failed for document '%s': %s";
private static final String WARN_WRONG_DATABASE = "The database to which the document produced by"
+ " conflict resolution for document '%s' belongs, '%s', is not the one in which it will be stored (%s)";
private static final String WARN_WRONG_ID = "The ID of the document produced by conflict resolution"
+ " for document (%s) does not match the IDs of the conflicting documents (%s)";
private static final LogDomain DOMAIN = LogDomain.DATABASE;
@VisibleForTesting
static final String DB_EXTENSION = ".cblite2";
private static final int MAX_CHANGES = 100;
private static final int DB_CLOSE_WAIT_SECS = 6; // > Core replicator timeout
private static final int DB_CLOSE_MAX_RETRIES = 5; // random choice: wait for 5 replicators
private static final int EXECUTOR_CLOSE_MAX_WAIT_SECS = 5;
// A random but absurdly large number.
private static final int MAX_CONFLICT_RESOLUTION_RETRIES = 13;
// How long to wait after a database opens before expiring docs
private static final long INITIAL_PURGE_DELAY_MS = 3;
private static final long STANDARD_PURGE_INTERVAL_MS = 1000;
private static final int DEFAULT_DATABASE_FLAGS
= C4Constants.DatabaseFlags.CREATE
| C4Constants.DatabaseFlags.AUTO_COMPACT
| C4Constants.DatabaseFlags.SHARED_KEYS;
static class ActiveProcess {
@NonNull
private final T process;
ActiveProcess(@NonNull T process) { this.process = process; }
public boolean isActive() { return true; }
public void stop() {}
@NonNull
@Override
public String toString() { return process.toString(); }
@Override
public int hashCode() { return process.hashCode(); }
@Override
public boolean equals(Object o) {
if (this == o) { return true; }
if (!(o instanceof ActiveProcess)) { return false; }
final ActiveProcess> other = (ActiveProcess>) o;
return process.equals(other.process);
}
}
// ---------------------------------------------
// API - public static methods
// ---------------------------------------------
/**
* Deletes a database of the given name in the given directory.
*
* @param name the database's name
* @param directory the directory containing the database: the database's parent directory.
* @throws CouchbaseLiteException Throws an exception if any error occurs during the operation.
*/
public static void delete(@NonNull String name, @Nullable File directory) throws CouchbaseLiteException {
Preconditions.assertNotNull(name, "name");
if (directory == null) { directory = new File(AbstractDatabaseConfiguration.getDbDirectory(null)); }
if (!exists(name, directory)) {
throw new CouchbaseLiteException(
"Database not found for delete",
CBLError.Domain.CBLITE,
CBLError.Code.NOT_FOUND);
}
final File path = getDatabaseFile(directory, name);
try {
Log.v(DOMAIN, "Delete database %s at %s", name, path.toString());
C4Database.deleteDbAtPath(path.getPath());
}
catch (LiteCoreException e) {
throw CBLStatus.convertException(e);
}
}
/**
* Checks whether a database of the given name exists in the given directory or not.
*
* @param name the database's name
* @param directory the path where the database is located.
* @return true if exists, false otherwise.
*/
public static boolean exists(@NonNull String name, @NonNull File directory) {
Preconditions.assertNotNull(name, "name");
Preconditions.assertNotNull(directory, "directory");
return getDatabaseFile(directory, name).exists();
}
protected static void copy(
@NonNull File path,
@NonNull String name,
@NonNull DatabaseConfiguration config,
int algorithm,
byte[] encryptionKey)
throws CouchbaseLiteException {
String fromPath = path.getPath();
if (fromPath.charAt(fromPath.length() - 1) != File.separatorChar) { fromPath += File.separator; }
String toPath = getDatabaseFile(new File(config.getDirectory()), name).getPath();
if (toPath.charAt(toPath.length() - 1) != File.separatorChar) { toPath += File.separator; }
// Setting the temp directory from the Database Configuration is deprecated and will go away:
CouchbaseLiteInternal.setupDirectories(config.getRootDirectory());
try {
C4Database.copyDb(
fromPath,
toPath,
DEFAULT_DATABASE_FLAGS,
null,
C4Constants.DocumentVersioning.REVISION_TREES,
algorithm,
encryptionKey);
}
catch (LiteCoreException e) {
FileUtils.eraseFileOrDir(toPath);
throw CBLStatus.convertException(e);
}
}
/**
* Set log level for the given log domain.
*
* @param domain The log domain
* @param level The log level
* @deprecated As of 2.5 because it is being replaced with the
* {@link com.couchbase.lite.Log#getConsole() getConsole} method from the {@link #log log} property.
* This method will eventually be replaced with a no-op to preserve API compatibility.
* Until it is, its interactions with other logging maybe be fairly unpredictable.
*/
@Deprecated
public static void setLogLevel(@NonNull LogDomain domain, @NonNull LogLevel level) {
Preconditions.assertNotNull(domain, "domain");
Preconditions.assertNotNull(level, "level");
//noinspection deprecation
final EnumSet domains = (domain == LogDomain.ALL)
? LogDomain.ALL_DOMAINS
: EnumSet.of(domain);
final ConsoleLogger logger = log.getConsole();
logger.setDomains(domains);
logger.setLevel(level);
}
@VisibleForTesting
static File getDatabaseFile(File dir, String name) {
return new File(dir, name.replaceAll("/", ":") + DB_EXTENSION);
}
//---------------------------------------------
// Member variables
//---------------------------------------------
@NonNull
final DatabaseConfiguration config;
// Main database lock object for thread-safety
@NonNull
private final Object dbLock = new Object();
private final String name;
private final String path;
// Executor for purge and posting Database/Document changes.
private final ExecutionService.CloseableExecutor postExecutor;
// Executor for LiveQuery.
private final ExecutionService.CloseableExecutor queryExecutor;
private final SharedKeys sharedKeys;
private final DocumentExpirationStrategy purgeStrategy;
@GuardedBy("activeProcesses")
private final Set> activeProcesses;
@GuardedBy("dbLock")
private final Map docChangeNotifiers;
@GuardedBy("dbLock")
private C4Database c4Database;
@GuardedBy("dbLock")
private ChangeNotifier dbChangeNotifier;
@GuardedBy("dbLock")
private C4DatabaseObserver c4DbObserver;
private volatile CountDownLatch closeLatch;
//---------------------------------------------
// Constructors
//---------------------------------------------
/**
* Construct a AbstractDatabase with a given name and database config.
* If the database does not yet exist, it will be created, unless the `readOnly` option is used.
*
* @param name The name of the database. May NOT contain capital letters!
* @param config The database config, Note: null config parameter is not allowed with Android platform
* @throws CouchbaseLiteException Throws an exception if any error occurs during the open operation.
*/
protected AbstractDatabase(@NonNull String name, @NonNull DatabaseConfiguration config)
throws CouchbaseLiteException {
Preconditions.assertNotEmpty(name, "db name");
Preconditions.assertNotNull(config, "config");
CouchbaseLiteInternal.requireInit("Cannot create database");
// Name:
this.name = name;
// Copy configuration
this.config = config.readOnlyCopy();
this.postExecutor = CouchbaseLiteInternal.getExecutionService().getSerialExecutor();
this.queryExecutor = CouchbaseLiteInternal.getExecutionService().getSerialExecutor();
this.activeProcesses = new HashSet<>();
this.docChangeNotifiers = new HashMap<>();
// !!! Remove this code.
// Setting the temp directory from the Database Configuration is a bad idea and should be deprecated.
// Directories should be set in the CouchbaseLite.init method
fixHydrogenBug(config, name);
CouchbaseLiteInternal.setupDirectories(config.getRootDirectory());
// Can't open the DB until the file system is set up.
this.c4Database = openC4Db();
this.path = c4Database.getPath();
// Initialize a shared keys:
this.sharedKeys = new SharedKeys(c4Database);
this.purgeStrategy = new DocumentExpirationStrategy(this, STANDARD_PURGE_INTERVAL_MS, postExecutor);
this.purgeStrategy.schedulePurge(INITIAL_PURGE_DELAY_MS);
// warn if logging has not been turned on
Log.warn();
}
/**
* Initialize Database with a give C4Database object in the shell mode. The life of the
* C4Database object will be managed by the caller. This is currently used for creating a
* Dictionary as an input of the predict() method of the PredictiveModel.
*/
// !!! This should be a separate class...
protected AbstractDatabase(long c4dbHandle) {
CouchbaseLiteInternal.requireInit("Cannot create database");
this.c4Database = new C4Database(c4dbHandle);
this.path = c4Database.getPath();
this.name = null;
this.config = new DatabaseConfiguration();
this.postExecutor = null;
this.queryExecutor = null;
this.activeProcesses = null;
this.docChangeNotifiers = null;
this.sharedKeys = null;
this.purgeStrategy = null;
}
//---------------------------------------------
// API - public methods
//---------------------------------------------
// GET EXISTING DOCUMENT
/**
* Return the database name
*
* @return the database's name
*/
@NonNull
public String getName() { return this.name; }
/**
* Return the database's path. If the database is closed or deleted, null value will be returned.
*
* @return the database's path.
*/
public String getPath() {
synchronized (dbLock) { return (!isOpen()) ? null : path; }
}
/**
* The number of documents in the database.
*
* @return the number of documents in the database, 0 if database is closed.
*/
public long getCount() {
synchronized (dbLock) { return (!isOpen()) ? 0L : c4Database.getDocumentCount(); }
}
/**
* Returns a READONLY config object which will throw a runtime exception
* when any setter methods are called.
*
* @return the READONLY copied config object
*/
@NonNull
public DatabaseConfiguration getConfig() { return config.readOnlyCopy(); }
/**
* Gets an existing Document object with the given ID. If the document with the given ID doesn't
* exist in the database, the value returned will be null.
*
* @param id the document ID
* @return the Document object
*/
public Document getDocument(@NonNull String id) {
Preconditions.assertNotNull(id, "id");
synchronized (dbLock) {
mustBeOpen();
try { return Document.getDocument((Database) this, id, false); }
// only 404 - Not Found error throws CouchbaseLiteException
catch (CouchbaseLiteException ex) { return null; }
}
}
/**
* Saves a document to the database. When write operations are executed
* concurrently, the last writer will overwrite all other written values.
* Calling this method is the same as calling the ave(MutableDocument, ConcurrencyControl)
* method with LAST_WRITE_WINS concurrency control.
*
* @param document The document.
* @throws CouchbaseLiteException on error
*/
public void save(@NonNull MutableDocument document) throws CouchbaseLiteException {
save(document, ConcurrencyControl.LAST_WRITE_WINS);
}
/**
* Saves a document to the database. When used with LAST_WRITE_WINS
* concurrency control, the last write operation will win if there is a conflict.
* When used with FAIL_ON_CONFLICT concurrency control, save will fail with false value
*
* @param document The document.
* @param concurrencyControl The concurrency control.
* @return true if successful. false if the FAIL_ON_CONFLICT concurrency
* @throws CouchbaseLiteException on error
*/
public boolean save(@NonNull MutableDocument document, @NonNull ConcurrencyControl concurrencyControl)
throws CouchbaseLiteException {
try {
saveInternal(document, null, false, concurrencyControl);
return true;
}
catch (CouchbaseLiteException e) {
if (!CouchbaseLiteException.isConflict(e)) { throw e; }
}
return false;
}
/**
* Saves a document to the database. Conflicts will be resolved by the passed ConflictHandler
*
* @param document The document.
* @param conflictHandler A conflict handler.
* @return true if successful. false if the FAIL_ON_CONFLICT concurrency
* @throws CouchbaseLiteException on error
*/
public boolean save(@NonNull MutableDocument document, @NonNull ConflictHandler conflictHandler)
throws CouchbaseLiteException {
Preconditions.assertNotNull(document, "document");
Preconditions.assertNotNull(conflictHandler, "conflictHandler");
saveWithConflictHandler(document, conflictHandler);
return true;
}
/**
* Deletes a document from the database. When write operations are executed
* concurrently, the last writer will overwrite all other written values.
* Calling this function is the same as calling the delete(Document, ConcurrencyControl)
* function with LAST_WRITE_WINS concurrency control.
*
* @param document The document.
* @throws CouchbaseLiteException on error
*/
public void delete(@NonNull Document document) throws CouchbaseLiteException {
delete(document, ConcurrencyControl.LAST_WRITE_WINS);
}
/**
* Deletes a document from the database. When used with lastWriteWins concurrency
* control, the last write operation will win if there is a conflict.
* When used with FAIL_ON_CONFLICT concurrency control, delete will fail with
* 'false' value returned.
*
* @param document The document.
* @param concurrencyControl The concurrency control.
* @throws CouchbaseLiteException on error
*/
public boolean delete(@NonNull Document document, @NonNull ConcurrencyControl concurrencyControl)
throws CouchbaseLiteException {
// NOTE: synchronized in save(Document, boolean, ConcurrencyControl, ConflictHandler) method
try {
saveInternal(document, null, true, concurrencyControl);
return true;
}
catch (CouchbaseLiteException e) {
if (!CouchbaseLiteException.isConflict(e)) { throw e; }
}
return false;
}
// Batch operations:
/**
* Purges the given document from the database. This is more drastic than delete(Document),
* it removes all traces of the document. The purge will NOT be replicated to other databases.
*
* @param document the document to be purged.
*/
public void purge(@NonNull Document document) throws CouchbaseLiteException {
Preconditions.assertNotNull(document, "document");
if (document.isNewDocument()) {
throw new CouchbaseLiteException("DocumentNotFound", CBLError.Domain.CBLITE, CBLError.Code.NOT_FOUND);
}
synchronized (dbLock) {
prepareDocument(document);
try { purge(document.getId()); }
catch (CouchbaseLiteException e) {
// Ignore not found (already deleted)
if (e.getCode() != CBLError.Code.NOT_FOUND) { throw e; }
}
document.replaceC4Document(null); // Reset c4doc:
}
}
/**
* Purges the given document id for the document in database. This is more drastic than delete(Document),
* it removes all traces of the document. The purge will NOT be replicated to other databases.
*
* @param id the document ID
*/
public void purge(@NonNull String id) throws CouchbaseLiteException {
Preconditions.assertNotNull(id, "id");
synchronized (dbLock) { purgeLocked(id); }
}
// Database changes:
/**
* Sets an expiration date on a document. After this time, the document
* will be purged from the database.
*
* @param id The ID of the Document
* @param expiration Nullable expiration timestamp as a Date, set timestamp to null
* to remove expiration date time from doc.
* @throws CouchbaseLiteException Throws an exception if any error occurs during the operation.
*/
public void setDocumentExpiration(@NonNull String id, Date expiration) throws CouchbaseLiteException {
Preconditions.assertNotNull(id, "id");
if (purgeStrategy == null) {
Log.w(DOMAIN, "Attempt to set document expiration without a purge strategy");
return;
}
synchronized (dbLock) {
try {
getC4DatabaseLocked().setExpiration(id, (expiration == null) ? 0 : expiration.getTime());
purgeStrategy.schedulePurge(0);
}
catch (LiteCoreException e) {
throw CBLStatus.convertException(e);
}
}
}
/**
* Returns the expiration time of the document. null will be returned if there is
* no expiration time set
*
* @param id The ID of the Document
* @return Date a nullable expiration timestamp of the document or null if time not set.
* @throws CouchbaseLiteException Throws an exception if any error occurs during the operation.
*/
public Date getDocumentExpiration(@NonNull String id) throws CouchbaseLiteException {
Preconditions.assertNotNull(id, "id");
synchronized (dbLock) {
try {
if (getC4Document(id) == null) {
throw new CouchbaseLiteException(
"DocumentNotFound",
CBLError.Domain.CBLITE,
CBLError.Code.NOT_FOUND);
}
final long timestamp = getC4DatabaseLocked().getExpiration(id);
return (timestamp == 0) ? null : new Date(timestamp);
}
catch (LiteCoreException e) {
throw CBLStatus.convertException(e);
}
}
}
/**
* Runs a group of database operations in a batch. Use this when performing bulk write operations
* like multiple inserts/updates; it saves the overhead of multiple database commits, greatly
* improving performance.
*
* @param runnable the action which is implementation of Runnable interface
* @throws CouchbaseLiteException Throws an exception if any error occurs during the operation.
*/
public void inBatch(@NonNull Runnable runnable) throws CouchbaseLiteException {
Preconditions.assertNotNull(runnable, "runnable");
synchronized (dbLock) {
final C4Database db = getC4DatabaseLocked();
boolean commit = false;
try {
db.beginTransaction();
try {
runnable.run();
commit = true;
}
catch (RuntimeException e) {
throw new CouchbaseLiteException("In-batch task failed", e);
}
finally {
db.endTransaction(commit);
}
}
catch (LiteCoreException e) {
throw CBLStatus.convertException(e);
}
}
postDatabaseChanged();
}
// Compaction:
/**
* Compacts the database file by deleting unused attachment files and vacuuming the SQLite database
*
* @deprecated Use Database.performMaintenance(MaintenanceType.COMPACT)
*/
@Deprecated
public void compact() throws CouchbaseLiteException {
synchronized (dbLock) {
try { getC4DatabaseLocked().compact(); }
catch (LiteCoreException e) { throw CBLStatus.convertException(e); }
}
}
// Document changes:
/**
* Adds a change listener for the changes that occur in the database. The changes will be delivered on the UI
* thread for the Android platform and on an arbitrary thread for the Java platform. When developing a Java
* Desktop application using Swing or JavaFX that needs to update the UI after receiving the changes, make
* sure to schedule the UI update on the UI thread by using SwingUtilities.invokeLater(Runnable) or
* Platform.runLater(Runnable) respectively.
*
* @param listener callback
*/
@NonNull
public ListenerToken addChangeListener(@NonNull DatabaseChangeListener listener) {
return addChangeListener(null, listener);
}
// Others:
/**
* Adds a change listener for the changes that occur in the database with an executor on which the changes will be
* posted to the listener. If the executor is not specified, the changes will be delivered on the UI thread for
* the Android platform and on an arbitrary thread for the Java platform.
*
* @param listener callback
*/
@NonNull
public ListenerToken addChangeListener(@Nullable Executor executor, @NonNull DatabaseChangeListener listener) {
Preconditions.assertNotNull(listener, "listener");
synchronized (dbLock) {
mustBeOpen();
return addDatabaseChangeListenerLocked(executor, listener);
}
}
/**
* Removes the change listener added to the database.
*
* @param token returned by a previous call to addChangeListener or addDocumentListener.
*/
public void removeChangeListener(@NonNull ListenerToken token) {
Preconditions.assertNotNull(token, "token");
synchronized (dbLock) {
if (token instanceof ChangeListenerToken) {
final ChangeListenerToken> changeListenerToken = (ChangeListenerToken>) token;
if (changeListenerToken.getKey() != null) {
removeDocumentChangeListenerLocked(changeListenerToken);
return;
}
}
removeDatabaseChangeListenerLocked(token);
}
}
/**
* Adds a change listener for the changes that occur to the specified document.
* The changes will be delivered on the UI thread for the Android platform and on an arbitrary
* thread for the Java platform. When developing a Java Desktop application using Swing or JavaFX
* that needs to update the UI after receiving the changes, make sure to schedule the UI update
* on the UI thread by using SwingUtilities.invokeLater(Runnable) or Platform.runLater(Runnable)
* respectively.
*/
@NonNull
public ListenerToken addDocumentChangeListener(@NonNull String id, @NonNull DocumentChangeListener listener) {
return addDocumentChangeListener(id, null, listener);
}
/**
* Adds a change listener for the changes that occur to the specified document with an executor on which
* the changes will be posted to the listener. If the executor is not specified, the changes will be
* delivered on the UI thread for the Android platform and on an arbitrary thread for the Java platform.
*/
@NonNull
public ListenerToken addDocumentChangeListener(
@NonNull String id,
@Nullable Executor executor,
@NonNull DocumentChangeListener listener) {
Preconditions.assertNotNull(id, "id");
Preconditions.assertNotNull(listener, "listener");
synchronized (dbLock) {
mustBeOpen();
return addDocumentChangeListenerLocked(id, executor, listener);
}
}
/**
* Closes a database.
* Closing a database will stop all replicators, live queries and all listeners attached to it.
*
* @throws CouchbaseLiteException Throws an exception if any error occurs during the operation.
*/
public void close() throws CouchbaseLiteException {
Log.v(DOMAIN, "Closing %s at path %s", this, path);
if (!isOpen()) { return; }
shutdown(C4Database::close);
}
/**
* Deletes a database.
* Deleting a database will stop all replicators, live queries and all listeners attached to it.
* Although attempting to close a closed database is not an error, attempting to delete a closed database is.
*
* @throws CouchbaseLiteException Throws an exception if any error occurs during the operation.
*/
public void delete() throws CouchbaseLiteException {
Log.v(DOMAIN, "Deleting %s at path %s", this, path);
shutdown(C4Database::delete);
}
@SuppressWarnings("unchecked")
@NonNull
public List getIndexes() throws CouchbaseLiteException {
synchronized (dbLock) {
try { return (List) getC4DatabaseLocked().getIndexes().asObject(); }
catch (LiteCoreException e) { throw CBLStatus.convertException(e); }
}
}
public void createIndex(@NonNull String name, @NonNull Index idx) throws CouchbaseLiteException {
Preconditions.assertNotNull(name, "name");
final AbstractIndex index = (AbstractIndex) Preconditions.assertNotNull(idx, "index");
synchronized (dbLock) {
final C4Database c4Db = getC4DatabaseLocked();
try {
final String json = JsonUtils.toJson(index.items()).toString();
c4Db.createIndex(
name,
json,
index.type().getValue(),
index.language(),
index.ignoreAccents());
}
catch (LiteCoreException e) {
throw CBLStatus.convertException(e);
}
catch (JSONException e) {
throw new CouchbaseLiteException("Error encoding JSON", e);
}
}
}
public void deleteIndex(@NonNull String name) throws CouchbaseLiteException {
synchronized (dbLock) {
try { getC4DatabaseLocked().deleteIndex(name); }
catch (LiteCoreException e) { throw CBLStatus.convertException(e); }
}
}
public boolean performMaintenance(MaintenanceType type) throws CouchbaseLiteException {
synchronized (dbLock) {
try { return getC4DatabaseLocked().performMaintenance(type); }
catch (LiteCoreException e) { throw CBLStatus.convertException(e); }
}
}
//---------------------------------------------
// Override public method
//---------------------------------------------
@NonNull
@Override
public String toString() { return "Database{" + ClassUtils.objId(this) + ", name='" + name + "'}"; }
//---------------------------------------------
// Protected level access
//---------------------------------------------
// This method may return a closed database
@NonNull
protected C4Database getC4Database() {
synchronized (dbLock) { return getC4DatabaseLocked(); }
}
@GuardedBy("dbLock")
@NonNull
protected C4Database getC4DatabaseLocked() {
mustBeOpen();
return c4Database;
}
@SuppressWarnings("NoFinalizer")
@Override
protected void finalize() throws Throwable {
try {
// This is the only thing that is really essential.
final C4DatabaseObserver observer = c4DbObserver;
if (observer != null) { observer.close(); }
// This stuff might just speed things up a little
shutdownActiveProcesses(activeProcesses);
shutdownExecutors(postExecutor, queryExecutor, 0);
}
finally {
super.finalize();
}
}
//---------------------------------------------
// Package level access
//---------------------------------------------
// When seizing multiple locks, always seize this lock first.
@NonNull
Object getLock() { return dbLock; }
boolean equalsWithPath(Database other) {
if (other == null) { return false; }
final File path = getFilePath();
final File otherPath = other.getFilePath();
if ((path == null) && (otherPath == null)) { return true; }
return (path != null) && path.equals(otherPath);
}
@NonNull
C4BlobStore getBlobStore() throws LiteCoreException {
synchronized (dbLock) { return getC4DatabaseLocked().getBlobStore(); }
}
// Instead of clone()
Database copy() throws CouchbaseLiteException { return new Database(this.name, this.config); }
//////// DATABASES:
// WARNING: this is the state at the time of the call!
// You must hold dbLock to keep the state from changing.
boolean isOpen() {
synchronized (dbLock) { return c4Database != null; }
}
// WARNING: guarantees state at the time of the call!
// You must hold dbLock to keep the state from changing.
void mustBeOpen() {
synchronized (dbLock) {
if (!isOpen()) { throw new IllegalStateException(Log.lookupStandardMessage("DBClosed")); }
}
}
@Nullable
String getUuid() {
synchronized (dbLock) {
if (isOpen()) {
byte[] uuid = null;
try { uuid = c4Database.getPublicUUID(); }
catch (LiteCoreException e) { Log.i(DOMAIN, "Failed retrieving database UUID", e); }
if (uuid != null) { return PlatformUtils.getEncoder().encodeToString(uuid); }
}
return null;
}
}
@Nullable
File getFilePath() {
final String path = getPath();
return (path == null) ? null : new File(path);
}
@Nullable
File getDbFile() { return (path == null) ? null : new File(path); }
//////// DOCUMENTS:
// This method is *NOT* thread safe.
// If used wo/synchronization, there is a race on the open db
ListenerToken addActiveLiveQuery(@NonNull LiveQuery query) {
mustBeOpen();
registerProcess(new ActiveProcess(query) {
@Override
public void stop() { query.stop(); }
@Override
public boolean isActive() { return !LiveQuery.State.STOPPED.equals(query.getState()); }
});
return addChangeListener(query);
}
// This method is not thread safe
void removeActiveLiveQuery(@NonNull LiveQuery query, @NonNull ListenerToken token) {
removeChangeListener(token);
unregisterProcess(query);
}
C4Query createQuery(@NonNull String json) throws LiteCoreException {
synchronized (dbLock) { return getC4DatabaseLocked().createQuery(json); }
}
C4Document getC4Document(@NonNull String id) throws LiteCoreException {
synchronized (dbLock) { return getC4DatabaseLocked().get(id); }
}
FLEncoder getSharedFleeceEncoder() {
synchronized (dbLock) { return getC4DatabaseLocked().getSharedFleeceEncoder(); }
}
long getNextDocumentExpiration() {
synchronized (dbLock) { return getC4DatabaseLocked().nextDocExpiration(); }
}
long purgeExpiredDocs() {
synchronized (dbLock) { return getC4DatabaseLocked().purgeExpiredDocs(); }
}
@NonNull
C4DocumentObserver createDocumentObserver(
@NonNull ChangeNotifier> context,
@NonNull String docID,
@NonNull C4DocumentObserverListener listener) {
synchronized (dbLock) { return getC4DatabaseLocked().createDocumentObserver(docID, listener, context); }
}
//////// REPLICATORS:
@SuppressWarnings("PMD.ExcessiveParameterList")
@NonNull
C4Replicator createRemoteReplicator(
@NonNull Replicator replicator,
@Nullable String scheme,
@Nullable String host,
int port,
@Nullable String path,
@Nullable String remoteDatabaseName,
int push,
int pull,
@NonNull byte[] options,
@Nullable C4ReplicatorListener listener,
@Nullable C4ReplicationFilter pushFilter,
@Nullable C4ReplicationFilter pullFilter,
@Nullable SocketFactory socketFactoryContext,
int framing)
throws LiteCoreException {
final C4Replicator c4Repl;
synchronized (dbLock) {
c4Repl = getC4DatabaseLocked().createRemoteReplicator(
scheme,
host,
port,
path,
remoteDatabaseName,
push,
pull,
options,
listener,
pushFilter,
pullFilter,
replicator,
socketFactoryContext,
framing);
}
return c4Repl;
}
@SuppressWarnings("PMD.ExcessiveParameterList")
@NonNull
C4Replicator createLocalReplicator(
@NonNull Replicator replicator,
@NonNull Database otherLocalDb,
int push,
int pull,
@NonNull byte[] options,
@Nullable C4ReplicatorListener listener,
@Nullable C4ReplicationFilter pushFilter,
@Nullable C4ReplicationFilter pullFilter)
throws LiteCoreException {
final C4Replicator c4Repl;
synchronized (dbLock) {
c4Repl = getC4DatabaseLocked().createLocalReplicator(
otherLocalDb.getC4Database(),
push,
pull,
options,
listener,
pushFilter,
pullFilter,
replicator);
}
return c4Repl;
}
// This method is *NOT* thread safe.
// If used wo/synchronization, there is a race on the open db
void addActiveReplicator(AbstractReplicator replicator) {
mustBeOpen();
registerProcess(new ActiveProcess(replicator) {
@Override
public void stop() { replicator.stop(); }
@Override
public boolean isActive() {
return !AbstractReplicator.ActivityLevel.STOPPED.equals(replicator.getState());
}
});
}
void removeActiveReplicator(AbstractReplicator replicator) { unregisterProcess(replicator); }
//////// RESOLVING REPLICATED CONFLICTS:
void resolveReplicationConflict(
@Nullable ConflictResolver resolver,
@NonNull String docId,
@NonNull Fn.Consumer callback) {
int n = 0;
CouchbaseLiteException err = null;
try {
while (true) {
if (n++ > MAX_CONFLICT_RESOLUTION_RETRIES) {
err = new CouchbaseLiteException(
"Too many attempts to resolve a conflicted document: " + n,
CBLError.Domain.CBLITE,
CBLError.Code.UNEXPECTED_ERROR);
break;
}
try {
resolveConflictOnce(resolver, docId);
callback.accept(null);
return;
}
catch (CouchbaseLiteException e) {
if (!CouchbaseLiteException.isConflict(e)) {
err = e;
break;
}
}
catch (CBLInternalException e) {
// This error occurs when a resolver that starts after this one
// fixes the conflict before this one does. When this one attempts
// to save, it gets a conflict error and retries. During the retry,
// it cannot find a conflicting revision and throws this error.
// The other resolver did the right thing, so there is no reason
// to report an error.
if (e.getCode() != CBLInternalException.FAILED_SELECTING_CONFLICTING_REVISION) {
err = new CouchbaseLiteException("Conflict resolution failed", e);
}
break;
}
}
}
catch (RuntimeException e) {
final String msg = e.getMessage();
err = new CouchbaseLiteException(
(msg != null) ? msg : "Conflict resolution failed",
e,
CBLError.Domain.CBLITE,
CBLError.Code.UNEXPECTED_ERROR);
}
callback.accept(err);
}
//////// Cookie Store:
void setCookie(@NonNull URI uri, @NonNull String setCookieHeader) {
try {
synchronized (dbLock) { getC4DatabaseLocked().setCookie(uri, setCookieHeader); }
}
catch (LiteCoreException e) { Log.e(DOMAIN, "Cannot save cookie for " + uri, e); }
}
@Nullable
String getCookies(@NonNull URI uri) {
try {
synchronized (dbLock) { return getC4DatabaseLocked().getCookies(uri); }
}
catch (LiteCoreException e) { Log.e(DOMAIN, "Cannot get cookies for " + uri, e); }
return null;
}
//////// Execution:
void scheduleOnPostNotificationExecutor(@NonNull Runnable task, long delayMs) {
CouchbaseLiteInternal.getExecutionService().postDelayedOnExecutor(delayMs, postExecutor, task);
}
void scheduleOnQueryExecutor(@NonNull Runnable task, long delayMs) {
CouchbaseLiteInternal.getExecutionService().postDelayedOnExecutor(delayMs, queryExecutor, task);
}
void registerProcess(ActiveProcess> process) {
synchronized (activeProcesses) { activeProcesses.add(process); }
}
void unregisterProcess(T process) {
synchronized (activeProcesses) { activeProcesses.remove(new ActiveProcess(process)); }
verifyActiveProcesses();
}
abstract int getEncryptionAlgorithm();
abstract byte[] getEncryptionKey();
//---------------------------------------------
// Private (in class only)
//---------------------------------------------
//////// DATABASES:
@GuardedBy("dbLock")
private void beginTransaction() throws CouchbaseLiteException {
try { getC4DatabaseLocked().beginTransaction(); }
catch (LiteCoreException e) { throw CBLStatus.convertException(e); }
}
@GuardedBy("dbLock")
private void endTransaction(boolean commit) throws CouchbaseLiteException {
try { getC4DatabaseLocked().endTransaction(commit); }
catch (LiteCoreException e) { throw CBLStatus.convertException(e); }
}
private C4Database openC4Db() throws CouchbaseLiteException {
final File dbFile = getDatabaseFile(new File(config.getDirectory()), this.name);
Log.v(DOMAIN, "Opening %s at path %s", this, dbFile.getPath());
try {
return new C4Database(
dbFile.getPath(),
getDatabaseFlags(),
null,
C4Constants.DocumentVersioning.REVISION_TREES,
getEncryptionAlgorithm(),
getEncryptionKey());
}
catch (LiteCoreException e) {
if (e.code == CBLError.Code.NOT_A_DATABSE_FILE) {
throw new CouchbaseLiteException(
"The provided encryption key was incorrect.",
e,
CBLError.Domain.CBLITE,
e.code);
}
if (e.code == CBLError.Code.CANT_OPEN_FILE) {
throw new CouchbaseLiteException("CreateDBDirectoryFailed", e, CBLError.Domain.CBLITE, e.code);
}
throw CBLStatus.convertException(e);
}
}
private int getDatabaseFlags() { return DEFAULT_DATABASE_FLAGS; }
//////// DOCUMENTS:
// --- Database changes:
@GuardedBy("dbLock")
@NonNull
private ListenerToken addDatabaseChangeListenerLocked(
@Nullable Executor executor,
@NonNull DatabaseChangeListener listener) {
if (dbChangeNotifier == null) {
dbChangeNotifier = new ChangeNotifier<>();
registerC4DbObserver();
}
return dbChangeNotifier.addChangeListener(executor, listener);
}
// --- Notification: - C4DatabaseObserver/C4DocumentObserver
@GuardedBy("dbLock")
private void removeDatabaseChangeListenerLocked(@NonNull ListenerToken token) {
if (dbChangeNotifier.removeChangeListener(token) == 0) {
freeC4DbObserver();
dbChangeNotifier = null;
}
}
// --- Document changes:
@GuardedBy("dbLock")
@NonNull
private ListenerToken addDocumentChangeListenerLocked(
@NonNull String docID,
@Nullable Executor executor,
@NonNull DocumentChangeListener listener) {
DocumentChangeNotifier docNotifier = docChangeNotifiers.get(docID);
if (docNotifier == null) {
docNotifier = new DocumentChangeNotifier((Database) this, docID);
docChangeNotifiers.put(docID, docNotifier);
}
final ChangeListenerToken> token = docNotifier.addChangeListener(executor, listener);
token.setKey(docID);
return token;
}
@GuardedBy("dbLock")
private void removeDocumentChangeListenerLocked(@NonNull ChangeListenerToken> token) {
final String docID = (String) token.getKey();
if (docChangeNotifiers.containsKey(docID)) {
final DocumentChangeNotifier notifier = docChangeNotifiers.get(docID);
if ((notifier != null) && (notifier.removeChangeListener(token) == 0)) { docChangeNotifiers.remove(docID); }
}
}
@GuardedBy("dbLock")
private void registerC4DbObserver() {
if (!isOpen()) { return; }
c4DbObserver = c4Database.createDatabaseObserver(
(observer, context) -> scheduleOnPostNotificationExecutor(this::postDatabaseChanged, 0),
this);
}
private void postDatabaseChanged() {
synchronized (dbLock) {
if (!isOpen() || (c4DbObserver == null)) { return; }
boolean external = false;
int nChanges;
List docIDs = new ArrayList<>();
do {
// Read changes in batches of kMaxChanges:
final C4DatabaseChange[] c4DbChanges = c4DbObserver.getChanges(MAX_CHANGES);
nChanges = (c4DbChanges == null) ? 0 : c4DbChanges.length;
final boolean newExternal = (nChanges > 0) && c4DbChanges[0].isExternal();
if ((!docIDs.isEmpty()) && ((nChanges <= 0) || (external != newExternal) || (docIDs.size() > 1000))) {
dbChangeNotifier.postChange(new DatabaseChange((Database) this, docIDs));
docIDs = new ArrayList<>();
}
external = newExternal;
for (int i = 0; i < nChanges; i++) { docIDs.add(c4DbChanges[i].getDocID()); }
}
while (nChanges > 0);
}
}
@GuardedBy("dbLock")
private void prepareDocument(Document document) throws CouchbaseLiteException {
mustBeOpen();
final Database db = document.getDatabase();
if (db == null) { document.setDatabase((Database) this); }
else if (db != this) {
throw new CouchbaseLiteException(
"DocumentAnotherDatabase",
CBLError.Domain.CBLITE,
CBLError.Code.INVALID_PARAMETER);
}
}
//////// RESOLVE REPLICATED CONFLICTS:
private void resolveConflictOnce(@Nullable ConflictResolver resolver, @NonNull String docID)
throws CouchbaseLiteException, CBLInternalException {
final Document localDoc;
final Document remoteDoc;
synchronized (dbLock) {
localDoc = Document.getDocument((Database) this, docID);
remoteDoc = getConflictingRevision(docID);
}
final Document resolvedDoc;
// If both docs have been deleted, we're done here
if (localDoc.isDeleted() && remoteDoc.isDeleted()) { resolvedDoc = remoteDoc; }
else {
// Resolve with conflict resolver:
resolvedDoc = resolveConflict(
(resolver != null) ? resolver : ConflictResolver.DEFAULT,
docID,
localDoc,
remoteDoc);
}
synchronized (dbLock) {
boolean commit = false;
beginTransaction();
try {
saveResolvedDocument(resolvedDoc, localDoc, remoteDoc);
commit = true;
}
finally { endTransaction(commit); }
}
}
private Document getConflictingRevision(@NonNull String docID)
throws CouchbaseLiteException, CBLInternalException {
final Document remoteDoc = Document.getDocument((Database) this, docID);
try {
if (!remoteDoc.selectConflictingRevision()) {
final String msg = "Unable to select conflicting revision for doc '" + docID + "'. Skipping.";
Log.w(DOMAIN, msg);
throw new CBLInternalException(CBLInternalException.FAILED_SELECTING_CONFLICTING_REVISION, msg);
}
}
catch (LiteCoreException e) { throw CBLStatus.convertException(e); }
return remoteDoc;
}
private Document resolveConflict(
@NonNull ConflictResolver resolver,
@NonNull String docID,
@NonNull Document localDoc,
@NonNull Document remoteDoc)
throws CouchbaseLiteException {
final Conflict conflict
= new Conflict(localDoc.isDeleted() ? null : localDoc, remoteDoc.isDeleted() ? null : remoteDoc);
Log.v(
DOMAIN,
"Resolving doc '%s' (local=%s and remote=%s) with resolver %s",
docID,
localDoc.getRevisionID(),
remoteDoc.getRevisionID(),
resolver);
final Document doc;
try { doc = resolver.resolve(conflict); }
catch (Exception err) {
final String msg = String.format(ERROR_RESOLVER_FAILED, docID, err.getLocalizedMessage());
Log.w(DOMAIN, msg, err);
throw new CouchbaseLiteException(msg, err, CBLError.Domain.CBLITE, CBLError.Code.UNEXPECTED_ERROR);
}
if (doc == null) { return null; }
final Database target = doc.getDatabase();
if (!this.equals(target)) {
if (target == null) { doc.setDatabase((Database) this); }
else {
final String msg = String.format(WARN_WRONG_DATABASE, docID, target.getName(), getName());
Log.w(DOMAIN, msg);
throw new CouchbaseLiteException(msg, CBLError.Domain.CBLITE, CBLError.Code.UNEXPECTED_ERROR);
}
}
if (!docID.equals(doc.getId())) {
Log.w(DOMAIN, WARN_WRONG_ID, doc.getId(), docID);
return new MutableDocument(docID, doc);
}
return doc;
}
// Call in a transaction
@GuardedBy("dbLock")
@SuppressWarnings("PMD.NPathComplexity")
private void saveResolvedDocument(
@Nullable Document resolvedDoc,
@NonNull Document localDoc,
@NonNull Document remoteDoc)
throws CouchbaseLiteException {
FLSliceResult mergedBody = null;
int mergedFlags = 0x00;
if (resolvedDoc == null) {
if (remoteDoc.isDeleted()) { resolvedDoc = remoteDoc; }
else if (localDoc.isDeleted()) { resolvedDoc = localDoc; }
}
if (resolvedDoc != null) {
if (resolvedDoc != localDoc) { resolvedDoc.setDatabase((Database) this); }
final C4Document c4Doc = resolvedDoc.getC4doc();
if (c4Doc != null) { mergedFlags = c4Doc.getSelectedFlags(); }
}
try {
// Unless the remote revision is being used as-is, we need a new revision:
if (resolvedDoc != remoteDoc) {
if ((resolvedDoc != null) && !resolvedDoc.isDeleted()) { mergedBody = resolvedDoc.encode(); }
else {
mergedFlags |= C4Constants.RevisionFlags.DELETED;
final FLEncoder enc = getSharedFleeceEncoder();
try {
enc.writeValue(new HashMap<>()); // Need an empty dictionary body
mergedBody = enc.finish2();
}
finally { enc.reset(); }
}
}
// Merged body:
final byte[] mergedBodyBytes = (mergedBody == null) ? null : mergedBody.getBuf();
// Ask LiteCore to do the resolution:
final C4Document rawDoc = Preconditions.assertNotNull(localDoc.getC4doc(), "raw doc is null");
// The remote branch has to win so that the doc revision history matches the server's.
rawDoc.resolveConflict(remoteDoc.getRevisionID(), localDoc.getRevisionID(), mergedBodyBytes, mergedFlags);
rawDoc.save(0);
Log.v(DOMAIN, "Conflict resolved as doc '%s' rev %s", rawDoc.getDocID(), rawDoc.getRevID());
}
catch (LiteCoreException e) {
throw CBLStatus.convertException(e);
}
finally {
if (mergedBody != null) { mergedBody.free(); }
}
}
private void saveWithConflictHandler(@NonNull MutableDocument document, @NonNull ConflictHandler handler)
throws CouchbaseLiteException {
Document oldDoc = null;
int n = 0;
while (true) {
if (n++ > MAX_CONFLICT_RESOLUTION_RETRIES) {
throw new CouchbaseLiteException(
"Too many attempts to resolve a conflicted document: " + n,
CBLError.Domain.CBLITE,
CBLError.Code.UNEXPECTED_ERROR);
}
try {
saveInternal(document, oldDoc, false, ConcurrencyControl.FAIL_ON_CONFLICT);
return;
}
catch (CouchbaseLiteException e) {
if (!CouchbaseLiteException.isConflict(e)) { throw e; }
}
// Conflict
synchronized (dbLock) { oldDoc = Document.getDocument((Database) this, document.getId()); }
try {
if (!handler.handle(document, (oldDoc.isDeleted()) ? null : oldDoc)) {
throw new CouchbaseLiteException(
"Conflict handler returned false",
CBLError.Domain.CBLITE,
CBLError.Code.CONFLICT
);
}
}
catch (Exception e) {
throw new CouchbaseLiteException(
"Conflict handler threw an exception",
e,
CBLError.Domain.CBLITE,
CBLError.Code.CONFLICT
);
}
}
}
// The main save method.
private void saveInternal(
@NonNull Document document,
@Nullable Document baseDoc,
boolean deleting,
@NonNull ConcurrencyControl concurrencyControl)
throws CouchbaseLiteException {
Preconditions.assertNotNull(document, "document");
Preconditions.assertNotNull(concurrencyControl, "concurrencyControl");
if (deleting && (!document.exists())) {
throw new CouchbaseLiteException(
"DeleteDocFailedNotSaved",
CBLError.Domain.CBLITE,
CBLError.Code.NOT_FOUND);
}
synchronized (dbLock) {
prepareDocument(document);
boolean commit = false;
beginTransaction();
try {
try {
saveInTransaction(document, (baseDoc == null) ? null : baseDoc.getC4doc(), deleting);
commit = true;
return;
}
catch (CouchbaseLiteException e) {
if (!CouchbaseLiteException.isConflict(e)) { throw e; }
}
// Conflict
// return false if FAIL_ON_CONFLICT
if (concurrencyControl.equals(ConcurrencyControl.FAIL_ON_CONFLICT)) {
throw new CouchbaseLiteException("Conflict", CBLError.Domain.CBLITE, CBLError.Code.CONFLICT);
}
commit = saveConflicted(document, deleting);
}
finally {
endTransaction(commit);
}
}
}
@GuardedBy("dbLock")
private boolean saveConflicted(@NonNull Document document, boolean deleting)
throws CouchbaseLiteException {
final C4Document curDoc;
try { curDoc = getC4Document(document.getId()); }
catch (LiteCoreException e) {
// here if deleting and the curDoc doesn't exist.
if (deleting
&& (e.domain == C4Constants.ErrorDomain.LITE_CORE)
&& (e.code == C4Constants.LiteCoreError.NOT_FOUND)) {
return false;
}
// here if the save failed.
throw CBLStatus.convertException(e);
}
// here if deleting and the curDoc has already been deleted
if (deleting && curDoc.deleted()) {
document.replaceC4Document(curDoc);
return false;
}
// Save changes on the current branch:
saveInTransaction(document, curDoc, deleting);
return true;
}
// Low-level save method
@GuardedBy("dbLock")
@SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE")
private void saveInTransaction(@NonNull Document document, @Nullable C4Document base, boolean deleting)
throws CouchbaseLiteException {
FLSliceResult body = null;
try {
int revFlags = 0;
if (deleting) { revFlags = C4Constants.RevisionFlags.DELETED; }
else if (!document.isEmpty()) {
// Encode properties to Fleece data:
body = document.encode();
if (C4Document.dictContainsBlobs(body, sharedKeys.getFLSharedKeys())) {
revFlags |= C4Constants.RevisionFlags.HAS_ATTACHMENTS;
}
}
// Save to database:
C4Document c4Doc = (base != null) ? base : document.getC4doc();
c4Doc = (c4Doc != null)
? c4Doc.update(body, revFlags)
: getC4DatabaseLocked().create(document.getId(), body, revFlags);
document.replaceC4Document(c4Doc);
}
catch (LiteCoreException e) {
throw CBLStatus.convertException(e);
}
finally {
if (body != null) { body.free(); }
}
}
@GuardedBy("dbLock")
private void purgeLocked(@NonNull String id) throws CouchbaseLiteException {
boolean commit = false;
beginTransaction();
try {
getC4DatabaseLocked().purgeDoc(id);
commit = true;
}
catch (LiteCoreException e) {
throw CBLStatus.convertException(e);
}
finally {
endTransaction(commit);
}
}
private void verifyActiveProcesses() {
final Set> processes;
final Set> deadProcesses = new HashSet<>();
synchronized (activeProcesses) { processes = new HashSet<>(activeProcesses); }
for (ActiveProcess> process: processes) {
if (!process.isActive()) {
Log.w(DOMAIN, "Found dead process: " + process);
deadProcesses.add(process);
}
}
if (!deadProcesses.isEmpty()) {
synchronized (activeProcesses) { activeProcesses.removeAll(deadProcesses); }
}
if (closeLatch == null) { return; }
final int activeProcessCount;
synchronized (activeProcesses) { activeProcessCount = activeProcesses.size(); }
Log.v(DOMAIN, "Active processes: %d", activeProcessCount);
if (activeProcessCount <= 0) { closeLatch.countDown(); }
}
private void shutdown(Fn.ConsumerThrows onShut) throws CouchbaseLiteException {
final C4Database c4Db;
synchronized (dbLock) {
c4Db = getC4DatabaseLocked();
c4Database = null;
// don't do any of this stuff in shell mode
if (name == null) { return; }
purgeStrategy.cancelPurges();
freeC4DbObserver();
docChangeNotifiers.clear();
closeLatch = new CountDownLatch(1);
Set> liveProcesses = null;
synchronized (activeProcesses) {
if (!activeProcesses.isEmpty()) { liveProcesses = new HashSet<>(activeProcesses); }
}
shutdownActiveProcesses(liveProcesses);
// the replicators won't be able to shut down until this lock is released
}
try {
for (int i = 0; ; i++) {
verifyActiveProcesses();
if ((i >= DB_CLOSE_MAX_RETRIES) && (closeLatch.getCount() > 0)) {
throw new IllegalStateException("Shutdown failed");
}
if (closeLatch.await(DB_CLOSE_WAIT_SECS, TimeUnit.SECONDS)) { break; }
}
}
catch (InterruptedException ignore) { }
synchronized (dbLock) {
try { onShut.accept(c4Db); }
catch (LiteCoreException e) { throw CBLStatus.convertException(e); }
}
shutdownExecutors(postExecutor, queryExecutor, EXECUTOR_CLOSE_MAX_WAIT_SECS);
}
@GuardedBy("dbLock")
private void freeC4DbObserver() {
final C4DatabaseObserver observer = c4DbObserver;
c4DbObserver = null;
if (observer == null) { return; }
observer.close();
}
// called from the finalizer
// be careful here:
// The call to 'stop' may cause a synchronous call to another method that modifies
// the passed collection! Since this thread already holds the lock, the call will
// execute immediately causing a concurrent modification exception.
private void shutdownActiveProcesses(Collection> processes) {
if (processes == null) { return; }
for (ActiveProcess> process: processes) { process.stop(); }
}
// called from the finalizer
private void shutdownExecutors(
ExecutionService.CloseableExecutor pExec,
ExecutionService.CloseableExecutor qExec,
int waitTime) {
// shutdown executor service
if (pExec != null) { pExec.stop(waitTime, TimeUnit.SECONDS); }
if (qExec != null) { qExec.stop(waitTime, TimeUnit.SECONDS); }
}
// Fix the bug in 2.8.0 that caused databases created in the
// default directory to be created in a *different* default directory.
// The fix is to use the original "real" default dir (the one used by all pre 2.8.0 code)
// and to copy a database from the "2.8" default directory into the "real" default
// directory as long as it won't overwrite anything that is already there.
private void fixHydrogenBug(@NonNull DatabaseConfiguration config, @NonNull String dbName)
throws CouchbaseLiteException {
// This is the real default directory
final String defaultDirPath = AbstractDatabaseConfiguration.getDbDirectory(null);
// Check to see if the rootDirPath refers to the default directory. If not, none of this is relevant.
// Both rootDir and defaultDir are canonical, so string comparison should work.
if (!defaultDirPath.equals(AbstractDatabaseConfiguration.getDbDirectory(config.getDirectory()))) { return; }
final File defaultDir = new File(defaultDirPath);
// If this database doesn't exist in the 2.8 default dir, were'r done here.
final File twoDotEightDefaultDir = new File(defaultDir, ".couchbase");
if (!exists(dbName, twoDotEightDefaultDir)) { return; }
// If this database already exists in the real default directory,
// we can't risk trashing it. We just use the database in the real default
// directory and leave well enough alone.
// It is *always* possible to use 2.8 database, by specifying
// its directory explicitly.
if (exists(dbName, defaultDir)) { return; }
// This database is in the 2.8 default dir but not in the real
// default dir. Copy it to where it belongs.
final File twoDotEightDb = getDatabaseFile(twoDotEightDefaultDir, dbName);
try { Database.copy(twoDotEightDb, dbName, config); }
catch (CouchbaseLiteException e) {
// Per review: If the copy fails, delete the partial DB
// and throw an exception. This is a poison pill.
// The db can only be opened by explicitly specifying 2.8.0 directory.
try { FileUtils.eraseFileOrDir(getDatabaseFile(defaultDir, dbName)); }
catch (Exception ignore) { }
throw e;
}
}
}