diff --git a/android-ktx/main/kotlin/com/couchbase/lite/internal/AbstractWorkManagerReplication.kt b/android-ktx/main/kotlin/com/couchbase/lite/internal/AbstractWorkManagerReplication.kt index 6eef3da2d..dabc19ca0 100644 --- a/android-ktx/main/kotlin/com/couchbase/lite/internal/AbstractWorkManagerReplication.kt +++ b/android-ktx/main/kotlin/com/couchbase/lite/internal/AbstractWorkManagerReplication.kt @@ -23,6 +23,8 @@ import com.couchbase.lite.ReplicatorType import java.security.cert.X509Certificate abstract class AbstractWorkManagerReplicatorConfiguration(protected val replConfig: ReplicatorConfiguration) { + val collections: Set by replConfig::collections + var type: ReplicatorType by replConfig::type var authenticator: Authenticator? by replConfig::authenticator var headers: Map? by replConfig::headers @@ -34,5 +36,20 @@ abstract class AbstractWorkManagerReplicatorConfiguration(protected val replConf } fun getConfig() = ReplicatorConfiguration(replConfig) + + fun addCollection(collection: Collection, collectionConfig: CollectionConfiguration? = null) { + replConfig.addCollection(collection, collectionConfig) + } + + fun addCollections( + collections: kotlin.collections.Collection, + collectionConfig: CollectionConfiguration? = null + ) { + replConfig.addCollections(collections, collectionConfig) + } + + fun removeCollection(collection: Collection) { + replConfig.removeCollection(collection) + } } diff --git a/common/main/java/com/couchbase/lite/AbstractDatabase.java b/common/main/java/com/couchbase/lite/AbstractDatabase.java index 2c12696ad..8c6f3294c 100644 --- a/common/main/java/com/couchbase/lite/AbstractDatabase.java +++ b/common/main/java/com/couchbase/lite/AbstractDatabase.java @@ -22,13 +22,16 @@ import java.io.File; import java.io.IOException; import java.net.URI; +import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import com.couchbase.lite.internal.CouchbaseLiteInternal; @@ -46,6 +49,8 @@ import com.couchbase.lite.internal.fleece.FLEncoder; import com.couchbase.lite.internal.fleece.FLSharedKeys; import com.couchbase.lite.internal.fleece.FLSliceResult; +import com.couchbase.lite.internal.listener.ChangeListenerToken; +import com.couchbase.lite.internal.listener.Listenable; import com.couchbase.lite.internal.logging.Log; import com.couchbase.lite.internal.logging.LogSinksImpl; import com.couchbase.lite.internal.replicator.ConflictResolutionException; @@ -77,7 +82,7 @@ "PMD.TooManyMethods", "PMD.CouplingBetweenObjects"}) abstract class AbstractDatabase extends BaseDatabase - implements AutoCloseable { + implements Listenable, AutoCloseable { //--------------------------------------------- // Constants //--------------------------------------------- @@ -126,6 +131,22 @@ public boolean equals(@Nullable Object o) { } } + // --------------------------------------------- + // API - public static fields + // --------------------------------------------- + + /** + * Gets the logging controller for the Couchbase Lite library to configure the + * logging settings and add custom logging. + * + * @deprecated Use LogSinks.get() + */ + // Public API. Do not fix the name. + @Deprecated + @SuppressWarnings({"ConstantName", "PMD.FieldNamingConventions"}) + @NonNull + public static final com.couchbase.lite.Log log = new com.couchbase.lite.Log(); + // --------------------------------------------- // API - public static methods // --------------------------------------------- @@ -620,6 +641,315 @@ public boolean equals(Object o) { return Objects.equals(getPath(), other.getPath()) && name.equals(other.name); } + //--------------------------------------------- + // Deprecated API methods + //--------------------------------------------- + + /** + * The number of documents in the default collection. + * + * @return the number of documents in the database. + * @deprecated Use getDefaultCollection().getCount() + */ + @Deprecated + public long getCount() { + try { + synchronized (getDbLock()) { return getDefaultCollectionLocked().getCount(); } + } + catch (CouchbaseLiteException e) { throw new CouchbaseLiteError("Failed getting default collection", e); } + } + + /** + * Gets an existing Document with the given ID from the default collection. + * If the document with the given ID doesn't exist in the default collection, + * the method will return null. If the default collection does not exist or if + * the database is closed, the method will throw an CouchbaseLiteError + * + * @param id the document ID + * @return the Document object or null + * @throws CouchbaseLiteError when the database is closed or the default collection has been deleted + * @deprecated Use getDefaultCollection().getDocument() + */ + @SuppressWarnings("PMD.PreserveStackTrace") + @Deprecated + @Nullable + public Document getDocument(@NonNull String id) { + synchronized (getDbLock()) { + try { return getDefaultCollectionOrThrow().getDocument(id); } + catch (CouchbaseLiteException e) { Log.i(LogDomain.DATABASE, "Failed retrieving document: %s", e, id); } + } + return null; + } + + /** + * Saves a document to the default collection. When write operations are executed + * concurrently, the last writer will overwrite all other written values. + * Calling this method is the same as calling save(MutableDocument, ConcurrencyControl.LAST_WRITE_WINS) + * + * @param document The document. + * @throws CouchbaseLiteException on error + * @deprecated Use getDefaultCollection().save + */ + @Deprecated + public void save(@NonNull MutableDocument document) throws CouchbaseLiteException { + save(document, ConcurrencyControl.LAST_WRITE_WINS); + } + + /** + * Saves a document to the default collection. 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 when there + * is a conflict and the method will return false + * + * @param document The document. + * @param concurrencyControl The concurrency control. + * @return true if successful. false if the FAIL_ON_CONFLICT concurrency + * @throws CouchbaseLiteException on error + * @deprecated Use getDefaultCollection().save() + */ + @Deprecated + public boolean save(@NonNull MutableDocument document, @NonNull ConcurrencyControl concurrencyControl) + throws CouchbaseLiteException { + return getDefaultCollectionOrThrow().save(document, concurrencyControl); + } + + /** + * Saves a document to the default collection. 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 + * @deprecated Use getDefaultCollection().save + */ + @Deprecated + public boolean save(@NonNull MutableDocument document, @NonNull ConflictHandler conflictHandler) + throws CouchbaseLiteException { + Preconditions.assertNotNull(document, "document"); + Preconditions.assertNotNull(conflictHandler, "conflictHandler"); + try { return getDefaultCollectionOrThrow().save(document, conflictHandler); } + catch (CouchbaseLiteException e) { + if (!(CBLError.Domain.CBLITE.equals(e.getDomain()) && (CBLError.Code.NOT_OPEN == e.getCode()))) { throw e; } + else { throw new CouchbaseLiteError(Log.lookupStandardMessage("DBClosedOrCollectionDeleted"), e); } + } + } + + /** + * Deletes a document from the default collection. When write operations are executed + * concurrently, the last writer will overwrite all other written values. + * Calling this function is the same as calling delete(Document, ConcurrencyControl.LAST_WRITE_WINS) + * + * @param document The document. + * @throws CouchbaseLiteException on error + * @deprecated Use getDefaultCollection().delete + */ + @Deprecated + public void delete(@NonNull Document document) throws CouchbaseLiteException { + delete(document, ConcurrencyControl.LAST_WRITE_WINS); + } + + /** + * Deletes a document from the default collection. 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 and the method will return false. + * + * @param document The document. + * @param concurrencyControl The concurrency control. + * @throws CouchbaseLiteException on error + * @deprecated Use getDefaultCollection().delete + */ + @Deprecated + public boolean delete(@NonNull Document document, @NonNull ConcurrencyControl concurrencyControl) + throws CouchbaseLiteException { + return getDefaultCollectionOrThrow().delete(document, concurrencyControl); + } + + /** + * Purges the passed document from the default collection. This is more drastic than delete(Document): + * it removes all local traces of the document. Purges will NOT be replicated to other databases. + * + * @param document the document to be purged. + * @deprecated Use getDefaultCollection().purge + */ + @Deprecated + public void purge(@NonNull Document document) throws CouchbaseLiteException { + getDefaultCollectionOrThrow().purge(document); + } + + /** + * Purges the document with the passed id from default collection. This is more drastic than delete(Document), + * it removes all local traces of the document. Purges will NOT be replicated to other databases. + * + * @param id the document ID + * @deprecated Use getDefaultCollection().purge + */ + @Deprecated + public void purge(@NonNull String id) throws CouchbaseLiteException { getDefaultCollectionOrThrow().purge(id); } + + /** + * Sets an expiration date for a document in the default collection. The document + * will be purged from the database at the set time. + * + * @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. + * @deprecated Use getDefaultCollection().setDocumentExpiration + */ + @Deprecated + public void setDocumentExpiration(@NonNull String id, @Nullable Date expiration) throws CouchbaseLiteException { + getDefaultCollectionOrThrow().setDocumentExpiration(id, expiration); + } + + /** + * Returns the expiration time of the document. If the document has no expiration time set, + * the method will return null. + * + * @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. + * @deprecated Use getDefaultCollection().getDocumentExpiration + */ + @Deprecated + @Nullable + public Date getDocumentExpiration(@NonNull String id) throws CouchbaseLiteException { + return getDefaultCollectionOrThrow().getDocumentExpiration(id); + } + + // - Change listeners: + + /** + * 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 + * @deprecated Use getDefaultCollection().addChangeListener + */ + @Deprecated + @NonNull + public ListenerToken addChangeListener(@NonNull DatabaseChangeListener listener) { + return addChangeListener(null, listener); + } + + /** + * 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 + * @deprecated Use getDefaultCollection().addChangeListener + */ + @Deprecated + @NonNull + public ListenerToken addChangeListener(@Nullable Executor executor, @NonNull DatabaseChangeListener listener) { + return getDefaultCollectionOrThrow().addChangeListener(executor, listener::changed); + } + + /** + * Adds a change listener for the changes that occur to the specified document, in the default collection. + * On the Android platform changes will be delivered on the UI thread. On other Java platforms changes + * will be delivered on an arbitrary thread. When developing a Java Desktop application using Swing or JavaFX + * for a UI that will be updated in response to a change, make sure to schedule the UI update + * on the UI thread by using SwingUtilities.invokeLater(Runnable) or Platform.runLater(Runnable) + * respectively. + * + * @deprecated Use getDefaultCollection().addDocumentChangeListener + */ + @Deprecated + @NonNull + public ListenerToken addDocumentChangeListener(@NonNull String docId, @NonNull DocumentChangeListener listener) { + return addDocumentChangeListener(docId, null, listener); + } + + /** + * Adds a change listener for the changes that occur to the specified document, in the default collection. + * Changes will be posted to the listener on the passed executor on which. + * 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. + * + * @deprecated Use getDefaultCollection().addDocumentChangeListener + */ + @Deprecated + @NonNull + public ListenerToken addDocumentChangeListener( + @NonNull String docId, + @Nullable Executor executor, + @NonNull DocumentChangeListener listener) { + return getDefaultCollectionOrThrow().addDocumentChangeListener(docId, executor, listener); + } + + /** + * Removes a change listener added to the default collection. + * + * @param token returned by a previous call to addChangeListener or addDocumentListener. + * @deprecated Use ListenerToken.remove + */ + @Deprecated + public void removeChangeListener(@NonNull ListenerToken token) { + Preconditions.assertNotNull(token, "token"); + if (!(token instanceof ChangeListenerToken)) { return; } + final Collection defaultCollection = getDefaultCollectionOrThrow(); + final String docId = ((ChangeListenerToken) token).getKey(); + // This hackery depends on the fact that we only set the keys for DocumentChangeListeners + if (docId == null) { defaultCollection.removeCollectionChangeListener(token); } + else { defaultCollection.removeDocumentChangeListener(token); } + } + + // - Indices: + + /** + * Get a list of the names of indices on the default collection. + * + * @return the list of index names + * @throws CouchbaseLiteException on failure + * @deprecated Use getDefaultCollection().getIndexes + */ + @Deprecated + @NonNull + public List getIndexes() throws CouchbaseLiteException { + return new ArrayList<>(getDefaultCollectionOrThrow().getIndexes()); + } + + /** + * Add an index to the default collection. + * + * @param name index name + * @param index index description + * @throws CouchbaseLiteException on failure + * @deprecated Use getDefaultCollection().createIndex + */ + @Deprecated + public void createIndex(@NonNull String name, @NonNull Index index) throws CouchbaseLiteException { + getDefaultCollectionOrThrow().createIndexInternal(name, index); + } + + /** + * Add an index to the default collection. + * + * @param name index name + * @param config index configuration + * @throws CouchbaseLiteException on failure + * @deprecated Use getDefaultCollection().createIndex + */ + @Deprecated + public void createIndex(@NonNull String name, @NonNull IndexConfiguration config) throws CouchbaseLiteException { + getDefaultCollectionOrThrow().createIndexInternal(name, config); + } + + /** + * Delete the named index from the default collection. + * + * @param name name of the index to delete + * @throws CouchbaseLiteException on failure + * @deprecated Use getDefaultCollection().deleteIndex + */ + @Deprecated + public void deleteIndex(@NonNull String name) throws CouchbaseLiteException { + getDefaultCollectionOrThrow().deleteIndex(name); + } //--------------------------------------------- // Protected access @@ -969,6 +1299,19 @@ FLEncoder getSharedFleeceEncoder() { // - Collection: + @NonNull + private Collection getDefaultCollectionOrThrow() { + try { + synchronized (getDbLock()) { + assertOpenUnchecked(); + return getDefaultCollectionLocked(); + } + } + catch (CouchbaseLiteException e) { + throw new CouchbaseLiteError(Log.lookupStandardMessage("DBClosedOrCollectionDeleted"), e); + } + } + @NonNull private Collection getDefaultCollectionLocked() throws CouchbaseLiteException { if (defaultCollection == null) { defaultCollection = Collection.getDefaultCollection(getDatabase()); } @@ -1158,19 +1501,8 @@ private void saveResolvedDocumentWithFlags( throws LiteCoreException { byte[] mergedBodyBytes = null; - // Determine the actual winner and loser based on which doc was chosen - final String winningRevID; - final String losingRevID; - if (resolvedDoc == localDoc) { - winningRevID = localDoc.getRevisionID(); - losingRevID = remoteDoc.getRevisionID(); - } else { - winningRevID = remoteDoc.getRevisionID(); - losingRevID = localDoc.getRevisionID(); - } - - // Generate merged body only for the merge case (not local-wins or remote-wins) - if (resolvedDoc != localDoc && resolvedDoc != remoteDoc) { + // Unless the remote revision is being used as-is, we need a new revision: + if (resolvedDoc != remoteDoc) { if ((resolvedDoc == null) || resolvedDoc.isDeleted()) { mergedFlags |= C4Constants.RevisionFlags.DELETED; try (FLEncoder enc = getSharedFleeceEncoder()) { @@ -1192,8 +1524,8 @@ private void saveResolvedDocumentWithFlags( // Ask LiteCore to do the resolution: final C4Document rawDoc = Preconditions.assertNotNull(localDoc.getC4doc(), "raw doc is null"); - // Pass the actual winner and loser revision IDs - rawDoc.resolveConflict(winningRevID, losingRevID, mergedBodyBytes, mergedFlags); + // 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.d(DOMAIN, "Conflict resolved as doc '%s' rev %s", localDoc.getId(), rawDoc.getRevID()); diff --git a/common/main/java/com/couchbase/lite/AbstractQuery.java b/common/main/java/com/couchbase/lite/AbstractQuery.java index bef869a12..a35620ee0 100644 --- a/common/main/java/com/couchbase/lite/AbstractQuery.java +++ b/common/main/java/com/couchbase/lite/AbstractQuery.java @@ -245,6 +245,15 @@ public ListenerToken addChangeListener(@Nullable Executor executor, @NonNull Que return token; } + /** + * Removes a change listener wih the given listener token. + * + * @param token The listener token. + * @deprecated use ListenerToken.remove() + */ + @Deprecated + @Override + public void removeChangeListener(@NonNull ListenerToken token) { removeListener(token); } @Nullable protected abstract AbstractDatabase getDatabase(); diff --git a/common/main/java/com/couchbase/lite/AbstractReplicator.java b/common/main/java/com/couchbase/lite/AbstractReplicator.java index 753f886b9..dfa7f91f0 100644 --- a/common/main/java/com/couchbase/lite/AbstractReplicator.java +++ b/common/main/java/com/couchbase/lite/AbstractReplicator.java @@ -337,6 +337,18 @@ public List getServerCertificates() { : new ArrayList<>(serverCerts); } + /** + * Get a best effort list of documents in the default collection, that are still pending replication. + * + * @return a set of ids for documents in the default collection still awaiting replication. + * @deprecated Use getPendingDocumentIds(Collection) + */ + @Deprecated + @NonNull + public Set getPendingDocumentIds() throws CouchbaseLiteException { + return getPendingDocIds(Scope.DEFAULT_NAME, Collection.DEFAULT_NAME); + } + /** * Get a best effort list of documents in the passed collection that are still pending replication. * @@ -347,6 +359,19 @@ public Set getPendingDocumentIds(@NonNull Collection collection) throws return getPendingDocIds(collection.getScope().getName(), collection.getName()); } + /** + * Best effort check to see if the document whose ID is passed is still pending replication. + * + * @param docId Document id + * @return true if the document is pending + * @deprecated Use isDocumentPending(String, Collection) + */ + @Deprecated + public boolean isDocumentPending(@NonNull String docId) + throws CouchbaseLiteException { + return isDocPending(docId, Scope.DEFAULT_NAME, Collection.DEFAULT_NAME); + } + /** * Best effort check to see if the document whose ID is passed is still pending replication. * @@ -438,6 +463,29 @@ public ListenerToken addDocumentReplicationListener( return token; } + /** + * Remove the given ReplicatorChangeListener or DocumentReplicationListener from the this replicator. + * + * @param token returned by a previous call to addChangeListener or addDocumentListener. + * @deprecated use ListenerToken.remove + */ + @Deprecated + public void removeChangeListener(@NonNull ListenerToken token) { + Preconditions.assertNotNull(token, "token"); + synchronized (getReplicatorLock()) { + if (token instanceof ReplicatorChangeListenerToken) { + removeReplicationListener(token); + return; + } + + if (token instanceof DocumentReplicationListenerToken) { + removeDocumentReplicationListener(token); + return; + } + + throw new IllegalArgumentException("unexpected token: " + token); + } + } // I've thought a lot about how to implement this. The problem is that you cannot, fundamentally, // close a replicator(discard its resources before this method returns). If it is not stopped, diff --git a/common/main/java/com/couchbase/lite/AbstractReplicatorConfiguration.java b/common/main/java/com/couchbase/lite/AbstractReplicatorConfiguration.java index 3355f135b..5148f029a 100644 --- a/common/main/java/com/couchbase/lite/AbstractReplicatorConfiguration.java +++ b/common/main/java/com/couchbase/lite/AbstractReplicatorConfiguration.java @@ -21,15 +21,19 @@ import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import com.couchbase.lite.internal.BaseReplicatorConfiguration; import com.couchbase.lite.internal.ImmutableReplicatorConfiguration; +import com.couchbase.lite.internal.logging.Log; import com.couchbase.lite.internal.utils.CertUtils; +import com.couchbase.lite.internal.utils.Fn; import com.couchbase.lite.internal.utils.Preconditions; @@ -77,6 +81,22 @@ private static void checkDuration(String name, long duration) { } } + @Nullable + protected static Map configureDefaultCollection(@Nullable Database db) { + if (db == null) { return null; } + + final Collection defaultCollection; + try { defaultCollection = db.getDefaultCollection(); } + catch (CouchbaseLiteException e) { + throw new CouchbaseLiteError(Log.lookupStandardMessage("NoDefaultCollectionInConfig"), e); + } + + final Map collections = new HashMap<>(); + collections.put(defaultCollection, new CollectionConfiguration()); + + return collections; + } + @NonNull protected static Map createCollectionConfigMap( @NonNull java.util.Collection configs) { @@ -94,10 +114,10 @@ protected static Map createCollectionConfig return map; } - @NonNull + @Nullable private static Map copyConfigs( - @NonNull Map configs) { - return new HashMap<>(configs); + @Nullable Map configs) { + return (configs == null) ? null : new HashMap<>(configs); } //--------------------------------------------- @@ -133,7 +153,7 @@ private static Map copyConfigs( @SuppressWarnings({"PMD.ExcessiveParameterList", "PMD.ArrayIsStoredDirectly"}) protected AbstractReplicatorConfiguration( @Nullable Database db, - @NonNull Map collections, + @Nullable Map collections, @NonNull Endpoint target) { this( collections, @@ -194,7 +214,7 @@ protected AbstractReplicatorConfiguration(@NonNull AbstractReplicatorConfigurati // They are, therefore, effectively immutable @SuppressWarnings("PMD.ExcessiveParameterList") private AbstractReplicatorConfiguration( - @NonNull Map collections, + @Nullable Map collections, @NonNull Endpoint target, @NonNull com.couchbase.lite.ReplicatorType type, boolean continuous, @@ -244,6 +264,65 @@ private AbstractReplicatorConfiguration( // Setters //--------------------------------------------- + /** + * Add a collection used for the replication with an optional collection configuration. + * If the collection has been added before, the previously added collection + * and its configuration if specified will be replaced. + * + * @param collection the collection + * @param config its configuration + * @return this + * + * @deprecated Use ReplicatorConfiguration(java.util.Collection<CollectionConfiguration>, Endpoint) instead. + */ + @Deprecated + @NonNull + public final ReplicatorConfiguration addCollection( + @NonNull Collection collection, + @Nullable CollectionConfiguration config) { + addCollectionConfig( + collection, + (config == null) ? new CollectionConfiguration() : new CollectionConfiguration(config)); + return getReplicatorConfiguration(); + } + + /** + * Add multiple collections used for the replication with an optional shared collection configuration. + * If any of the collections have been added before, the previously added collections and their + * configuration if specified will be replaced. Adding an empty collection array is a no-op. + * + * @param collections a collection of Collections + * @param config the configuration to be applied to all of the collections + * @return this + * + * @deprecated Use ReplicatorConfiguration(java.util.Collection<CollectionConfiguration>, Endpoint) instead. + */ + @Deprecated + @NonNull + public final ReplicatorConfiguration addCollections( + @NonNull java.util.Collection collections, + @Nullable CollectionConfiguration config) { + // Use a single config instance for all of the collections + if (config == null) { config = new CollectionConfiguration(); } + for (Collection collection: collections) { addCollectionConfig(collection, config); } + return getReplicatorConfiguration(); + } + + /** + * Remove a collection from the replication. + * + * @param collection the collection to be removed + * @return this + * + * @deprecated Use ReplicatorConfiguration(java.util.Collection<CollectionConfiguration>, Endpoint) instead. + */ + @Deprecated + @NonNull + public final ReplicatorConfiguration removeCollection(@NonNull Collection collection) { + removeCollectionInternal(collection); + return getReplicatorConfiguration(); + } + /** * Sets the replicator type indicating the direction of the replicator. * The default is ReplicatorType.PUSH_AND_PULL: bi-directional replication. @@ -449,6 +528,101 @@ public final ReplicatorConfiguration setPinnedServerCertificate(@Nullable byte[] return getReplicatorConfiguration(); } + /** + * A collection of document IDs identifying documents to be replicated. + * If non-empty, only documents with IDs in this collection will be pushed and/or pulled. + * Default is empty: do not filter documents. + * + * @param documentIDs The document IDs. + * @return this. + * @deprecated Use CollectionConfiguration.setDocumentIDs + */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + @NonNull + public final ReplicatorConfiguration setDocumentIDs(@Nullable List documentIDs) { + updateValidDefaultConfigOrThrow( + config -> config.setDocumentIDs((documentIDs == null) ? null : new ArrayList<>(documentIDs))); + return getReplicatorConfiguration(); + } + + /** + * Sets a collection of Sync Gateway channel names from which to pull Documents. + * If unset, all accessible channels will be pulled. + * Default is empty: pull from all accessible channels. + *

+ * Note: Channel specifications apply only to replications + * pulling from a SyncGateway and only the channels visible + * to the authenticated user. Channel specs are ignored: + *

    + *
  • during a push replication.
  • + *
  • during peer-to-peer or database-to-database replication
  • + *
  • when the specified channel is not accessible to the user
  • + *
+ * + * @param channels The Sync Gateway channel names. + * @return this. + * @deprecated Use CollectionConfiguration.setChannels + */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + @NonNull + public final ReplicatorConfiguration setChannels(@Nullable List channels) { + updateValidDefaultConfigOrThrow( + config -> config.setChannels((channels == null) ? null : new ArrayList<>(channels))); + return getReplicatorConfiguration(); + } + + /** + * Sets the the conflict resolver. + * Default is ConflictResolver.DEFAULT + * + * @param conflictResolver A conflict resolver. + * @return this. + * @deprecated Use CollectionConfiguration.setConflictResolver + */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + @NonNull + public final ReplicatorConfiguration setConflictResolver(@Nullable ConflictResolver conflictResolver) { + updateValidDefaultConfigOrThrow(config -> config.setConflictResolver(conflictResolver)); + return getReplicatorConfiguration(); + } + + /** + * Sets a filter object for validating whether the documents can be pulled from the + * remote endpoint. Only documents for which the object returns true are replicated. + * Default is no filter. + * + * @param pullFilter The filter to filter the document to be pulled. + * @return this. + * @deprecated Use CollectionConfiguration.setPullFilter + */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + @NonNull + public final ReplicatorConfiguration setPullFilter(@Nullable ReplicationFilter pullFilter) { + updateValidDefaultConfigOrThrow(config -> config.setPullFilter(pullFilter)); + return getReplicatorConfiguration(); + } + + /** + * Sets a filter object for validating whether the documents can be pushed + * to the remote endpoint. + * Default is no filter. + * + * @param pushFilter The filter to filter the document to be pushed. + * @return this. + * @deprecated Use CollectionConfiguration.setPushFilter + */ + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + @NonNull + public final ReplicatorConfiguration setPushFilter(@Nullable ReplicationFilter pushFilter) { + updateValidDefaultConfigOrThrow(config -> config.setPushFilter(pushFilter)); + return getReplicatorConfiguration(); + } + //--------------------------------------------- // Getters //--------------------------------------------- @@ -459,13 +633,39 @@ public final ReplicatorConfiguration setPinnedServerCertificate(@Nullable byte[] @NonNull public final Endpoint getTarget() { return target; } + /** + * Get the CollectionConfiguration for the passed Collection. + * + * @param collection a collection whose configuration is sought. + * @return the collections configuration + * + * @deprecated Use getCollectionConfigs() instead. + */ + @Deprecated + @Nullable + public final CollectionConfiguration getCollectionConfiguration(@NonNull Collection collection) { + final CollectionConfiguration config = collectionConfigurations.get(collection); + return (config == null) ? null : new CollectionConfiguration(config); + } + + /** + * Return the list of collections in the replicator configuration + * + * @deprecated Use getCollectionConfigs() instead. + * This method will return {@code Set} in the next major release. + */ + @Deprecated + @NonNull + public final Set getCollections() { return new HashSet<>(collectionConfigurations.keySet()); } + /** * Returns a copy of the collection configurations associated with this replicator configuration. * * @return a set of {@link CollectionConfiguration} objects. + * @apiNote This method will be renamed to {@code getCollections()} in the next major release. */ @NonNull - public final Set getCollections() { + public final Set getCollectionConfigs() { return new HashSet<>(collectionConfigurations.values()); } @@ -586,6 +786,84 @@ public final byte[] getPinnedServerCertificate() { } } + /** + * Return the local database to replicate with the replication target. + * + * @deprecated Use Collection.getDatabase + */ + @Deprecated + @NonNull + public final Database getDatabase() { + if (database != null) { return database; } + // Can't change the nullity of this method: it has to throw. + throw new CouchbaseLiteError("No database or collections provided for replication configuration"); + } + + /** + * A collection of document IDs to filter: if not nil, only documents with these IDs will be pushed + * and/or pulled. + * + * @deprecated Use CollectionConfiguration.getDocumentIDs + */ + @Deprecated + @Nullable + public final List getDocumentIDs() { + final List docIds = getValidDefaultConfigOrThrow().getDocumentIDs(); + return (docIds == null) ? null : new ArrayList<>(docIds); + } + + /** + * Gets the collection of Sync Gateway channel names from which to pull documents. + * If unset, all accessible channels will be pulled. + * Default is empty: pull from all accessible channels. + *

+ * Note: Channel specifications apply only to replications + * pulling from a SyncGateway and only the channels visible + * to the authenticated user. Channel specs are ignored: + *

    + *
  • during a push replication.
  • + *
  • during peer-to-peer or database-to-database replication
  • + *
  • when the specified channel is not accessible to the user
  • + *
+ * + * @deprecated Use CollectionConfiguration.getChannels + */ + @Deprecated + @Nullable + public final List getChannels() { + final List channels = getValidDefaultConfigOrThrow().getChannels(); + return (channels == null) ? null : new ArrayList<>(channels); + } + + /** + * Return the conflict resolver. + * + * @deprecated Use CollectionConfiguration.getConflictResolver + */ + @Deprecated + @Nullable + public final ConflictResolver getConflictResolver() { return getValidDefaultConfigOrThrow().getConflictResolver(); } + + /** + * Gets the filter used to determine whether a document will be pulled + * from the remote endpoint. + * + * @deprecated Use CollectionConfiguration.getPullFilter + */ + @Deprecated + @Nullable + public final ReplicationFilter getPullFilter() { return getValidDefaultConfigOrThrow().getPullFilter(); } + + /** + * Gets a filter used to determine whether a document will be pushed + * to the remote endpoint. + * + * @deprecated Use CollectionConfiguration.getPushFilter + */ + @Deprecated + @Nullable + public final ReplicationFilter getPushFilter() { return getValidDefaultConfigOrThrow().getPushFilter(); } + @SuppressWarnings("PMD.NPathComplexity") @NonNull @Override @@ -623,4 +901,70 @@ public String toString() { @NonNull abstract ReplicatorConfiguration getReplicatorConfiguration(); + + //--------------------------------------------- + // Private + //--------------------------------------------- + + private void addCollectionConfig(@NonNull Collection collection, @NonNull CollectionConfiguration config) { + Preconditions.assertThat( + config.getCollection() == null || config.getCollection() == collection, + "CollectionConfiguration collection must be null or match the given collection." + ); + final Database db = Preconditions.assertNotNull(collection, "collection").getDatabase(); + if (database == null) { database = db; } + else { + if (!database.equals(db)) { + throw new IllegalArgumentException( + Log.formatStandardMessage("AddCollectionFromAnotherDB", collection.toString(), database.getName())); + } + } + + if (!database.isOpen()) { + throw new IllegalArgumentException( + Log.formatStandardMessage("AddCollectionFromClosedDB", collection.toString(), database.getName())); + } + + try (Collection coll = database.getCollection(collection.getName(), collection.getScope().getName())) { + if (coll == null) { + throw new IllegalArgumentException( + Log.formatStandardMessage("AddDeletedCollection", collection.toString())); + } + } + catch (CouchbaseLiteException e) { + throw new IllegalArgumentException("Failed getting collection " + collection, e); + } + + addCollectionInternal(collection, config); + } + + @NonNull + private CollectionConfiguration getValidDefaultConfigOrThrow() { return getAndUpdateDefaultConfig(null); } + + private void updateValidDefaultConfigOrThrow(@NonNull Fn.Consumer updater) { + getAndUpdateDefaultConfig(updater); + } + + @NonNull + private CollectionConfiguration getAndUpdateDefaultConfig(@Nullable Fn.Consumer updater) { + final Collection defaultCollection = Fn.firstOrNull(collectionConfigurations.keySet(), Collection::isDefault); + if (defaultCollection == null) { + throw new IllegalArgumentException("Cannot use legacy parameters when there is no default collection"); + } + + CollectionConfiguration config = collectionConfigurations.get(defaultCollection); + if (config == null) { + throw new IllegalArgumentException( + "Cannot use legacy parameters when the default collection has no configuration"); + } + + // Copy on write... + if (updater != null) { + config = new CollectionConfiguration(config); + updater.accept(config); + addCollectionInternal(defaultCollection, config); + } + + return config; + } } diff --git a/common/main/java/com/couchbase/lite/CollectionConfiguration.java b/common/main/java/com/couchbase/lite/CollectionConfiguration.java index 6fdf4b376..7239e151f 100644 --- a/common/main/java/com/couchbase/lite/CollectionConfiguration.java +++ b/common/main/java/com/couchbase/lite/CollectionConfiguration.java @@ -27,9 +27,8 @@ public class CollectionConfiguration { - @NonNull - private final Collection collection; @Nullable + private Collection collection; private List channels; @Nullable private List documentIDs; @@ -44,6 +43,41 @@ public class CollectionConfiguration { // Constructors //--------------------------------------------- + /** + * Creates a configuration instance. + * + * @deprecated This constructor is deprecated. Use {@link #CollectionConfiguration(Collection)} + * and setter methods to configure channels, filters, and a custom conflict resolver. + */ + @Deprecated + public CollectionConfiguration() { } + + /** + * Creates a configuration instance. + * + * @deprecated This constructor is deprecated. Use {@link #CollectionConfiguration(Collection)} + * and setter methods to configure channels, filters, and a custom conflict resolver. + * + * @param channels The list of channels to pull from Sync Gateway. + * @param documentIDs The list of document IDs to filter replication. + * @param pullFilter The filter function for pulling documents. + * @param pushFilter The filter function for pushing documents. + * @param conflictResolver The custom conflict resolver. + */ + @Deprecated + public CollectionConfiguration( + @Nullable List channels, + @Nullable List documentIDs, + @Nullable ReplicationFilter pullFilter, + @Nullable ReplicationFilter pushFilter, + @Nullable ConflictResolver conflictResolver) { + this.channels = channels; + this.documentIDs = documentIDs; + this.pullFilter = pullFilter; + this.pushFilter = pushFilter; + this.conflictResolver = conflictResolver; + } + /** * Creates a new configuration instance for the given collection. * @@ -182,7 +216,7 @@ public final CollectionConfiguration setPushFilter(@Nullable ReplicationFilter p /** * Returns the collection. */ - @NonNull + @Nullable public final Collection getCollection() { return collection; } @@ -235,13 +269,13 @@ public final Collection getCollection() { @NonNull public String toString() { return "CollectionConfiguration{" - + "(" - + (pullFilter != null ? "<" : "") - + (conflictResolver != null ? "!" : "") - + (pushFilter != null ? ">" : "") - + "): " - + channels + ", " - + documentIDs + "}" - + ", collection=" + collection.getFullName(); + + "(" + + ((pullFilter != null) ? "<" : "") + + ((conflictResolver != null) ? "!" : "") + + ((pushFilter != null) ? ">" : "") + + "): " + + channels + ", " + + documentIDs + "}" + + (collection != null ? ", collection=" + collection.getFullName() : ""); } } diff --git a/common/main/java/com/couchbase/lite/ConsoleLogger.java b/common/main/java/com/couchbase/lite/ConsoleLogger.java new file mode 100644 index 000000000..2861fa501 --- /dev/null +++ b/common/main/java/com/couchbase/lite/ConsoleLogger.java @@ -0,0 +1,145 @@ +// +// Copyright (c) 2020 Couchbase, Inc. +// +// 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.Set; + +import com.couchbase.lite.internal.utils.Preconditions; +import com.couchbase.lite.logging.ConsoleLogSink; +import com.couchbase.lite.logging.LogSinks; + + +/** + * A class that sends log messages to Android's system log, available via 'adb logcat'. + * + * @deprecated Use com.couchbase.lite.logging.ConsoleLogSink + */ +@SuppressWarnings({"PMD.UnnecessaryFullyQualifiedName", "DeprecatedIsStillUsed"}) +@Deprecated +public class ConsoleLogger implements Logger { + @VisibleForTesting + static class ShimLogger extends ConsoleLogSink { + ShimLogger(@NonNull LogLevel level, @Nullable EnumSet logDomains) { super(level, logDomains); } + + protected void writeLog(@NonNull LogLevel level, @NonNull LogDomain domain, @NonNull String message) { + doWriteLog(level, domain, message); + } + + @VisibleForTesting + protected void doWriteLog(@NonNull LogLevel level, @NonNull LogDomain domain, @NonNull String message) { + final ConsoleLogSink curLogger = LogSinks.get().getConsole(); + if (this == curLogger) { super.writeLog(level, domain, message); } + } + + void doLog(@NonNull LogLevel level, @NonNull LogDomain domain, @NonNull String message) { + super.log(level, domain, message); + } + + protected boolean isLegacy() { return true; } + } + + + @Nullable + private ShimLogger logger; + + ConsoleLogger() { } + + /** + * Gets the level that will be logged by the current logger. + * + * @return The maximum level to log + */ + @NonNull + public LogLevel getLevel() { + final ConsoleLogSink curLogger = LogSinks.get().getConsole(); + return (curLogger == null) ? LogLevel.NONE : curLogger.getLevel(); + } + + /** + * Sets the lowest level that will be logged to the console. + * + * @param level The lowest (most verbose) level to include in the logs + */ + public void setLevel(@NonNull LogLevel level) { + Preconditions.assertNotNull(level, "level"); + + // if the logging level has changed, install a new logger with the new level + final LogLevel curLevel = getLevel(); + if (curLevel == level) { return; } + + logger = shimFactory(level, getDomains()); + LogSinks.get().setConsole(logger); + } + + /** + * Get the set of domains currently being logged to the console. + * + * @return The currently active domains + */ + @NonNull + public EnumSet getDomains() { + final ConsoleLogSink curLogger = LogSinks.get().getConsole(); + if (curLogger == null) { return EnumSet.noneOf(LogDomain.class); } + final Set curDomains = curLogger.getDomains(); + return (curDomains.isEmpty()) + ? EnumSet.noneOf(LogDomain.class) + : EnumSet.copyOf(curDomains); + } + + /** + * Sets the domains that will be considered for writing to the console log. + * + * @param domains The domains to make active + */ + public void setDomains(@NonNull EnumSet domains) { + final LogSinks loggers = LogSinks.get(); + + // trivial optimization: if the domain filter hasn't changed, we're done + final ConsoleLogSink curLogger = loggers.getConsole(); + if ((curLogger != null) && domains.equals(curLogger.getDomains())) { return; } + + // otherwise, install a new shim + logger = shimFactory(getLevel(), Preconditions.assertNotNull(domains, "domains")); + loggers.setConsole(logger); + } + + /** + * Sets the domains that will be considered for writing to the console log. + * + * @param domains The domains to make active (vararg) + */ + public void setDomains(@NonNull LogDomain... domains) { + Preconditions.assertNotNull(domains, "domains"); + setDomains((domains.length <= 0) ? EnumSet.noneOf(LogDomain.class) : EnumSet.copyOf(Arrays.asList(domains))); + } + + @Override + public void log(@NonNull LogLevel level, @NonNull LogDomain domain, @NonNull String message) { + if (logger != null) { logger.doLog(level, domain, message); } + } + + @VisibleForTesting + @NonNull + ShimLogger shimFactory(@NonNull LogLevel level, @NonNull EnumSet domains) { + return new ShimLogger(level, domains); + } +} diff --git a/common/main/java/com/couchbase/lite/DataSource.java b/common/main/java/com/couchbase/lite/DataSource.java index 23ae3e948..03b2adbbc 100644 --- a/common/main/java/com/couchbase/lite/DataSource.java +++ b/common/main/java/com/couchbase/lite/DataSource.java @@ -56,6 +56,27 @@ public DataSource as(@NonNull String alias) { } } + /** + * Create a database as a data source. + * + * @param database the database used as a source of data for query. + * @return {@code DataSource.Database} object. + * @deprecated use DataSource.collection(Collection) + */ + @Deprecated + @NonNull + public static As database(@NonNull Database database) { + Preconditions.assertNotNull(database, "database"); + + final Collection defaultCollection; + try { defaultCollection = database.getDefaultCollection(); } + catch (CouchbaseLiteException e) { throw new IllegalArgumentException("Database not open", e); } + + final As source = new As(defaultCollection); + source.as(database.getName()); + return source; + } + /** * Create a collection as a data source. * diff --git a/common/main/java/com/couchbase/lite/DocumentChange.java b/common/main/java/com/couchbase/lite/DocumentChange.java index da443bd03..40f7b1ba7 100644 --- a/common/main/java/com/couchbase/lite/DocumentChange.java +++ b/common/main/java/com/couchbase/lite/DocumentChange.java @@ -48,4 +48,13 @@ public final class DocumentChange { @NonNull @Override public String toString() { return "DocumentChange{" + collection + ", " + documentID + "}"; } + + /** + * Return the Database instance + * + * @deprecated Use DocumentChange.getCollection() + */ + @Deprecated + @NonNull + public Database getDatabase() { return collection.getDatabase(); } } diff --git a/common/main/java/com/couchbase/lite/Expression.java b/common/main/java/com/couchbase/lite/Expression.java index 059bfded2..3d93e2649 100644 --- a/common/main/java/com/couchbase/lite/Expression.java +++ b/common/main/java/com/couchbase/lite/Expression.java @@ -867,4 +867,30 @@ public Expression in(@NonNull List expressions) { @SuppressWarnings("PMD.LinguisticNaming") @NonNull public Expression isNotValued() { return negated(new UnaryExpression(this, UnaryExpression.OP_VALUED)); } + + /** + * Creates an IS NULL OR MISSING expression that evaluates whether or not the current + * expression is null or missing. + * + * @return An IS NULL expression. + * @deprecated Use Expression.isNotValued + */ + @Deprecated + @SuppressWarnings("PMD.LinguisticNaming") + @NonNull + public Expression isNullOrMissing() { + return new UnaryExpression(this, UnaryExpression.OP_NULL) + .or(new UnaryExpression(this, UnaryExpression.OP_MISSING)); + } + + /** + * Creates an NOT IS NULL OR MISSING expression that evaluates whether or not the current + * expression is NOT null or missing. + * + * @return An NOT IS NULL expression. + * @deprecated Use Expression.isValued + */ + @Deprecated + @NonNull + public Expression notNullOrMissing() { return negated(isNullOrMissing()); } } diff --git a/common/main/java/com/couchbase/lite/FileLogger.java b/common/main/java/com/couchbase/lite/FileLogger.java new file mode 100644 index 000000000..23cf141ad --- /dev/null +++ b/common/main/java/com/couchbase/lite/FileLogger.java @@ -0,0 +1,143 @@ +// +// Copyright (c) 2020 Couchbase, Inc. +// +// 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +import com.couchbase.lite.internal.logging.Log; +import com.couchbase.lite.internal.utils.Preconditions; +import com.couchbase.lite.logging.FileLogSink; +import com.couchbase.lite.logging.LogSinks; + + +/** + * A logger for writing to a file in the application's storage so + * that log messages can persist durably after the application has + * stopped or encountered a problem. Each log level is written to + * a separate file. + * + * @deprecated Use com.couchbase.lite.logging.FileLogSink + */ +@SuppressWarnings("PMD.UnnecessaryFullyQualifiedName") +@Deprecated +public final class FileLogger implements Logger { + private static final class ShimLogger extends FileLogSink { + ShimLogger(@NonNull FileLogSink.Builder builder) { super(builder); } + + @Override + protected void writeLog(@NonNull LogLevel level, @NonNull LogDomain domain, @NonNull String message) { + final FileLogSink curLogger = LogSinks.get().getFile(); + if (this == curLogger) { super.writeLog(level, domain, message); } + } + + void doLog(@NonNull LogLevel level, @NonNull LogDomain domain, @NonNull String message) { + super.log(level, domain, message); + } + + protected boolean isLegacy() { return true; } + } + + @Nullable + private LogFileConfiguration configuration; + @Nullable + private ShimLogger logger; + + FileLogger() { } + + /** + * Gets the level that will be logged via this logger. + * + * @return The maximum level to log + */ + @Override + @NonNull + public LogLevel getLevel() { + final FileLogSink curLogger = LogSinks.get().getFile(); + return (curLogger == null) ? LogLevel.NONE : curLogger.getLevel(); + } + + /** + * Sets the lowest level that will be logged to the logging files. + * + * @param level The lowest (most verbose) level to include in the logs + */ + public void setLevel(@NonNull LogLevel level) { + Preconditions.assertNotNull(level, "level"); + + final LogFileConfiguration config = configuration; + if (config == null) { throw new CouchbaseLiteError(Log.lookupStandardMessage("CannotSetLogLevel")); } + + // if the logging level has changed, install a new logger with the new level + final LogLevel curLevel = getLevel(); + if (curLevel == level) { return; } + + installLogger(config, level); + } + + /** + * Gets the configuration currently in use by the file logger. + * Note the configuration returned from this method is read-only + * and cannot be modified. An attempt to modify it will throw an exception. + * + * @return The configuration currently in use + */ + @Nullable + public LogFileConfiguration getConfig() { + final FileLogSink fileLogger = LogSinks.get().getFile(); + return (fileLogger == null) + ? null + : new LogFileConfiguration( + fileLogger.getDirectory(), + fileLogger.getMaxFileSize(), + fileLogger.getMaxKeptFiles() - 1, + fileLogger.isPlainText(), + true); + } + + /** + * Sets the configuration for use by the file logger. + * + * @param newConfig The configuration to use + */ + public void setConfig(@Nullable LogFileConfiguration newConfig) { + if (Objects.equals(getConfig(), newConfig)) { return; } + final LogFileConfiguration config = (newConfig == null) ? null : new LogFileConfiguration(newConfig); + installLogger(config, getLevel()); + configuration = config; + } + + @Override + public void log(@NonNull LogLevel level, @NonNull LogDomain domain, @NonNull String message) { + if (logger != null) { logger.doLog(level, domain, message); } + } + + private void installLogger(@Nullable LogFileConfiguration config, @NonNull LogLevel level) { + final ShimLogger newLogger = (config == null) + ? null + : new ShimLogger( + new FileLogSink.Builder() + .setDirectory(config.getDirectory()) + .setLevel(level) + .setMaxKeptFiles(config.getMaxRotateCount() + 1) + .setMaxFileSize(config.getMaxSize()) + .setPlainText(config.usesPlaintext())); + LogSinks.get().setFile(newLogger); + this.logger = newLogger; + } +} diff --git a/common/main/java/com/couchbase/lite/FullTextExpression.java b/common/main/java/com/couchbase/lite/FullTextExpression.java new file mode 100644 index 000000000..ac1322c4b --- /dev/null +++ b/common/main/java/com/couchbase/lite/FullTextExpression.java @@ -0,0 +1,96 @@ +// +// Copyright (c) 2020 Couchbase, Inc. +// +// 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 androidx.annotation.NonNull; + +import java.util.Arrays; + +import com.couchbase.lite.internal.utils.Preconditions; + + +/** + * Full-text expression + * + * @deprecated Use FullTextFunction.match() + */ +@Deprecated +public final class FullTextExpression { + static final class FullTextMatchExpression extends Expression { + //--------------------------------------------- + // member variables + //--------------------------------------------- + @NonNull + private final String indexName; + @NonNull + private final String text; + + //--------------------------------------------- + // Constructors + //--------------------------------------------- + FullTextMatchExpression(@NonNull String indexName, @NonNull String text) { + this.indexName = indexName; + this.text = text; + } + + //--------------------------------------------- + // package level access + //--------------------------------------------- + + @NonNull + @Override + Object asJSON() { return Arrays.asList("MATCH()", indexName, text); } + } + + /** + * Creates a full-text expression with the given full-text index name. + * + * @param name The full-text index name. + * @return The full-text expression. + * @deprecated Use FullTextFunction.match() + */ + @Deprecated + @NonNull + public static FullTextExpression index(@NonNull String name) { + Preconditions.assertNotNull(name, "name"); + return new FullTextExpression(name); + } + + //--------------------------------------------- + // member variables + //--------------------------------------------- + @NonNull + private final String name; + + //--------------------------------------------- + // Constructors + //--------------------------------------------- + private FullTextExpression(@NonNull String name) { this.name = name; } + + /** + * Creates a full-text match expression with the given search text. + * + * @param query The search text + * @return The full-text match expression + * @deprecated Use FullTextFunction.match() + */ + @Deprecated + @NonNull + public Expression match(@NonNull String query) { + Preconditions.assertNotNull(query, "query"); + return new FullTextMatchExpression(this.name, query); + } +} diff --git a/common/main/java/com/couchbase/lite/FullTextFunction.java b/common/main/java/com/couchbase/lite/FullTextFunction.java index b45158771..6f63128bf 100644 --- a/common/main/java/com/couchbase/lite/FullTextFunction.java +++ b/common/main/java/com/couchbase/lite/FullTextFunction.java @@ -74,4 +74,21 @@ public static Expression rank(@NonNull String indexName) { "RANK()", Expression.string(Preconditions.assertNotNull(indexName, "indexName"))); } + + /** + * Creates a full-text expression with the given full-text index name and search text. + * + * @param indexName The full-text index name. + * @param query The query string. + * @return The full-text match expression + * @deprecated Use: FullTextFunction.match(IndexExpression) + */ + @Deprecated + @NonNull + public static Expression match(@NonNull String indexName, @NonNull String query) { + return new Expression.FunctionExpression( + "MATCH()", + Expression.string(Preconditions.assertNotNull(indexName, "indexName")), + Expression.string(query)); + } } diff --git a/common/main/java/com/couchbase/lite/Log.java b/common/main/java/com/couchbase/lite/Log.java new file mode 100644 index 000000000..37399314e --- /dev/null +++ b/common/main/java/com/couchbase/lite/Log.java @@ -0,0 +1,127 @@ +// +// Copyright (c) 2020 Couchbase, Inc. +// +// 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Set; + +import com.couchbase.lite.internal.CouchbaseLiteInternal; +import com.couchbase.lite.logging.BaseLogSink; +import com.couchbase.lite.logging.LogSinks; + + +/** + * Holder for the three Couchbase Lite loggers: console, file, and custom. + * + * @deprecated Use com.couchbase.lite.logging.LogSinks + */ +@SuppressWarnings({"PMD.UnnecessaryFullyQualifiedName", "DeprecatedIsStillUsed"}) +@Deprecated +public final class Log { + private final class ShimLogger extends BaseLogSink { + ShimLogger(@NonNull LogLevel level, @NonNull Set domains) { super(level, domains); } + + @Override + protected void writeLog(@NonNull LogLevel level, @NonNull LogDomain domain, @NonNull String message) { + final BaseLogSink curLogger = LogSinks.get().getCustom(); + if (this != curLogger) { return; } + + final Logger logger = customLogger; + if (logger == null) { return; } + + logger.log(level, domain, message); + + // if the custom logger has changed its level since the last log, install a new one. + // NOTE: this may call back into lite core! + // If it was called from a LiteCore thread it may deadlock + if (getLevel() != logger.getLevel()) { installCustomLogger(logger); } + } + + protected boolean isLegacy() { return true; } + } + + + // Singleton instance. + private final ConsoleLogger consoleLogger = new ConsoleLogger(); + + // Singleton instance. + private final FileLogger fileLogger = new FileLogger(); + + // Singleton instance. + @Nullable + private Logger customLogger; + + // The singleton instance is available from Database.log + Log() { } + + /** + * Gets the logger that writes to the system console + * + * @return The logger that writes to the system console + * @deprecated Use com.couchbase.lite.logging.LogSinks.getConsole + */ + @Deprecated + @NonNull + public ConsoleLogger getConsole() { + CouchbaseLiteInternal.requireInit("Console logging not initialized"); + return consoleLogger; + } + + /** + * Gets the logger that writes to log files + * + * @return The logger that writes to log files + * @deprecated Use com.couchbase.lite.logging.LogSinks.getFile + */ + @Deprecated + @NonNull + public FileLogger getFile() { + CouchbaseLiteInternal.requireInit("File logging not initialized"); + return fileLogger; + } + + /** + * Gets the custom logger that was registered by the + * application (if any) + * + * @return The custom logger that was registered by + * the application, or null. + * @deprecated Use com.couchbase.lite.logging.LogSinks.getCustom + */ + @Deprecated + @Nullable + public Logger getCustom() { return customLogger; } + + /** + * Sets an application specific logging method + * + * @param customLogger A Logger implementation that will receive logging messages + * @deprecated Use com.couchbase.lite.logging.LogSinks.setCustom + */ + @Deprecated + public void setCustom(@Nullable Logger customLogger) { + this.customLogger = customLogger; + installCustomLogger(customLogger); + } + + private void installCustomLogger(@Nullable Logger logger) { + LogSinks.get().setCustom((logger == null) + ? null + : new ShimLogger(logger.getLevel(), LogDomain.ALL)); + } +} diff --git a/common/main/java/com/couchbase/lite/LogFileConfiguration.java b/common/main/java/com/couchbase/lite/LogFileConfiguration.java new file mode 100644 index 000000000..790b8defc --- /dev/null +++ b/common/main/java/com/couchbase/lite/LogFileConfiguration.java @@ -0,0 +1,215 @@ +// +// Copyright (c) 2020 Couchbase, Inc. +// 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +import com.couchbase.lite.internal.utils.Preconditions; + + +/** + * A class that describes the file configuration for the {@link FileLogger} class. + * Once a configuration has been assigned to a Logger, it becomes read-only: + * an attempt to mutate it will cause an exception. + * To change the configuration of a logger, copy its configuration, mutate the + * copy and then use it to replace the loggers current configuration. + * + * @deprecated Use FileLogSink.Builder + */ +@Deprecated +public final class LogFileConfiguration { + //--------------------------------------------- + // member variables + //--------------------------------------------- + private final boolean readonly; + + @NonNull + private final String directory; + + private boolean usePlaintext; + private int maxRotateCount; + private long maxSize; + + //--------------------------------------------- + // Constructors + //--------------------------------------------- + + /** + * Constructs a file configuration object with the given directory + * + * @param directory The directory that the logs will be written to + */ + public LogFileConfiguration(@NonNull String directory) { this(directory, null); } + + /** + * Constructs a file configuration object based on another one so + * that it may be modified + * + * @param config The other configuration to copy settings from + */ + public LogFileConfiguration(@NonNull LogFileConfiguration config) { + this((config == null) ? null : config.getDirectory(), config); + } + + /** + * Constructs a file configuration object based on another one but changing + * the directory + * + * @param directory The directory that the logs will be written to + * @param config The other configuration to copy settings from + */ + public LogFileConfiguration(@NonNull String directory, @Nullable LogFileConfiguration config) { + this(directory, config, false); + } + + LogFileConfiguration(@NonNull String directory, @Nullable LogFileConfiguration config, boolean readonly) { + this( + directory, + (config == null) ? null : config.maxSize, + (config == null) ? null : config.maxRotateCount, + (config == null) ? null : config.usePlaintext, + readonly); + } + + LogFileConfiguration( + @NonNull String directory, + @Nullable Long maxSize, + @Nullable Integer maxRotateCount, + @Nullable Boolean usePlaintext, + boolean readonly) { + this.directory = Preconditions.assertNotNull(directory, "directory"); + this.maxSize = (maxSize != null) ? maxSize : Defaults.LogFile.MAX_SIZE; + this.maxRotateCount = (maxRotateCount != null) ? maxRotateCount : Defaults.LogFile.MAX_ROTATE_COUNT; + this.usePlaintext = (usePlaintext != null) ? usePlaintext : Defaults.LogFile.USE_PLAINTEXT; + this.readonly = readonly; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (!(o instanceof LogFileConfiguration)) { return false; } + final LogFileConfiguration that = (LogFileConfiguration) o; + return (maxSize == that.maxSize) + && (maxRotateCount == that.maxRotateCount) + && (usePlaintext == that.usePlaintext) + && directory.equals(that.directory); + } + + @Override + public int hashCode() { return Objects.hash(directory); } + + @NonNull + @Override + public String toString() { + return "LogFileConfig{" + + ((readonly) ? "" : "!") + + ((usePlaintext) ? "+" : "") + + directory + ", " + maxSize + ", " + maxRotateCount + "}"; + } + + //--------------------------------------------- + // Setters + //--------------------------------------------- + + /** + * Sets the max size of the log file in bytes. If a log file + * passes this size then a new log file will be started. This + * number is a best effort and the actual size may go over slightly. + * The default size is 500Kb. + * + * @param maxSize The max size of the log file in bytes + * @return The self object + */ + @NonNull + public LogFileConfiguration setMaxSize(long maxSize) { + if (readonly) { throw new CouchbaseLiteError("LogFileConfiguration is readonly mode."); } + + this.maxSize = Preconditions.assertNotNegative(maxSize, "max size"); + return this; + } + + /** + * Sets the number of rotated logs that are saved. For instance, + * if the value is 1 then 2 logs will be present: the 'current' log + * and the previous 'rotated' log. + * The default value is 1. + * + * @param maxRotateCount The number of rotated logs to be saved + * @return The self object + */ + @NonNull + public LogFileConfiguration setMaxRotateCount(int maxRotateCount) { + if (readonly) { throw new CouchbaseLiteError("LogFileConfiguration is readonly mode."); } + + this.maxRotateCount = Preconditions.assertNotNegative(maxRotateCount, "max rotation count"); + return this; + } + + /** + * Sets whether or not CBL logs in plaintext. The default (false) is + * to log in a binary encoded format that is more CPU and I/O friendly. + * Enabling plaintext is not recommended in production. + * + * @param usePlaintext Whether or not to log in plaintext + * @return The self object + */ + @NonNull + public LogFileConfiguration setUsePlaintext(boolean usePlaintext) { + if (readonly) { throw new CouchbaseLiteError("LogFileConfiguration is readonly mode."); } + + this.usePlaintext = usePlaintext; + return this; + } + + //--------------------------------------------- + // Getters + //--------------------------------------------- + + /** + * Gets the directory that the logs files are stored in. + * + * @return The directory that the logs files are stored in. + */ + @NonNull + public String getDirectory() { return directory; } + + /** + * Gets the max size of the log file in bytes. If a log file + * passes this size then a new log file will be started. This + * number is a best effort and the actual size may go over slightly. + * The default size is 500Kb. + * + * @return The max size of the log file in bytes + */ + public long getMaxSize() { return maxSize; } + + /** + * Gets the number of rotated logs that are saved. For instance, + * if the value is 1 then 2 logs will be present: the 'current' log + * and the previous 'rotated' log. + * The default value is 1. + * + * @return The number of rotated logs that are saved + */ + public int getMaxRotateCount() { return maxRotateCount; } + + /** + * Gets whether or not CBL is logging in plaintext. The default (false) is + * to log in a binary encoded format that is more CPU and I/O friendly. + * Enabling plaintext is not recommended in production. + * + * @return Whether or not CBL is logging in plaintext + */ + public boolean usesPlaintext() { return usePlaintext; } +} diff --git a/common/main/java/com/couchbase/lite/Logger.java b/common/main/java/com/couchbase/lite/Logger.java new file mode 100644 index 000000000..7ed2d6a64 --- /dev/null +++ b/common/main/java/com/couchbase/lite/Logger.java @@ -0,0 +1,47 @@ +// +// Copyright (c) 2020 Couchbase, Inc. +// +// 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 androidx.annotation.NonNull; + + +/** + * The logging interface for Couchbase Lite. An application that wishes + * to route log messages to an arbitrary endpoint can do so by + * installing an implementation of this interface with {@link Log#setCustom(Logger)}. + * + * @deprecated Use com.couchbase.lite.logging.BaseLogSink + */ +@SuppressWarnings("DeprecatedIsStillUsed") +@Deprecated +public interface Logger { + /** + * Gets the level that will be logged via this logger. + * + * @return The maximum level to log + */ + @NonNull + LogLevel getLevel(); + + /** + * Performs the actual logging logic + * + * @param level The level of the message to log + * @param domain The domain of the message to log + * @param message The content of the message to log + */ + void log(@NonNull LogLevel level, @NonNull LogDomain domain, @NonNull String message); +} diff --git a/common/main/java/com/couchbase/lite/Query.java b/common/main/java/com/couchbase/lite/Query.java index 31dea3996..11dbc30f7 100644 --- a/common/main/java/com/couchbase/lite/Query.java +++ b/common/main/java/com/couchbase/lite/Query.java @@ -101,4 +101,13 @@ public interface Query { */ @NonNull ListenerToken addChangeListener(@Nullable Executor executor, @NonNull QueryChangeListener listener); + + /** + * Removes a change listener wih the given listener token. + * + * @param token The listener token. + * @deprecated Use ListenerToken.remove() + */ + @Deprecated + void removeChangeListener(@NonNull ListenerToken token); } diff --git a/common/main/java/com/couchbase/lite/internal/BaseReplicatorConfiguration.java b/common/main/java/com/couchbase/lite/internal/BaseReplicatorConfiguration.java index 47249c1a6..5cb727b5b 100644 --- a/common/main/java/com/couchbase/lite/internal/BaseReplicatorConfiguration.java +++ b/common/main/java/com/couchbase/lite/internal/BaseReplicatorConfiguration.java @@ -16,8 +16,10 @@ package com.couchbase.lite.internal; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import com.couchbase.lite.Collection; @@ -25,15 +27,26 @@ public class BaseReplicatorConfiguration { + @NonNull + private final Map internalCollectionConfigurations; // subclasses can read the collection configurations directly but must use mutators to change them. @NonNull protected final Map collectionConfigurations; - protected BaseReplicatorConfiguration(@NonNull Map configs) { + protected BaseReplicatorConfiguration(@Nullable Map configs) { CouchbaseLiteInternal.requireInit("Can't create ReplicatorConfiguration"); - collectionConfigurations = Collections.unmodifiableMap(configs); + internalCollectionConfigurations = (configs != null) ? configs : new HashMap<>(); + collectionConfigurations = Collections.unmodifiableMap(internalCollectionConfigurations); + } + + protected void addCollectionInternal(@Nullable Collection coll, @Nullable CollectionConfiguration config) { + if (coll != null) { internalCollectionConfigurations.put(coll, config); } + } + + protected void removeCollectionInternal(@Nullable Collection coll) { + if (coll != null) { internalCollectionConfigurations.remove(coll); } } @NonNull diff --git a/common/main/java/com/couchbase/lite/internal/core/C4Database.java b/common/main/java/com/couchbase/lite/internal/core/C4Database.java index 59612ffc5..1e03da9a6 100644 --- a/common/main/java/com/couchbase/lite/internal/core/C4Database.java +++ b/common/main/java/com/couchbase/lite/internal/core/C4Database.java @@ -48,7 +48,7 @@ @SuppressWarnings({"PMD.ExcessivePublicCount", "PMD.TooManyMethods"}) public abstract class C4Database extends C4Peer { - public static final boolean VERSION_VECTORS_ENABLED = true; + public static final boolean VERSION_VECTORS_ENABLED = false; @VisibleForTesting public static final String DB_EXTENSION = ".cblite2"; diff --git a/common/main/kotlin/com/couchbase/lite/CommonConfigurationFactories.kt b/common/main/kotlin/com/couchbase/lite/CommonConfigurationFactories.kt index a2e54f96b..3dddb3535 100644 --- a/common/main/kotlin/com/couchbase/lite/CommonConfigurationFactories.kt +++ b/common/main/kotlin/com/couchbase/lite/CommonConfigurationFactories.kt @@ -15,6 +15,7 @@ // package com.couchbase.lite +import com.couchbase.lite.internal.getCollectionConfigs import com.couchbase.lite.internal.logging.Log import com.couchbase.lite.logging.FileLogSink import com.couchbase.lite.logging.LogSinks @@ -31,7 +32,30 @@ val CollectionConfigurationFactory: CollectionConfiguration? = null /** * * @see com.couchbase.lite.CollectionConfiguration + * @deprecated Use CollectionConfiguration?.newConfig(collection: Collection, channels: List?, documentIDs: List?, pullFilter: ReplicationFilter?, pushFilter: ReplicationFilter?, conflictResolver: ConflictResolver?) */ +@Deprecated( + "Use CollectionConfiguration?.newConfig(collection: Collection, channels: List?, documentIDs: List?, pullFilter: ReplicationFilter?, pushFilter: ReplicationFilter?, conflictResolver: ConflictResolver?)", + replaceWith = ReplaceWith("CollectionConfiguration?.newConfig(collection: Collection, channels: List?, documentIDs: List?, pullFilter: ReplicationFilter?, pushFilter: ReplicationFilter?, conflictResolver: ConflictResolver?)") +) +fun CollectionConfiguration?.newConfig( + channels: List? = null, + documentIDs: List? = null, + pullFilter: ReplicationFilter? = null, + pushFilter: ReplicationFilter? = null, + conflictResolver: ConflictResolver? = null +): CollectionConfiguration { + val config = CollectionConfiguration() + + (channels ?: this?.channels)?.let { config.channels = it } + (documentIDs ?: this?.documentIDs)?.let { config.documentIDs = it } + (pushFilter ?: this?.pushFilter)?.let { config.pushFilter = it } + (pullFilter ?: this?.pullFilter)?.let { config.pullFilter = it } + (conflictResolver ?: this?.conflictResolver)?.let { config.conflictResolver = it } + + return config +} + fun CollectionConfiguration?.newConfig( collection: Collection, channels: List? = null, @@ -169,6 +193,48 @@ fun FileLogSink?.install( LogSinks.get().file = builder.build() } +/** + * Configuration factory for new LogFileConfigurations + * + * Usage: + * val logFileConfig = LogFileConfigurationFactory.newConfig(...) + */ +val LogFileConfigurationFactory: LogFileConfiguration? = null + +/** + * Create a LogFileConfiguration, overriding the receiver's + * values with the passed parameters: + * + * @param directory (required) the directory in which the logs files are stored. + * @param maxSize the max size of the log file in bytes. + * @param maxRotateCount the number of rotated logs that are saved. + * @param usePlainText whether or not to log in plaintext. + * + * @see com.couchbase.lite.LogFileConfiguration + * @deprecated Use FileLogSink?.install(String?, LogLevel?, Long?, Int?, Boolean?) + */ +@Deprecated( + "Use FileLogSink?.install(String?, LogLevel?, Long?, Int?, Boolean?)", + replaceWith = ReplaceWith("FileLogSink?.install(dir: String?, level: LogLevel?, maxSize: Long?, maxRotate: Int?, plaintxt: Boolean?)") +) +fun LogFileConfiguration?.newConfig( + directory: String? = null, + maxSize: Long? = null, + maxRotateCount: Int? = null, + usePlainText: Boolean? = null +): LogFileConfiguration { + val config = LogFileConfiguration( + directory ?: this?.directory + ?: throw IllegalArgumentException("A LogFileConfiguration must specify a directory") + ) + + (maxSize ?: this?.maxSize)?.let { config.maxSize = it } + (maxRotateCount ?: this?.maxRotateCount)?.let { config.maxRotateCount = it } + (usePlainText ?: this?.usesPlaintext())?.let { config.setUsePlaintext(it) } + + return config +} + /** * Create a FullTextIndexConfiguration, overriding the receiver's * values with the passed parameters: @@ -203,6 +269,29 @@ fun FullTextIndexConfiguration?.create( ) fun ValueIndexConfiguration?.create(vararg expressions: String = emptyArray()) = this.newConfig(*expressions) +/** + * Create a LogFileConfiguration, overriding the receiver's + * values with the passed parameters: + * + * @param directory (required) the directory in which the logs files are stored. + * @param maxSize the max size of the log file in bytes. + * @param maxRotateCount the number of rotated logs that are saved. + * @param usePlainText whether or not to log in plaintext. + * + * @see com.couchbase.lite.LogFileConfiguration + * @deprecated Use FileLogSink?.install(String?, LogLevel?, Long?, Int?, Boolean?) + */ +@Deprecated( + "Use FileLogSink?.install(String?, LogLevel?, Long?, Int?, Boolean?)", + replaceWith = ReplaceWith("FileLogSink?.install(dir: String?, level: LogLevel?, maxSize: Long?, maxRotate: Int?, plaintxt: Boolean?)") +) +fun LogFileConfiguration?.create( + directory: String? = null, + maxSize: Long? = null, + maxRotateCount: Int? = null, + usePlainText: Boolean? = null +) = this.newConfig(directory, maxSize, maxRotateCount, usePlainText) + // If the source config contains anything other than exactly the // database default collection, we are about to lose information. @@ -236,3 +325,25 @@ internal fun copyReplConfig( (enableAutoPurge ?: src?.isAutoPurgeEnabled)?.let { dst.setAutoPurgeEnabled(it) } (acceptParentDomainCookies ?: src?.isAcceptParentDomainCookies)?.let { dst.setAcceptParentDomainCookies(it) } } + +@Suppress("DEPRECATION") +internal fun copyLegacyReplConfig( + src: ReplicatorConfiguration?, + dst: ReplicatorConfiguration, + pinnedServerCertificate: ByteArray?, + channels: List?, + documentIDs: List?, + pushFilter: ReplicationFilter?, + pullFilter: ReplicationFilter?, + conflictResolver: ConflictResolver? +) { + (pinnedServerCertificate ?: src?.pinnedServerCertificate)?.let { dst.setPinnedServerCertificate(it) } + + // copy the default collection configuration, if it exists + val srcConfig = src?.database?.defaultCollection?.let { getCollectionConfigs(src)?.get(it) } + (channels ?: srcConfig?.channels)?.let { dst.channels = it } + (documentIDs ?: srcConfig?.documentIDs)?.let { dst.documentIDs = it } + (pushFilter ?: srcConfig?.pushFilter)?.let { dst.pushFilter = it } + (pullFilter ?: srcConfig?.pullFilter)?.let { dst.pullFilter = it } + (conflictResolver ?: srcConfig?.conflictResolver)?.let { dst.conflictResolver = it } +} diff --git a/common/main/kotlin/com/couchbase/lite/CommonFlows.kt b/common/main/kotlin/com/couchbase/lite/CommonFlows.kt index 7c9788f45..9ebf0aa5d 100644 --- a/common/main/kotlin/com/couchbase/lite/CommonFlows.kt +++ b/common/main/kotlin/com/couchbase/lite/CommonFlows.kt @@ -111,4 +111,51 @@ fun Query.queryChangeFlow(executor: Executor? = null) = callbackFlow { trySend(it) } awaitClose { token.remove() } -} \ No newline at end of file +} + +/** + * A Flow of database changes. + * + * @param executor Optional executor on which to run the change listener. If no executor + * is provided, the listener will be called on the Flow's CoroutineDispatcher. + * + * @see com.couchbase.lite.Database.addChangeListener + * @deprecated Use Database.getCollection(String, String?).collectionChangeFlow(Executor?) + */ +@Suppress("DEPRECATION") +@Deprecated( + "Use Database.getCollection(String, String?).collectionChangeFlow(executor)", + replaceWith = ReplaceWith("Database.getCollection(String, String?).collectionChangeFlow(executor)") +) +fun Database.databaseChangeFlow(executor: Executor? = null) = callbackFlow { + val token = this@databaseChangeFlow.addChangeListener( + executor ?: (coroutineContext[ContinuationInterceptor] as? CoroutineDispatcher)?.asExecutor() + ) { + trySend(it) + } + awaitClose { this@databaseChangeFlow.removeChangeListener(token) } +} + +/** + * A Flow of document changes. + * + * @param executor Optional executor on which to run the change listener. If no executor + * is provided, the listener will be called on the Flow's CoroutineDispatcher. + * + * @see com.couchbase.lite.Database.addDocumentChangeListener + * @deprecated Use Database.getCollection(String, String?).documentChangeFlow(String, Executor?) + */ +@Suppress("DEPRECATION") +@Deprecated( + "Use Database.getCollection(String, String?).documentChangeFlow(documentId, executor)", + replaceWith = ReplaceWith("Database.getCollection(String, String?).documentChangeFlow(documentId, executor)") +) +fun Database.documentChangeFlow(documentId: String, executor: Executor? = null) = callbackFlow { + val token = this@documentChangeFlow.addDocumentChangeListener( + documentId, + executor ?: (coroutineContext[ContinuationInterceptor] as? CoroutineDispatcher)?.asExecutor() + ) { + trySend(it) + } + awaitClose { this@documentChangeFlow.removeChangeListener(token) } +} diff --git a/common/test/java/com/couchbase/lite/LegacyLogTest.kt b/common/test/java/com/couchbase/lite/LegacyLogTest.kt new file mode 100644 index 000000000..21f751c4a --- /dev/null +++ b/common/test/java/com/couchbase/lite/LegacyLogTest.kt @@ -0,0 +1,543 @@ +// +// Copyright (c) 2022 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. +// +@file:Suppress("DEPRECATION") + +package com.couchbase.lite + +import com.couchbase.lite.internal.core.CBLVersion +import com.couchbase.lite.internal.logging.Log +import com.couchbase.lite.internal.logging.LogSinksImpl +import com.couchbase.lite.logging.BaseLogSink +import com.couchbase.lite.logging.ConsoleLogSink +import com.couchbase.lite.logging.FileLogSink +import com.couchbase.lite.logging.LogSinks +import com.couchbase.lite.utils.KotlinHelpers +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.io.BufferedReader +import java.io.File +import java.io.FileInputStream +import java.io.FileReader +import java.nio.charset.StandardCharsets +import java.util.EnumSet +import java.util.Locale +import java.util.UUID +import kotlin.Array + + +private class TestDeprecatedConsoleLogger : ConsoleLogger() { + private val buf = StringBuilder() + val content + get() = buf.toString() + + override fun shimFactory(level: LogLevel, domain: EnumSet): ShimLogger { + return object : ShimLogger(level, domain) { + override fun doWriteLog(level: LogLevel, domain: LogDomain, message: String) { + buf.append(message) + } + } + } + + fun clearContent() = buf.clear() +} + +class LegacyLogTest : BaseDbTest() { + private var scratchDirPath: String? = null + + private val tempDir: File? + get() { + val dir = scratchDirPath + return dir?.let { File(it) } + } + + private val logFiles: Array + get() = assertNonNull(tempDir?.listFiles()) + + @Before + fun setUpLogTest() { + scratchDirPath = getScratchDirectoryPath(getUniqueName("log-dir")) + LogSinksImpl.initLogging() + } + + @After + fun tearDownLogTest() = LogSinksImpl.initLogging() + + @Test + fun testConsoleLoggerLevel() { + val consoleLogger = TestDeprecatedConsoleLogger() + + consoleLogger.setDomains(LogDomain.DATABASE) + for (level in LogLevel.values()) { + if (level == LogLevel.NONE) { + continue + } + + consoleLogger.level = level + consoleLogger.log(LogLevel.DEBUG, LogDomain.DATABASE, "D") + consoleLogger.log(LogLevel.VERBOSE, LogDomain.DATABASE, "V") + consoleLogger.log(LogLevel.INFO, LogDomain.DATABASE, "I") + consoleLogger.log(LogLevel.WARNING, LogDomain.DATABASE, "W") + consoleLogger.log(LogLevel.ERROR, LogDomain.DATABASE, "E") + } + + Assert.assertEquals("DVIWEVIWEIWEWEE", consoleLogger.content) + } + + @Test + fun testConsoleLoggerDomains() { + val consoleLogger = TestDeprecatedConsoleLogger() + + consoleLogger.setDomains() + for (level in LogLevel.values()) { + if (level == LogLevel.NONE) { + continue + } + + consoleLogger.level = level + consoleLogger.log(LogLevel.DEBUG, LogDomain.DATABASE, "D") + consoleLogger.log(LogLevel.VERBOSE, LogDomain.DATABASE, "V") + consoleLogger.log(LogLevel.INFO, LogDomain.DATABASE, "I") + consoleLogger.log(LogLevel.WARNING, LogDomain.DATABASE, "W") + consoleLogger.log(LogLevel.ERROR, LogDomain.DATABASE, "E") + } + Assert.assertEquals("", consoleLogger.content) + consoleLogger.clearContent() + + consoleLogger.setDomains(LogDomain.NETWORK, LogDomain.QUERY) + for (level in LogLevel.values()) { + if (level == LogLevel.NONE) { + continue + } + + consoleLogger.level = level + consoleLogger.log(LogLevel.DEBUG, LogDomain.DATABASE, "D") + consoleLogger.log(LogLevel.VERBOSE, LogDomain.DATABASE, "V") + consoleLogger.log(LogLevel.INFO, LogDomain.DATABASE, "I") + consoleLogger.log(LogLevel.WARNING, LogDomain.DATABASE, "W") + consoleLogger.log(LogLevel.ERROR, LogDomain.DATABASE, "E") + } + Assert.assertEquals("", consoleLogger.content) + + consoleLogger.domains = LogDomain.ALL_DOMAINS + consoleLogger.level = LogLevel.DEBUG + consoleLogger.log(LogLevel.DEBUG, LogDomain.NETWORK, "N") + consoleLogger.log(LogLevel.DEBUG, LogDomain.QUERY, "Q") + consoleLogger.log(LogLevel.DEBUG, LogDomain.DATABASE, "D") + Assert.assertEquals("NQD", consoleLogger.content) + } + + @Test + fun testFileLoggerDefaults() { + val config = LogFileConfiguration("up/down") + Assert.assertEquals(Defaults.LogFile.MAX_SIZE, config.maxSize) + Assert.assertEquals(Defaults.LogFile.MAX_ROTATE_COUNT, config.maxRotateCount) + Assert.assertEquals(Defaults.LogFile.USE_PLAINTEXT, config.usesPlaintext()) + } + + @Test + fun testFileLoggingLevels() { + val mark = "$$$$ ${UUID.randomUUID()}" + testWithConfiguration( + LogLevel.DEBUG, + LogFileConfiguration(scratchDirPath!!).setUsePlaintext(true).setMaxRotateCount(0) + ) { + for (level in LogLevel.values()) { + if (level == LogLevel.NONE) { + continue + } + Database.log.file.level = level + + Log.d(LogDomain.DATABASE, mark) + Log.i(LogDomain.DATABASE, mark) + Log.w(LogDomain.DATABASE, mark) + Log.e(LogDomain.DATABASE, mark) + } + + for (log in logFiles) { + var lineCount = 0 + BufferedReader(FileReader(log)).use { + while (true) { + val l = it.readLine() ?: break + if (l.contains(mark)) { + lineCount++ + } + } + } + + val logPath = log.canonicalPath + when { + logPath.contains("error") -> Assert.assertEquals(5, lineCount) + logPath.contains("warning") -> Assert.assertEquals(4, lineCount) + logPath.contains("info") -> Assert.assertEquals(3, lineCount) + logPath.contains("debug") -> Assert.assertEquals(1, lineCount) + logPath.contains("verbose") -> Assert.assertEquals(0, lineCount) + } + } + } + } + + @Test + fun testFileLoggingDefaultBinaryFormat() { + testWithConfiguration(LogLevel.INFO, LogFileConfiguration(scratchDirPath!!)) { + Log.i(LogDomain.DATABASE, "TEST INFO") + + val files = logFiles + Assert.assertTrue(files.isNotEmpty()) + + val lastModifiedFile = getMostRecent(files) + Assert.assertNotNull(lastModifiedFile) + + val bytes = ByteArray(4) + FileInputStream(lastModifiedFile!!).use { inStr -> Assert.assertEquals(4, inStr.read(bytes)) } + Assert.assertEquals(0xCF.toByte(), bytes[0]) + Assert.assertEquals(0xB2.toByte(), bytes[1]) + Assert.assertEquals(0xAB.toByte(), bytes[2]) + Assert.assertEquals(0x1B.toByte(), bytes[3]) + } + } + + @Test + fun testFileLoggingUsePlainText() { + testWithConfiguration(LogLevel.INFO, LogFileConfiguration(scratchDirPath!!).setUsePlaintext(true)) { + val uuidString = UUID.randomUUID().toString() + Log.i(LogDomain.DATABASE, uuidString) + val files = tempDir!!.listFiles { _: File?, name: String -> + name.lowercase(Locale.getDefault()).startsWith("cbl_info_") + } + + Assert.assertNotNull(files) + Assert.assertEquals(1, files?.size ?: 0) + + val file = getMostRecent(files) + Assert.assertNotNull(file) + Assert.assertTrue(getLogContents(file!!).contains(uuidString)) + } + } + + @Test + fun testFileLoggingLogFilename() { + testWithConfiguration(LogLevel.DEBUG, LogFileConfiguration(scratchDirPath!!)) { + Log.e(LogDomain.DATABASE, "$$\$TEST MESSAGE") + + val files = logFiles + Assert.assertTrue(files.size >= 4) + + val rex = Regex("cbl_(debug|verbose|info|warning|error)_\\d+\\.cbllog") + for (file in files) { + Assert.assertTrue(file.name.contains("crash") || file.name.matches(rex)) + } + } + } + + @Test + fun testFileLoggingMaxSize() { + val config = LogFileConfiguration(scratchDirPath!!) + .setUsePlaintext(true) + .setMaxSize(1024) + .setMaxRotateCount(10) + testWithConfiguration(LogLevel.DEBUG, config) { + // This should create two files for each of the 5 levels except verbose (debug, info, warning, error): + // 1k of logs plus .5k headers. There should be only one file at the verbose level (just the headers) + // plus a log for crashes + write1KBToLog() + Assert.assertEquals((4 * 2) + 2, logFiles.size) + } + } + + @Test + fun testFileLoggingDisableLogging() { + val uuidString = UUID.randomUUID().toString() + + testWithConfiguration(LogLevel.NONE, LogFileConfiguration(scratchDirPath!!).setUsePlaintext(true)) { + writeAllLogs(uuidString) + for (log in logFiles) { + Assert.assertFalse(getLogContents(log).contains(uuidString)) + } + } + } + + @Test + fun testFileLoggingReEnableLogging() { + val uuidString = UUID.randomUUID().toString() + + testWithConfiguration(LogLevel.NONE, LogFileConfiguration(scratchDirPath!!).setUsePlaintext(true)) { + writeAllLogs(uuidString) + + for (log in logFiles) { + if(log.name.contains("crash")) { + continue; + } + + Assert.assertFalse(getLogContents(log).contains(uuidString)) + } + + Database.log.file.level = LogLevel.INFO + writeAllLogs(uuidString) + + val logFiles = tempDir!!.listFiles() + Assert.assertNotNull(tempDir!!.listFiles()) + for (log in logFiles!!) { + val fn = log.name.lowercase(Locale.getDefault()) + if (fn.startsWith("cbl_debug_") || fn.startsWith("cbl_verbose_") || fn.startsWith("cbl_crash")) { + Assert.assertFalse(getLogContents(log).contains(uuidString)) + } else { + Assert.assertTrue(getLogContents(log).contains(uuidString)) + } + } + } + } + + @Test + fun testFileLoggingHeader() { + testWithConfiguration(LogLevel.VERBOSE, LogFileConfiguration(scratchDirPath!!).setUsePlaintext(true)) { + write1KBToLog() + for (log in logFiles) { + if(log.name.contains("crash")) { + continue; + } + + var logLine: String + BufferedReader(FileReader(log)).use { + logLine = it.readLine() + logLine = it.readLine() // skip the LiteCore log line... + } + Assert.assertNotNull(logLine) + Assert.assertTrue(logLine.contains("CouchbaseLite $PRODUCT")) + Assert.assertTrue(logLine.contains("Core/")) + Assert.assertTrue(logLine.contains(CBLVersion.getSysInfo())) + } + } + } + + @Test + fun testWriteLogWithError() { + val message = "test message" + val uuid = UUID.randomUUID().toString() + val error = CouchbaseLiteException(uuid) + + testWithConfiguration(LogLevel.DEBUG, LogFileConfiguration(scratchDirPath!!).setUsePlaintext(true)) { + Log.d(LogDomain.DATABASE, message, error) + Log.i(LogDomain.DATABASE, message, error) + Log.w(LogDomain.DATABASE, message, error) + Log.e(LogDomain.DATABASE, message, error) + + for (log in logFiles) { + if (!log.name.contains("verbose") && !log.name.contains("crash")) { + Assert.assertTrue(getLogContents(log).contains(uuid)) + } + } + } + } + + @Test + fun testWriteLogWithErrorAndArgs() { + val uuid1 = UUID.randomUUID().toString() + val uuid2 = UUID.randomUUID().toString() + val message = "test message %s" + val error = CouchbaseLiteException(uuid1) + + testWithConfiguration(LogLevel.DEBUG, LogFileConfiguration(scratchDirPath!!).setUsePlaintext(true)) { + Log.d(LogDomain.DATABASE, message, error, uuid2) + Log.i(LogDomain.DATABASE, message, error, uuid2) + Log.w(LogDomain.DATABASE, message, error, uuid2) + Log.e(LogDomain.DATABASE, message, error, uuid2) + + for (log in logFiles) { + if (!log.name.contains("verbose") && !log.name.contains("crash")) { + val content = getLogContents(log) + Assert.assertTrue(content.contains(uuid1)) + Assert.assertTrue(content.contains(uuid2)) + } + } + } + } + + @Test + fun testLogFileConfigurationConstructors() { + val rotateCount = 4 + val maxSize = 2048L + val usePlainText = true + + Assert.assertThrows(IllegalArgumentException::class.java) { + KotlinHelpers.createLogFileConfigWithNullConfig() + } + + Assert.assertThrows(IllegalArgumentException::class.java) { + KotlinHelpers.createLogFileConfigWithNullDir() + } + + val config = LogFileConfiguration(scratchDirPath!!) + .setMaxRotateCount(rotateCount) + .setMaxSize(maxSize) + .setUsePlaintext(usePlainText) + + Assert.assertEquals(rotateCount, config.maxRotateCount) + Assert.assertEquals(maxSize, config.maxSize) + Assert.assertEquals(usePlainText, config.usesPlaintext()) + Assert.assertEquals(scratchDirPath, config.directory) + + val tempDir2 = getScratchDirectoryPath(getUniqueName("logtest2")) + val newConfig = LogFileConfiguration(tempDir2, config) + Assert.assertEquals(rotateCount, newConfig.maxRotateCount) + Assert.assertEquals(maxSize, newConfig.maxSize) + Assert.assertEquals(usePlainText, newConfig.usesPlaintext()) + Assert.assertEquals(tempDir2, newConfig.directory) + } + + @Test + fun testEditReadOnlyLogFileConfiguration() { + testWithConfiguration(LogLevel.DEBUG, LogFileConfiguration(scratchDirPath!!)) { + Assert.assertThrows(CouchbaseLiteError::class.java) { Database.log.file.config!!.maxSize = 1024 } + Assert.assertThrows(CouchbaseLiteError::class.java) { Database.log.file.config!!.maxRotateCount = 3 } + Assert.assertThrows(CouchbaseLiteError::class.java) { Database.log.file.config!!.setUsePlaintext(true) } + } + } + + @Test + fun testSetNewLogFileConfiguration() { + val config = LogFileConfiguration(scratchDirPath!!) + val fileLogger = Database.log.file + fileLogger.config = config + Assert.assertEquals(config, fileLogger.config) + fileLogger.config = null + Assert.assertNull(fileLogger.config) + fileLogger.config = config + Assert.assertEquals(config, fileLogger.config) + fileLogger.config = LogFileConfiguration("$scratchDirPath/legacyLogs") + Assert.assertEquals(LogFileConfiguration("$scratchDirPath/legacyLogs"), fileLogger.config) + } + + @Test + fun testMixLegacyAndNewAPIs1() { + Assert.assertThrows(CouchbaseLiteError::class.java) { + val fileLogger = Database.log.file + fileLogger.config = LogFileConfiguration(scratchDirPath!!) + fileLogger.setLevel(LogLevel.VERBOSE) + LogSinks.get().file = FileLogSink.Builder().setDirectory(scratchDirPath!!).setLevel(LogLevel.ERROR).build() + } + } + + @Test + fun testMixLegacyAndNewAPIs2() { + Assert.assertThrows(CouchbaseLiteError::class.java) { + LogSinks.get().file = FileLogSink.Builder().setDirectory(scratchDirPath!!).setLevel(LogLevel.ERROR).build() + val fileLogger = Database.log.file + fileLogger.config = LogFileConfiguration(scratchDirPath!!) + fileLogger.setLevel(LogLevel.VERBOSE) + } + } + + @Test + fun testMixLegacyAndNewAPIs3() { + Assert.assertThrows(CouchbaseLiteError::class.java) { + Database.log.console.level = LogLevel.VERBOSE + LogSinks.get().console = ConsoleLogSink(LogLevel.ERROR, LogDomain.ALL) + } + } + + @Test + fun testMixLegacyAndNewAPIs4() { + Assert.assertThrows(CouchbaseLiteError::class.java) { + LogSinks.get().console = ConsoleLogSink(LogLevel.VERBOSE, LogDomain.ALL) + Database.log.console.level = LogLevel.ERROR + } + } + + @Test + fun testMixLegacyAndNewAPIs5() { + Assert.assertThrows(CouchbaseLiteError::class.java) { + Database.log.custom = object : Logger { + override fun getLevel() = LogLevel.VERBOSE + override fun log(level: LogLevel, domain: LogDomain, message: String) {} + } + LogSinks.get().custom = object : BaseLogSink(LogLevel.ERROR, LogDomain.ALL) { + override fun writeLog(level: LogLevel, domain: LogDomain, message: String) {} + } + } + } + + @Test + fun testMixLegacyAndNewAPIs6() { + Assert.assertThrows(CouchbaseLiteError::class.java) { + LogSinks.get().custom = object : BaseLogSink(LogLevel.ERROR, LogDomain.ALL) { + override fun writeLog(level: LogLevel, domain: LogDomain, message: String) {} + } + Database.log.custom = object : Logger { + override fun getLevel() = LogLevel.VERBOSE + override fun log(level: LogLevel, domain: LogDomain, message: String) {} + } + } + } + + @Test + fun testMixLegacyAndNewAPIs7() { + Assert.assertThrows(CouchbaseLiteError::class.java) { + Database.log.custom = object : Logger { + override fun getLevel() = LogLevel.VERBOSE + override fun log(level: LogLevel, domain: LogDomain, message: String) {} + } + LogSinks.get().file = FileLogSink.Builder().setDirectory(scratchDirPath!!).setLevel(LogLevel.ERROR).build() + } + } + + @Test + fun testMixLegacyAndNewAPIs8() { + Assert.assertThrows(CouchbaseLiteError::class.java) { + LogSinks.get().file = FileLogSink.Builder().setDirectory(scratchDirPath!!).setLevel(LogLevel.ERROR).build() + Database.log.custom = object : Logger { + override fun getLevel() = LogLevel.VERBOSE + override fun log(level: LogLevel, domain: LogDomain, message: String) {} + } + } + } + + private fun testWithConfiguration(level: LogLevel, config: LogFileConfiguration, task: Runnable) { + val logger = Database.log + val consoleLogger = logger.console + consoleLogger.level = level + + val fileLogger = logger.file + fileLogger.config = config + fileLogger.level = level + + task.run() + } + + private fun write1KBToLog() { + val message = "11223344556677889900" // ~65 bytes including the line headers + // 16 * 65 ~= 1024. + for (i in 0..15) { + writeAllLogs(message) + } + } + + private fun writeAllLogs(message: String) { + Log.d(LogDomain.DATABASE, message) + Log.i(LogDomain.DATABASE, message) + Log.w(LogDomain.DATABASE, message) + Log.e(LogDomain.DATABASE, message) + } + + private fun getLogContents(log: File): String { + val b = ByteArray(log.length().toInt()) + FileInputStream(log).use { Assert.assertEquals(b.size, it.read(b)) } + return String(b, StandardCharsets.US_ASCII) + } + + private fun getMostRecent(files: Array?) = files?.maxByOrNull { it.lastModified() } +} \ No newline at end of file diff --git a/common/test/java/com/couchbase/lite/NotificationTest.java b/common/test/java/com/couchbase/lite/NotificationTest.java index a60e25044..6ba02b633 100644 --- a/common/test/java/com/couchbase/lite/NotificationTest.java +++ b/common/test/java/com/couchbase/lite/NotificationTest.java @@ -344,6 +344,26 @@ public void testLegacyChangeAPI() throws CouchbaseLiteException, InterruptedExce CollectionChangeListener colListener = change -> latch2.countDown(); colListener.changed(new CollectionChange(getTestCollection(), Collections.emptyList())); Assert.assertTrue(latch2.await(STD_TIMEOUT_SEC, TimeUnit.SECONDS)); + + CountDownLatch latch3 = new CountDownLatch(2); + try ( + ListenerToken ignore1 = getTestDatabase().addChangeListener(change -> latch3.countDown()); + ListenerToken ignore2 = defaultCollection.addChangeListener(change -> latch3.countDown())) { + Assert.assertEquals(2, defaultCollection.getCollectionListenerCount()); + saveDocsInCollection(createTestDocs(1000, 10), defaultCollection); + Assert.assertTrue(latch3.await(STD_TIMEOUT_SEC, TimeUnit.SECONDS)); + } + + Assert.assertEquals(0, defaultCollection.getCollectionListenerCount()); + + CountDownLatch latch4 = new CountDownLatch(2); + try (ListenerToken ignore1 = getTestDatabase().addChangeListener(change -> latch4.countDown()); + ListenerToken ignore2 = defaultCollection.addChangeListener(change -> latch4.countDown())) { + Assert.assertEquals(2, defaultCollection.getCollectionListenerCount()); + saveDocsInCollection(createTestDocs(2000, 10), defaultCollection); + Assert.assertTrue(latch4.await(STD_TIMEOUT_SEC, TimeUnit.SECONDS)); + } + Assert.assertEquals(0, defaultCollection.getCollectionListenerCount()); } // Kotlin shims diff --git a/common/test/java/com/couchbase/lite/PreInitTest.kt b/common/test/java/com/couchbase/lite/PreInitTest.kt index 0aea490de..8b4c092cf 100644 --- a/common/test/java/com/couchbase/lite/PreInitTest.kt +++ b/common/test/java/com/couchbase/lite/PreInitTest.kt @@ -47,4 +47,11 @@ class PreInitTest : BaseTest() { fun testCreateDatabaseBeforeInit() { Assert.assertThrows(CouchbaseLiteError::class.java) { Database("fail") } } + + @Test + fun testCreateReplConfigBeforeInit() { + Assert.assertThrows(CouchbaseLiteError::class.java) { + ReplicatorConfiguration(URLEndpoint(URI("wss://foo.bar"))) + } + } } diff --git a/common/test/java/com/couchbase/lite/ReplicatorConfigurationTest.kt b/common/test/java/com/couchbase/lite/ReplicatorConfigurationTest.kt index 9748d2ca8..8a481d181 100644 --- a/common/test/java/com/couchbase/lite/ReplicatorConfigurationTest.kt +++ b/common/test/java/com/couchbase/lite/ReplicatorConfigurationTest.kt @@ -133,7 +133,7 @@ class ReplicatorConfigurationTest : BaseReplicatorTest() { val collectConfigs = ReplicatorConfiguration( CollectionConfiguration.fromCollections(setOf(testDatabase.defaultCollection)), mockURLEndpoint - ).collections + ).collectionConfigs collectConfigs.forEach { it.conflictResolver = resolver } @@ -159,7 +159,7 @@ class ReplicatorConfigurationTest : BaseReplicatorTest() { val collectConfigs = CollectionConfiguration.fromCollections(setOf(testDatabase.defaultCollection)) val replConfig1 = ReplicatorConfiguration(collectConfigs, mockURLEndpoint) - replConfig1.collections.forEach { collecConfig -> + replConfig1.collectionConfigs.forEach { collecConfig -> collecConfig .setPushFilter(pushFilter1) .setPullFilter(pullFilter1) @@ -167,7 +167,7 @@ class ReplicatorConfigurationTest : BaseReplicatorTest() { .setDocumentIDs(listOf("doc1", "doc2")) } - replConfig1.collections.forEach { collecConfig -> + replConfig1.collectionConfigs.forEach { collecConfig -> Assert.assertEquals(pushFilter1, collecConfig.pushFilter) Assert.assertEquals(pullFilter1, collecConfig.pullFilter) Assert.assertArrayEquals(arrayOf("CNBC", "ABC"), collecConfig.channels?.toTypedArray()) @@ -216,7 +216,7 @@ class ReplicatorConfigurationTest : BaseReplicatorTest() { val collectConfigA = CollectionConfiguration(collectionA) val replConfig1 = ReplicatorConfiguration(setOf(collectConfigA), mockURLEndpoint) - val collectionConfigs = replConfig1.collections + val collectionConfigs = replConfig1.collectionConfigs Assert.assertEquals(1, collectionConfigs.size) Assert.assertTrue(collectionConfigs.contains(collectConfigA)) } @@ -274,4 +274,787 @@ class ReplicatorConfigurationTest : BaseReplicatorTest() { testDatabase.getCollection("colA", "scopeA") } } + + /****************** Scopes and Collections Section 8.13 ****************/ + + + // 8.13.1a Create a config object with ReplicatorConfiguration.init(database: database, + // endpoint: endpoint). + // + // Access collections property. The returned collections will have one collection + // which is the default collection. + // + // Access database property, and the database object from the init should be + // returned. + @Suppress("DEPRECATION") + @Test + fun testCreateConfigWithDatabaseA() { + val replConfig = ReplicatorConfiguration(testDatabase, mockURLEndpoint) + val collections = replConfig.collections + Assert.assertEquals(1, collections.size) + Assert.assertTrue(collections.contains(testDatabase.defaultCollection)) + Assert.assertEquals(testDatabase, replConfig.database) + } + + // 8.13.1b Create a config object with ReplicatorConfiguration.init(database: database, + // endpoint: endpoint). + // + // Access collections property. The returned collections will have one collection + // which is the default collection. + // + // Call getCollectionConfig() method with the default collection. A + // CollectionConfiguration object should be returned. + // + // Check CollectionConfiguration.conflictResolver, .pushFilter, pullFilters, + // channels, and documentIDs. The return object of those properties should be NULL. + @Suppress("DEPRECATION") + @Test + fun testCreateConfigWithDatabaseB() { + val collectionConfig = ReplicatorConfiguration(testDatabase, mockURLEndpoint) + .getCollectionConfiguration(testDatabase.defaultCollection) + Assert.assertNotNull(collectionConfig) + Assert.assertNull(collectionConfig!!.conflictResolver) + Assert.assertNull(collectionConfig.pushFilter) + Assert.assertNull(collectionConfig.pullFilter) + Assert.assertNull(collectionConfig.channels) + Assert.assertNull(collectionConfig.documentIDs) + } + + // 8.13.2 Create a config object with ReplicatorConfiguration.init(database: database, + // endpoint: endpoint). + // + // Set ReplicatorConfiguration.conflictResolver with a conflict resolver. + // + // Call getCollectionConfig() method with the default collection. A + // CollectionConfiguration object should be returned. + // + // Check CollectionConfiguration.conflictResolver. The returned conflict resolver + // should be the same as ReplicatorConfiguration.conflictResolver. + @Suppress("DEPRECATION") + @Test + fun testCreateConfigWithDatabaseAndConflictResolver() { + val resolver = localResolver + val replConfig = ReplicatorConfiguration(testDatabase, mockURLEndpoint).setConflictResolver(resolver) + Assert.assertEquals(resolver, replConfig.conflictResolver) + val collectionConfig = replConfig.getCollectionConfiguration(testDatabase.defaultCollection) + Assert.assertNotNull(collectionConfig) + Assert.assertEquals(resolver, collectionConfig?.conflictResolver) + } + + // 8.13.3Create a config object with ReplicatorConfiguration.init(database: database, + // endpoint: endpoint). + // + // Set ReplicatorConfiguration.conflictResolver with a conflict resolver. + // + // Call getCollectionConfig() method with the default collection. Check + // CollectionConfiguration.conflictResolver. The conflict resolver should be the + // same as ReplicatorConfiguration.conflictResolver. + // + // Update ReplicatorConfiguration.conflictResolver with a new conflict resolver. + // + // Call getCollectionConfig() method with the default collection. Check + // CollectionConfiguration.conflictResolver. The conflict resolver should be + // updated accordingly. + // + // Update CollectionConfiguration.conflictResolver with a new conflict resolver. + // Use addCollection() method to add the default collection with the updated + // config. + // + // Check ReplicatorConfiguration.conflictResolver. The conflict resolver should be + // updated accordingly. + @Suppress("DEPRECATION") + @Test + fun testUpdateConflictResolverForDefaultCollection() { + val resolver = localResolver + val replConfig = ReplicatorConfiguration(testDatabase, mockURLEndpoint).setConflictResolver(resolver) + Assert.assertEquals( + replConfig.conflictResolver, + replConfig.getCollectionConfiguration(testDatabase.defaultCollection)?.conflictResolver + ) + val resolver2 = localResolver + replConfig.conflictResolver = resolver2 + Assert.assertEquals( + resolver2, + replConfig.getCollectionConfiguration(testDatabase.defaultCollection)?.conflictResolver + ) + } + + // 8.13.4 Create a config object with ReplicatorConfiguration.init(database: + // database, endpoint: endpoint). + // + // Set values to ReplicatorConfiguration.pushFilter, pullFilters, channels, and + // documentIDs + // + // Call getCollectionConfig() method with the default collection. A + // CollectionConfiguration object should be returned. The filters in the config + // should be the same ReplicatorConfiguration.pushFilter, pullFilters, channels, + // and documentIDs. + @Suppress("DEPRECATION") + @Test + fun testCreateConfigWithDatabaseAndFilters() { + val pushFilter1 = ReplicationFilter { _, _ -> true } + val pullFilter1 = ReplicationFilter { _, _ -> true } + val replConfig1 = ReplicatorConfiguration(testDatabase, mockURLEndpoint) + .setPushFilter(pushFilter1) + .setPullFilter(pullFilter1) + .setChannels(listOf("CNBC", "ABC")) + .setDocumentIDs(listOf("doc1", "doc2")) + Assert.assertEquals(pushFilter1, replConfig1.pushFilter) + Assert.assertEquals(pullFilter1, replConfig1.pullFilter) + Assert.assertArrayEquals(arrayOf("CNBC", "ABC"), replConfig1.channels?.toTypedArray()) + Assert.assertArrayEquals(arrayOf("doc1", "doc2"), replConfig1.documentIDs?.toTypedArray()) + + val collectionConfig1 = replConfig1.getCollectionConfiguration(testDatabase.defaultCollection) + Assert.assertNotNull(collectionConfig1) + Assert.assertEquals(pushFilter1, collectionConfig1!!.pushFilter) + Assert.assertEquals(pullFilter1, collectionConfig1.pullFilter) + Assert.assertArrayEquals(arrayOf("CNBC", "ABC"), collectionConfig1.channels?.toTypedArray()) + Assert.assertArrayEquals(arrayOf("doc1", "doc2"), collectionConfig1.documentIDs?.toTypedArray()) + } + + // 8.13.5a Create a config object with ReplicatorConfiguration.init(database: database, + // endpoint: endpoint). + // + // Set values to ReplicatorConfiguration.pushFilter, pullFilters, channels, and + // documentIDs. + // + // Call getCollectionConfig() method with the default collection. A + // CollectionConfiguration object should be returned. The filters in the config + // should be the same ReplicatorConfiguration.pushFilter, pullFilters, channels, + // and documentIDs. + // + // Update ReplicatorConfiguration.pushFilter, pullFilters, channels, and + // documentIDs with new values. + // + // Call getCollectionConfig() method with the default collection object getting + // from the database. A CollectionConfiguration object should be returned. The + // filters in the config be updated accordingly. + // + // Update CollectionConfiguration.pushFilter, pullFilters, channels, and + // documentIDs with new values. Use addCollection() method to add the default + // collection with the updated config. + // + // Check ReplicatorConfiguration.pushFilter, pullFilters, channels, and + // documentIDs. The filters should be updated accordingly. + @Suppress("DEPRECATION") + @Test + fun testUpdateFiltersForDefaultCollectionA() { + val pushFilter1 = ReplicationFilter { _, _ -> true } + val pullFilter1 = ReplicationFilter { _, _ -> true } + val replConfig1 = ReplicatorConfiguration(testDatabase, mockURLEndpoint) + .setPushFilter(pushFilter1) + .setPullFilter(pullFilter1) + .setChannels(listOf("CNBC", "ABC")) + .setDocumentIDs(listOf("doc1", "doc2")) + Assert.assertEquals(pushFilter1, replConfig1.pushFilter) + Assert.assertEquals(pullFilter1, replConfig1.pullFilter) + Assert.assertArrayEquals(arrayOf("CNBC", "ABC"), replConfig1.channels?.toTypedArray()) + Assert.assertArrayEquals(arrayOf("doc1", "doc2"), replConfig1.documentIDs?.toTypedArray()) + + val collectionConfig1 = replConfig1.getCollectionConfiguration(testDatabase.defaultCollection) + Assert.assertNotNull(collectionConfig1) + Assert.assertEquals(pushFilter1, collectionConfig1!!.pushFilter) + Assert.assertEquals(pullFilter1, collectionConfig1.pullFilter) + Assert.assertArrayEquals(arrayOf("CNBC", "ABC"), collectionConfig1.channels?.toTypedArray()) + Assert.assertArrayEquals(arrayOf("doc1", "doc2"), collectionConfig1.documentIDs?.toTypedArray()) + + val pushFilter2 = ReplicationFilter { _, _ -> true } + val pullFilter2 = ReplicationFilter { _, _ -> true } + replConfig1 + .setPushFilter(pushFilter2) + .setPullFilter(pullFilter2) + .setChannels(listOf("Peacock", "History")).documentIDs = listOf("doc3") + + Assert.assertEquals(pushFilter1, collectionConfig1.pushFilter) + Assert.assertEquals(pullFilter1, collectionConfig1.pullFilter) + Assert.assertArrayEquals(arrayOf("CNBC", "ABC"), collectionConfig1.channels?.toTypedArray()) + Assert.assertArrayEquals(arrayOf("doc1", "doc2"), collectionConfig1.documentIDs?.toTypedArray()) + + val collectionConfig2 = replConfig1.getCollectionConfiguration(testDatabase.defaultCollection) + Assert.assertNotNull(collectionConfig2) + Assert.assertEquals(pushFilter2, collectionConfig2!!.pushFilter) + Assert.assertEquals(pullFilter2, collectionConfig2.pullFilter) + Assert.assertArrayEquals(arrayOf("Peacock", "History"), collectionConfig2.channels?.toTypedArray()) + Assert.assertArrayEquals(arrayOf("doc3"), collectionConfig2.documentIDs?.toTypedArray()) + } + + + // 8.13.5b Create a config object with ReplicatorConfiguration.init(database: database, + // endpoint: endpoint). + // + // Set values to ReplicatorConfiguration.pushFilter, pullFilters, channels, and + // documentIDs. + // + // Call getCollectionConfig() method with the default collection. A + // CollectionConfiguration object should be returned. The filters in the config + // should be the same ReplicatorConfiguration.pushFilter, pullFilters, channels, + // and documentIDs. + // + // Call getCollectionConfig() method with the default collection object getting + // from the database. A CollectionConfiguration object should be returned. The + // filters in the config be updated accordingly. + // + // Update CollectionConfiguration.pushFilter, pullFilters, channels, and + // documentIDs with new values. Use addCollection() method to add the default + // collection with the updated config. + // + // Check ReplicatorConfiguration.pushFilter, pullFilters, channels, and + // documentIDs. The filters should be updated accordingly. + @Suppress("DEPRECATION") + @Test + fun testUpdateFiltersForDefaultCollectionB() { + val pushFilter1 = ReplicationFilter { _, _ -> true } + val pullFilter1 = ReplicationFilter { _, _ -> true } + val replConfig1 = ReplicatorConfiguration(testDatabase, mockURLEndpoint) + .setPushFilter(pushFilter1) + .setPullFilter(pullFilter1) + .setChannels(listOf("CNBC", "ABC")) + .setDocumentIDs(listOf("doc1", "doc2")) + Assert.assertEquals(pushFilter1, replConfig1.pushFilter) + Assert.assertEquals(pullFilter1, replConfig1.pullFilter) + Assert.assertArrayEquals(arrayOf("CNBC", "ABC"), replConfig1.channels?.toTypedArray()) + Assert.assertArrayEquals(arrayOf("doc1", "doc2"), replConfig1.documentIDs?.toTypedArray()) + + val collectionConfig1 = replConfig1.getCollectionConfiguration(testDatabase.defaultCollection) + Assert.assertNotNull(collectionConfig1) + Assert.assertEquals(pushFilter1, collectionConfig1!!.pushFilter) + Assert.assertEquals(pullFilter1, collectionConfig1.pullFilter) + Assert.assertArrayEquals(arrayOf("CNBC", "ABC"), collectionConfig1.channels?.toTypedArray()) + Assert.assertArrayEquals(arrayOf("doc1", "doc2"), collectionConfig1.documentIDs?.toTypedArray()) + + val pushFilter2 = ReplicationFilter { _, _ -> true } + val pullFilter2 = ReplicationFilter { _, _ -> true } + val collectionConfig2 = CollectionConfiguration() + .setPushFilter(pushFilter2) + .setPullFilter(pullFilter2) + .setChannels(listOf("Peacock", "History")) + .setDocumentIDs(listOf("doc3")) + replConfig1.addCollection(testDatabase.defaultCollection, collectionConfig2) + + Assert.assertEquals(pushFilter2, replConfig1.pushFilter) + Assert.assertEquals(pullFilter2, replConfig1.pullFilter) + Assert.assertArrayEquals(arrayOf("Peacock", "History"), replConfig1.channels?.toTypedArray()) + Assert.assertArrayEquals(arrayOf("doc3"), replConfig1.documentIDs?.toTypedArray()) + + val collectionConfig3 = replConfig1.getCollectionConfiguration(testDatabase.defaultCollection) + Assert.assertNotNull(collectionConfig3) + Assert.assertEquals(pushFilter2, collectionConfig3!!.pushFilter) + Assert.assertEquals(pullFilter2, collectionConfig3.pullFilter) + Assert.assertArrayEquals(arrayOf("Peacock", "History"), collectionConfig3.channels?.toTypedArray()) + Assert.assertArrayEquals(arrayOf("doc3"), collectionConfig3.documentIDs?.toTypedArray()) + } + + // 8.13.6a Create a config object with ReplicatorConfiguration.init(endpoint: endpoint). + // Access collections property and an empty collection list should be returned.\ + @Suppress("DEPRECATION") + @Test + fun testCreateConfigWithEndpointOnly1() { + val replConfig1 = ReplicatorConfiguration(mockURLEndpoint) + + val collections = replConfig1.collections + Assert.assertNotNull(collections) + Assert.assertTrue(collections.isEmpty()) + } + + // 8.13.6b Create a config object with ReplicatorConfiguration.init(endpoint: endpoint). + // Access collections property and an empty collection list should be returned. + // Access database property and Illegal State Exception will be thrown. + @Suppress("DEPRECATION") + @Test + fun testCreateConfigWithEndpointOnly2() { + val replConfig1 = ReplicatorConfiguration(mockURLEndpoint) + Assert.assertThrows(CouchbaseLiteError::class.java) { replConfig1.database } + } + + // 8.13.7 Create Collection "colA" and "colB" in the scope "scopeA". + // + // Create a config object with ReplicatorConfiguration.init(endpoint: endpoint). + // + // Use addCollections() to add both colA and colB to the config without specifying + // a collection config. + // + // Check ReplicatorConfiguration.collections. The collections should have colA and + // colB. + // + // Use getCollectionConfig() to get the collection config for colA and colB. Check + // the returned configs of both collections. The returned configs should be + // different instances. The conflict resolver and filters of both configs should be + // all NULL. + @Suppress("DEPRECATION") + @Test + fun testAddCollectionsWithoutCollectionConfig() { + val collectionA = testDatabase.createCollection("colA", "scopeA") + val collectionB = testDatabase.createCollection("colB", "scopeA") + + val replConfig1 = ReplicatorConfiguration(mockURLEndpoint) + replConfig1.addCollections(setOf(collectionA, collectionB), null) + + val collectionConfig1 = replConfig1.getCollectionConfiguration(collectionA) + Assert.assertNotNull(collectionConfig1) + Assert.assertNull(collectionConfig1!!.conflictResolver) + Assert.assertNull(collectionConfig1.pushFilter) + Assert.assertNull(collectionConfig1.pullFilter) + Assert.assertNull(collectionConfig1.channels) + Assert.assertNull(collectionConfig1.documentIDs) + + val collectionConfig2 = replConfig1.getCollectionConfiguration(collectionB) + Assert.assertNotNull(collectionConfig2) + Assert.assertNull(collectionConfig1.conflictResolver) + Assert.assertNull(collectionConfig1.pushFilter) + Assert.assertNull(collectionConfig1.pullFilter) + Assert.assertNull(collectionConfig1.channels) + Assert.assertNull(collectionConfig1.documentIDs) + + Assert.assertNotSame(collectionConfig1, collectionConfig2) + } + + // 8.13.8 Create Collection "colA" and "colB" in the scope "scopeA". + // + // Create a config object with ReplicatorConfiguration.init(endpoint: endpoint). + // + // Create a CollectionConfiguration object, and set a conflictResolver and all + // filters. + // + // Use addCollections() to add both colA and colB to the config created from the + // previous step. + // + // Check ReplicatorConfiguration.collections. The collections should have colA and + // colB. + // + // Use getCollectionConfig() to get the collection config for colA and colB. The + // returned configs of both collections should be different instances. The conflict + // resolver and filters of both configs should be the same as what was specified + // when calling addCollections(). + @Suppress("DEPRECATION") + @Test + fun testAddCollectionsWithCollectionConfig() { + val collectionA = testDatabase.createCollection("colA", "scopeA") + val collectionB = testDatabase.createCollection("colB", "scopeA") + + val replConfig1 = ReplicatorConfiguration(mockURLEndpoint) + + val pushFilter1 = ReplicationFilter { _, _ -> true } + val pullFilter1 = ReplicationFilter { _, _ -> true } + val resolver = localResolver + val collectionConfig0 = CollectionConfiguration() + .setPushFilter(pushFilter1) + .setPullFilter(pullFilter1) + .setConflictResolver(resolver) + replConfig1.addCollections(setOf(collectionA, collectionB), collectionConfig0) + + val collectionConfig1 = replConfig1.getCollectionConfiguration(collectionA) + Assert.assertNotNull(collectionConfig1) + Assert.assertEquals(pushFilter1, collectionConfig1!!.pushFilter) + Assert.assertEquals(pullFilter1, collectionConfig1.pullFilter) + Assert.assertEquals(resolver, collectionConfig1.conflictResolver) + + val collectionConfig2 = replConfig1.getCollectionConfiguration(collectionB) + Assert.assertNotNull(collectionConfig2) + Assert.assertEquals(pushFilter1, collectionConfig2!!.pushFilter) + Assert.assertEquals(pullFilter1, collectionConfig2.pullFilter) + Assert.assertEquals(resolver, collectionConfig2.conflictResolver) + + Assert.assertNotSame(collectionConfig1, collectionConfig2) + } + + // 8.13.9 Create Collection "colA" and "colB" in the scope "scopeA". + // + // Create a config object with ReplicatorConfiguration.init(endpoint: endpoint). + // + // Use addCollection() to add the colA without specifying a collection config. + // + // Create a CollectionConfiguration object, and set a conflictResolver and all + // filters. + // + // Use addCollection() to add the colB with the collection config created from the + // previous step. + // + // Check ReplicatorConfiguration.collections. The collections should have colA and + // colB. + // + // Use getCollectionConfig() to get the collection config for colA and colB. The + // returned config of the colA should contain all NULL values. The returned config + // of the colB should contain the values according to the config used when adding + // the collection. + @Suppress("DEPRECATION") + @Test + fun testAddCollection() { + val collectionA = testDatabase.createCollection("colA", "scopeA") + val collectionB = testDatabase.createCollection("colB", "scopeA") + + val replConfig1 = ReplicatorConfiguration(mockURLEndpoint) + + replConfig1.addCollection(collectionA, null) + + val pushFilter1 = ReplicationFilter { _, _ -> true } + val pullFilter1 = ReplicationFilter { _, _ -> true } + val resolver = localResolver + val collectionConfig0 = CollectionConfiguration() + .setPushFilter(pushFilter1) + .setPullFilter(pullFilter1) + .setConflictResolver(resolver) + replConfig1.addCollection(collectionB, collectionConfig0) + + Assert.assertTrue(replConfig1.collections.contains(collectionA)) + Assert.assertTrue(replConfig1.collections.contains(collectionB)) + + val collectionConfig1 = replConfig1.getCollectionConfiguration(collectionA) + Assert.assertNotNull(collectionConfig1) + Assert.assertNull(collectionConfig1!!.pushFilter) + Assert.assertNull(collectionConfig1.pullFilter) + Assert.assertNull(collectionConfig1.conflictResolver) + + val collectionConfig2 = replConfig1.getCollectionConfiguration(collectionB) + Assert.assertNotNull(collectionConfig2) + Assert.assertEquals(pushFilter1, collectionConfig2!!.pushFilter) + Assert.assertEquals(pullFilter1, collectionConfig2.pullFilter) + Assert.assertEquals(resolver, collectionConfig2.conflictResolver) + } + + // 8.13.10a Create Collection "colA" and "colB" in the scope "scopeA". + // + // Create a config object with ReplicatorConfiguration.init(endpoint: endpoint). + // + // Create a CollectionConfiguration object, and set a conflictResolver and all + // filters. + // + // Use addCollection() to add the colA and colB with the collection config created + // from the previous step. + // + // Check ReplicatorConfiguration.collections. The collections should have colA and + // colB. + // + // Use getCollectionConfig() to get the collection config for colA and colB. Check + // the returned configs of both collections and ensure that both configs contain + // the values correctly. + // + // Use addCollection() to add colA again without specifying collection config. + // + // Create a new CollectionConfiguration object, and set a conflictResolver and all + // filters. + // + // Use addCollection() to add colB again with the updated collection config created + // from the previous step. + // + // Use getCollectionConfig() to get the collection config for colA and colB. Check + // the returned configs of both collections and ensure that both configs contain + // the updated values correctly. + @Suppress("DEPRECATION") + @Test + fun testUpdateCollectionConfigA() { + val collectionA = testDatabase.createCollection("colA", "scopeA") + val collectionB = testDatabase.createCollection("colB", "scopeA") + + val replConfig1 = ReplicatorConfiguration(mockURLEndpoint) + + val pushFilter1 = ReplicationFilter { _, _ -> true } + val pullFilter1 = ReplicationFilter { _, _ -> true } + val resolver1 = localResolver + val collectionConfig0 = CollectionConfiguration() + .setPushFilter(pushFilter1) + .setPullFilter(pullFilter1) + .setConflictResolver(resolver1) + + replConfig1.addCollection(collectionA, collectionConfig0) + replConfig1.addCollection(collectionB, collectionConfig0) + + + Assert.assertTrue(replConfig1.collections.contains(collectionA)) + Assert.assertTrue(replConfig1.collections.contains(collectionB)) + + val collectionConfig1 = replConfig1.getCollectionConfiguration(collectionA) + Assert.assertNotNull(collectionConfig1) + Assert.assertEquals(pushFilter1, collectionConfig1!!.pushFilter) + Assert.assertEquals(pullFilter1, collectionConfig1.pullFilter) + Assert.assertEquals(resolver1, collectionConfig1.conflictResolver) + + val collectionConfig2 = replConfig1.getCollectionConfiguration(collectionB) + Assert.assertNotNull(collectionConfig2) + Assert.assertEquals(pushFilter1, collectionConfig2!!.pushFilter) + Assert.assertEquals(pullFilter1, collectionConfig2.pullFilter) + Assert.assertEquals(resolver1, collectionConfig2.conflictResolver) + + val pushFilter2 = ReplicationFilter { _, _ -> true } + val pullFilter2 = ReplicationFilter { _, _ -> true } + val resolver2 = localResolver + val collectionConfig3 = CollectionConfiguration() + .setPushFilter(pushFilter2) + .setPullFilter(pullFilter2) + .setConflictResolver(resolver2) + + replConfig1.addCollection(collectionA, null) + replConfig1.addCollection(collectionB, collectionConfig3) + + val collectionConfig4 = replConfig1.getCollectionConfiguration(collectionA) + Assert.assertNotNull(collectionConfig3) + Assert.assertNull(collectionConfig4!!.pushFilter) + Assert.assertNull(collectionConfig4.pullFilter) + Assert.assertNull(collectionConfig4.conflictResolver) + + val collectionConfig5 = replConfig1.getCollectionConfiguration(collectionB) + Assert.assertNotNull(collectionConfig5) + Assert.assertEquals(pushFilter2, collectionConfig5!!.pushFilter) + Assert.assertEquals(pullFilter2, collectionConfig5.pullFilter) + Assert.assertEquals(resolver2, collectionConfig5.conflictResolver) + } + + + // 8.13.10a Create Collection "colA" and "colB" in the scope "scopeA". + // + // Create a config object with ReplicatorConfiguration.init(endpoint: endpoint). + // + // Create a CollectionConfiguration object, and set a conflictResolver and all + // filters. + // + // Use addCollection() to add the colA and colB with the collection config created + // from the previous step. + // + // Check ReplicatorConfiguration.collections. The collections should have colA and + // colB. + // + // Use getCollectionConfig() to get the collection config for colA and colB. Check + // the returned configs of both collections and ensure that both configs contain + // the values correctly. + // + // Use addCollection() to add colA again without specifying collection config. + // + // Create a new CollectionConfiguration object, and set a conflictResolver and all + // filters. + // + // Use addCollection() to add colB again with the updated collection config created + // from the previous step. + // + // Use getCollectionConfig() to get the collection config for colA and colB. Check + // the returned configs of both collections and ensure that both configs contain + // the updated values correctly. + @Suppress("DEPRECATION") + @Test + fun testUpdateCollectionConfigB() { + val defaultCollection = testDatabase.defaultCollection + val collectionA = testDatabase.createCollection("colA", "scopeA") + + val filter = ReplicationFilter { _, _ -> true } + + val replConfig = ReplicatorConfiguration(mockURLEndpoint) + var collConfig = CollectionConfiguration(null, null, filter, null, null) + + replConfig.addCollections(listOf(defaultCollection, collectionA), collConfig) + + collConfig = replConfig.getCollectionConfiguration(defaultCollection)!! + Assert.assertEquals(filter, collConfig.pullFilter) + Assert.assertEquals(null, collConfig.pushFilter) + + collConfig = replConfig.getCollectionConfiguration(collectionA)!! + Assert.assertEquals(filter, collConfig.pullFilter) + Assert.assertEquals(null, collConfig.pushFilter) + + replConfig.pushFilter = filter + + collConfig = replConfig.getCollectionConfiguration(defaultCollection)!! + Assert.assertEquals(filter, collConfig.pullFilter) + Assert.assertEquals(filter, collConfig.pushFilter) + + collConfig = replConfig.getCollectionConfiguration(collectionA)!! + Assert.assertEquals(filter, collConfig.pullFilter) + Assert.assertEquals(null, collConfig.pushFilter) + } + + // 8.13.11 Create Collection "colA" and "colB" in the scope "scopeA". + // + // Create a config object with ReplicatorConfiguration.init(endpoint: endpoint). + // + // Create a CollectionConfiguration object, and set a conflictResolvers and all + // filters. + // + // Use addCollections() to add both colA and colB to the config with the + // CollectionConfiguration created from the previous step. + // + // Check ReplicatorConfiguration.collections. The collections should have colA and + // colB. + // + // Use getCollectionConfig() to get the collection config for colA and colB. Check + // the returned config of both collections and ensure that both configs contain the + // values correctly. + // + // Remove "colB" by calling removeCollection(). + // + // Check ReplicatorConfiguration.collections. The collections should have only + // colA. + // + // Use getCollectionConfig() to get the collection config for colA and colB. The + // returned config for the colB should be NULL. + @Suppress("DEPRECATION") + @Test + fun testRemoveCollection() { + val collectionA = testDatabase.createCollection("colA", "scopeA") + val collectionB = testDatabase.createCollection("colB", "scopeA") + + val replConfig1 = ReplicatorConfiguration(mockURLEndpoint) + + val pushFilter1 = ReplicationFilter { _, _ -> true } + val pullFilter1 = ReplicationFilter { _, _ -> true } + val resolver1 = localResolver + val collectionConfig0 = CollectionConfiguration() + .setPushFilter(pushFilter1) + .setPullFilter(pullFilter1) + .setConflictResolver(resolver1) + + replConfig1.addCollection(collectionA, collectionConfig0) + replConfig1.addCollection(collectionB, collectionConfig0) + + Assert.assertTrue(replConfig1.collections.contains(collectionA)) + Assert.assertTrue(replConfig1.collections.contains(collectionB)) + + replConfig1.removeCollection(collectionB) + Assert.assertTrue(replConfig1.collections.contains(collectionA)) + Assert.assertFalse(replConfig1.collections.contains(collectionB)) + + Assert.assertNotNull(replConfig1.getCollectionConfiguration(collectionA)) + Assert.assertNull(replConfig1.getCollectionConfiguration(collectionB)) + } + + // 8.13.12a Create collection "colA" in the scope "scopeA" using database instance A. + // + // Create collection "colB" in the scope "scopeA" using database instance B. + // + // Create a config object with ReplicatorConfiguration.init(endpoint: endpoint). + // + // Use addCollections() to add both colA and colB. An invalid argument exception + // should be thrown as the collections are from different database instances. + // + // Use addCollection() to add colA. Ensure that the colA has been added correctly. + // + // Use addCollection() to add colB. An invalid argument exception should be thrown + // as the collections are from different database instances. + @Suppress("DEPRECATION") + @Test + fun testAddCollectionsFromDifferentDatabaseInstancesA() { + val collectionA = testDatabase.createCollection("colA", "scopeA") + val collectionB = targetDatabase.createCollection("colB", "scopeA") + + val replConfig1 = ReplicatorConfiguration(mockURLEndpoint) + + Assert.assertThrows(IllegalArgumentException::class.java) { + replConfig1.addCollections(setOf(collectionA, collectionB), null) + } + } + + // 8.13.12b Create collection "colA" in the scope "scopeA" using database instance A. + // + // Create collection "colB" in the scope "scopeA" using database instance B. + // + // Create a config object with ReplicatorConfiguration.init(endpoint: endpoint). + // + // Use addCollections() to add both colA and colB. An invalid argument exception + // should be thrown as the collections are from different database instances. + // + // Use addCollection() to add colA. Ensure that the colA has been added correctly. + // + // Use addCollection() to add colB. An invalid argument exception should be thrown + // as the collections are from different database instances. + @Suppress("DEPRECATION") + @Test + fun testAddCollectionsFromDifferentDatabaseInstancesB() { + val collectionA = testDatabase.createCollection("colA", "scopeA") + val collectionB = targetDatabase.createCollection("colB", "scopeA") + + val replConfig1 = ReplicatorConfiguration(mockURLEndpoint) + + replConfig1.addCollection(collectionA, null) + + Assert.assertThrows(IllegalArgumentException::class.java) { replConfig1.addCollection(collectionB, null) } + } + + // 8.13.13a Create collection "colA" in the scope "scopeA" using database instance A. + // + // Create collection "colB" in the scope "scopeA" using database instance B. + // + // Delete collection colB. + // + // Create a config object with ReplicatorConfiguration.init(endpoint: endpoint). + // + // Use addCollections() to add both colA and colB. An invalid argument exception should be thrown as an added collection has been deleted. + // + // Use addCollection() to add colA. Ensure that the colA has been added correctly. + // + // Use addCollection() to add colB. An invalid argument exception should be thrown as an added collection has been deleted. + @Suppress("DEPRECATION") + @Test + fun testAddDeletedCollectionsA() { + val collectionA = testDatabase.createCollection("colA", "scopeA") + val collectionB = targetDatabase.createCollection("colB", "scopeA") + + testDatabase.deleteCollection("colB", "scopeA") + + Assert.assertThrows(IllegalArgumentException::class.java) { + ReplicatorConfiguration(mockURLEndpoint).addCollections(setOf(collectionA, collectionB), null) + } + } + + + // 8.13.13a Create collection "colA" in the scope "scopeA" using database instance A. + // + // Create collection "colB" in the scope "scopeA" using database instance B. + // + // Delete collection colB. + // + // Create a config object with ReplicatorConfiguration.init(endpoint: endpoint). + // + // Use addCollections() to add both colA and colB. An invalid argument exception should be thrown as an added collection has been deleted. + // + // Use addCollection() to add colA. Ensure that the colA has been added correctly. + // + // Use addCollection() to add colB. An invalid argument exception should be thrown as an added collection has been deleted. + @Suppress("DEPRECATION") + @Test + fun testAddDeletedCollectionsB() { + val collectionA = testDatabase.createCollection("colA", "scopeA") + targetDatabase.createCollection("colB", "scopeA") + + testDatabase.deleteCollection("colB", "scopeA") + + val replConfig1 = ReplicatorConfiguration(mockURLEndpoint) + + replConfig1.addCollection(collectionA, null) + + val collections = replConfig1.collections + Assert.assertEquals(1, collections.size) + Assert.assertTrue(collections.contains(collectionA)) + } + + + // 8.13.13c Create collection "colA" in the scope "scopeA" using database instance A. + // + // Create collection "colB" in the scope "scopeA" using database instance B. + // + // Delete collection colB. + // + // Create a config object with ReplicatorConfiguration.init(endpoint: endpoint). + // + // Use addCollections() to add both colA and colB. An invalid argument exception should be thrown as an added collection has been deleted. + // + // Use addCollection() to add colA. Ensure that the colA has been added correctly. + // + // Use addCollection() to add colB. An invalid argument exception should be thrown as an added collection has been deleted. + @Suppress("DEPRECATION") + @Test + fun testAddDeletedCollectionsC() { + testDatabase.createCollection("colA", "scopeA") + val collectionB = targetDatabase.createCollection("colB", "scopeA") + + targetDatabase.deleteCollection("colB", "scopeA") + + Assert.assertThrows(IllegalArgumentException::class.java) { + ReplicatorConfiguration(mockURLEndpoint).addCollection(collectionB, null) + } + } + + // CBL-3736 + // Attempting to configure a replicator with no collection + // should throw an illegal argument exception. + @Suppress("DEPRECATION") + @Test + fun testCreateReplicatorWithNoCollections() { + Assert.assertThrows(IllegalArgumentException::class.java) { Replicator(ReplicatorConfiguration(mockURLEndpoint)) } + } } diff --git a/common/test/java/com/couchbase/lite/logging/LogTest.kt b/common/test/java/com/couchbase/lite/logging/LogTest.kt index f790a4040..4890874c2 100644 --- a/common/test/java/com/couchbase/lite/logging/LogTest.kt +++ b/common/test/java/com/couchbase/lite/logging/LogTest.kt @@ -292,7 +292,7 @@ class LogTest : BaseDbTest() { val rex = Regex("cbl_(debug|verbose|info|warning|error)_\\d+\\.cbllog") for (file in files) { - Assert.assertTrue(file.name.matches(rex)) + Assert.assertTrue(file.name.contains("crash") || file.name.matches(rex)) } } } @@ -347,8 +347,9 @@ class LogTest : BaseDbTest() { ) { // This should create two files for each of the 5 levels except verbose (debug, info, warning, error): // 1k of logs plus .5k headers. There should be only one file at the verbose level (just the headers) + // and a file for writing crashes write1KBToLog() - Assert.assertEquals((4 * 2) + 1, logFiles.size) + Assert.assertEquals((4 * 2) + 2, logFiles.size) } } @@ -364,11 +365,12 @@ class LogTest : BaseDbTest() { ) { // This should create several files for each of the 5 levels except verbose (debug, info, warning, error): // 1k of logs plus .5k headers. Although lots of files are created they should be trimmed to only 3 - // at each level. There should be only one file at the verbose level (just the headers) + // at each level. There should be only one file at the verbose level (just the headers) and a log + // for recording crashes repeat(21) { write1KBToLog() } - Assert.assertEquals((4 * 3) + 1, logFiles.size) + Assert.assertEquals((4 * 3) + 2, logFiles.size) } } @@ -403,7 +405,7 @@ class LogTest : BaseDbTest() { Assert.assertNotNull(tempDir!!.listFiles()) for (log in logFiles!!) { val fn = log.name.lowercase(Locale.getDefault()) - if (fn.startsWith("cbl_debug_") || fn.startsWith("cbl_verbose_")) { + if (fn.startsWith("cbl_debug_") || fn.startsWith("cbl_verbose_") || fn.startsWith("cbl_crash")) { Assert.assertFalse(getLogContents(log).contains(uuidString)) } else { Assert.assertTrue(getLogContents(log).contains(uuidString)) @@ -420,6 +422,10 @@ class LogTest : BaseDbTest() { ) { write1KBToLog() for (log in logFiles) { + if (log.name.contains("crash")) { + continue; + } + var logLine: String BufferedReader(FileReader(log)).use { logLine = it.readLine() @@ -477,7 +483,7 @@ class LogTest : BaseDbTest() { Log.e(LogDomain.DATABASE, message, error) for (log in logFiles) { - if (!log.name.contains("verbose")) { + if (!log.name.contains("verbose") && !log.name.contains("crash")) { Assert.assertTrue(getLogContents(log).contains(uuid)) } } @@ -498,7 +504,7 @@ class LogTest : BaseDbTest() { Log.e(LogDomain.DATABASE, message, error, uuid2) for (log in logFiles) { - if (!log.name.contains("verbose")) { + if (!log.name.contains("verbose") && !log.name.contains("crash")) { val content = getLogContents(log) Assert.assertTrue(content.contains(uuid1)) Assert.assertTrue(content.contains(uuid2)) diff --git a/common/test/java/com/couchbase/lite/utils/KotlinHelpers.java b/common/test/java/com/couchbase/lite/utils/KotlinHelpers.java index dca0b87d1..5bb7c2d62 100644 --- a/common/test/java/com/couchbase/lite/utils/KotlinHelpers.java +++ b/common/test/java/com/couchbase/lite/utils/KotlinHelpers.java @@ -21,6 +21,7 @@ import com.couchbase.lite.Collection; import com.couchbase.lite.CouchbaseLiteException; +import com.couchbase.lite.LogFileConfiguration; import com.couchbase.lite.Replicator; @@ -34,4 +35,20 @@ public static boolean callIsDocumentPendingWithNullId(@NonNull Replicator repl, throws CouchbaseLiteException { return repl.isDocumentPending(null, collection); } + + // Kotlin will not allow a the call isDocumentPending(null) + public static boolean callIsDocumentPendingWithNullId(@NonNull Replicator repl) + throws CouchbaseLiteException { + return repl.isDocumentPending(null); + } + + // Kotlin will not allow a the call LogFileConfiguration.((String) null) + public static LogFileConfiguration createLogFileConfigWithNullDir() { + return new LogFileConfiguration((String) null); + } + + // Kotlin will not allow a the call LogFileConfiguration.((LogFileConfiguration) null) + public static LogFileConfiguration createLogFileConfigWithNullConfig() { + return new LogFileConfiguration((LogFileConfiguration) null); + } } diff --git a/common/test/kotlin/com/couchbase/lite/CommonConfigFactoryTest.kt b/common/test/kotlin/com/couchbase/lite/CommonConfigFactoryTest.kt index 62ceb5280..b94c8061b 100644 --- a/common/test/kotlin/com/couchbase/lite/CommonConfigFactoryTest.kt +++ b/common/test/kotlin/com/couchbase/lite/CommonConfigFactoryTest.kt @@ -78,6 +78,27 @@ class CommonConfigFactoryTest : BaseTest() { Assert.assertEquals(CONFIG_FACTORY_TEST_STRING, config2.expressions[0]) } + @Test + fun testLogFileConfigurationFactory() { + val config = LogFileConfigurationFactory.newConfig(directory = CONFIG_FACTORY_TEST_STRING, maxSize = 4096L) + Assert.assertEquals(CONFIG_FACTORY_TEST_STRING, config.directory) + Assert.assertEquals(4096L, config.maxSize) + } + + @Test + fun testLogFileConfigurationFactoryNullDir() { + Assert.assertThrows(IllegalArgumentException::class.java) { LogFileConfigurationFactory.newConfig() } + } + + @Test + fun testLogFileConfigurationFactoryCopy() { + val config1 = LogFileConfigurationFactory.newConfig(directory = CONFIG_FACTORY_TEST_STRING, maxSize = 4096L) + val config2 = config1.newConfig(maxSize = 1024L) + Assert.assertNotEquals(config1, config2) + Assert.assertEquals(CONFIG_FACTORY_TEST_STRING, config2.directory) + Assert.assertEquals(1024L, config2.maxSize) + } + @Test fun testFileLogSinkFactory() { val dir = getScratchDirectoryPath(getUniqueName("sink-dir")) diff --git a/common/test/kotlin/com/couchbase/lite/DeprecatedConfigFactoryTest.kt b/common/test/kotlin/com/couchbase/lite/DeprecatedConfigFactoryTest.kt new file mode 100644 index 000000000..13e8a3807 --- /dev/null +++ b/common/test/kotlin/com/couchbase/lite/DeprecatedConfigFactoryTest.kt @@ -0,0 +1,129 @@ +// +// Copyright (c) 2020 Couchbase. All rights reserved. +// COUCHBASE CONFIDENTIAL - part of Couchbase Lite Enterprise Edition +// +@file:Suppress("DEPRECATION") + +package com.couchbase.lite + +import org.junit.Assert +import org.junit.Test +import java.net.URI + + +// The suite of tests that verifies behavior +// with a deleted default collection are in +// cbl-java-common @ a2de0d43d09ce64fd3a1301dc35 +class DeprecatedConfigFactoryTest : BaseDbTest() { + private val testEndpoint = URLEndpoint(URI("ws://foo.couchbase.com/db")) + + ///// Test ReplicatorConfiguration Factory + + @Test + fun testReplicatorConfigNoArgs() { + Assert.assertThrows(IllegalArgumentException::class.java) { ReplicatorConfigurationFactory.create() } + } + + // Create on factory with no db should fail + @Test + fun testReplicatorConfigNoDb() { + Assert.assertThrows(IllegalArgumentException::class.java) { + ReplicatorConfigurationFactory.create(target = testEndpoint, type = ReplicatorType.PULL) + } + } + + // Create on factory with no target should fail + @Test + fun testReplicatorConfigNoProtocol() { + Assert.assertThrows(IllegalArgumentException::class.java) { + ReplicatorConfigurationFactory.create(testDatabase, type = ReplicatorType.PULL) + } + } + + // Create with db and endpoint should succeed + @Test + fun testReplicatorConfigWithGoodArgs() { + val config = ReplicatorConfigurationFactory.create(testDatabase, testEndpoint) + Assert.assertEquals(testDatabase, config.database) + Assert.assertEquals(testEndpoint, config.target) + } + + // Create should copy source + @Test + fun testReplicatorConfigCopy() { + val config1 = ReplicatorConfigurationFactory.create(testDatabase, testEndpoint, type = ReplicatorType.PULL) + val config2 = config1.create() + Assert.assertNotSame(config1, config2) + Assert.assertEquals(config1.database, config2.database) + Assert.assertEquals(config1.target, config2.target) + Assert.assertEquals(config1.type, config2.type) + } + + // Create should replace source + @Test + fun testReplicatorConfigReplace() { + val config1 = ReplicatorConfigurationFactory.create(testDatabase, testEndpoint, type = ReplicatorType.PULL) + val config2 = config1.create(type = ReplicatorType.PUSH) + Assert.assertNotSame(config1, config2) + Assert.assertEquals(config1.database, config2.database) + Assert.assertEquals(config1.target, config2.target) + Assert.assertEquals(ReplicatorType.PUSH, config2.type) + } + + // Create from a source explicitly specifying a default collection + @Test + fun testReplicatorConfigFromCollectionWithDefault() { + val config1 = ReplicatorConfigurationFactory + .newConfig(testEndpoint, mapOf(listOf(testDatabase.defaultCollection) to CollectionConfiguration())) + val config2 = config1.create() + Assert.assertNotSame(config1, config2) + Assert.assertEquals(config1.database, config2.database) + Assert.assertEquals(setOf(testCollection.database.defaultCollection), config2.collections) + } + + // Create from a source with default collection, explicitly specifying a non-default collection + @Test + fun testReplicatorConfigFromCollectionWithDefaultAndOther() { + val config1 = ReplicatorConfigurationFactory + .newConfig(testEndpoint, mapOf(listOf(testCollection) to CollectionConfiguration())) + val filter = ReplicationFilter { _, _ -> true } + + // Information gets lost here (the configuration of testCollection): should be a log message + val config2 = config1.create(pushFilter = filter) + + Assert.assertNotSame(config1, config2) + Assert.assertEquals(config1.database, config2.database) + + val db = config1.database + val defaultCollection = db.defaultCollection + + Assert.assertEquals(setOf(defaultCollection), config2.collections) + Assert.assertEquals(filter, config2.getCollectionConfiguration(defaultCollection)?.pushFilter) + } + + // Create with one of the parameters that has migrated to the collection configuration + @Test + fun testReplicatorFromCollectionWithLegacyParameter() { + val config = ReplicatorConfigurationFactory.create(testDatabase, testEndpoint, channels = listOf("boop")) + Assert.assertEquals(testDatabase, config.database) + Assert.assertEquals(testEndpoint, config.target) + Assert.assertEquals( + listOf("boop"), + config.getCollectionConfiguration(testDatabase.defaultCollection)!!.channels + ) + } + + // Create a collection style config from one built with the legacy call + @Test + fun testReplicatorConfigFromLegacy() { + val config1 = ReplicatorConfigurationFactory.create(testDatabase, testEndpoint, channels = listOf("boop")) + val config2 = config1.newConfig(continuous = true) + Assert.assertEquals(testDatabase, config2.database) + Assert.assertEquals(testEndpoint, config2.target) + val colls = config2.collections + Assert.assertEquals(1, colls.size) + val defaultCollection = testDatabase.defaultCollection + Assert.assertTrue(colls.contains(defaultCollection)) + Assert.assertEquals(listOf("boop"), config2.getCollectionConfiguration(defaultCollection)!!.channels) + } +} diff --git a/common/test/kotlin/com/couchbase/lite/FlowTest.kt b/common/test/kotlin/com/couchbase/lite/FlowTest.kt index 26f2fac36..7ce36e2ac 100644 --- a/common/test/kotlin/com/couchbase/lite/FlowTest.kt +++ b/common/test/kotlin/com/couchbase/lite/FlowTest.kt @@ -30,6 +30,56 @@ import java.util.concurrent.TimeUnit @Suppress("BlockingMethodInNonBlockingContext") class FlowTest : BaseReplicatorTest() { + @Suppress("DEPRECATION") + @Test + fun testDatabaseChangeFlow() { + val docIds = mutableListOf() + + runBlocking { + val latch = CountDownLatch(1) + + val collector = launch(Dispatchers.Default) { + testDatabase.databaseChangeFlow(testSerialExecutor) + .map { + Assert.assertEquals("change on wrong db", testDatabase, it.database) + it.documentIDs + } + .onEach { ids -> + docIds.addAll(ids) + if (docIds.size >= 10) { + latch.countDown() + } + } + .catch { + latch.countDown() + throw it + } + .collect() + } + + launch(Dispatchers.Default) { + // Hate this: wait until the collector starts + delay(20L) + + // make 10 db changes + for (i in 0..9) { + val doc = MutableDocument("doc-${i}") + doc.setValue("type", "demo") + saveDocInCollection(doc, testDatabase.defaultCollection) + } + } + + Assert.assertTrue("Timeout", latch.await(1, TimeUnit.SECONDS)) + collector.cancel() + } + + Assert.assertEquals(10, docIds.size) + for (i in 0..9) { + val id = "doc-${i}" + Assert.assertTrue("missing ${id}", docIds.contains(id)) + } + } + @Test fun testDocumentChangeFlowOnSave() { val changes = mutableListOf()