All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.couchbase.lite.AbstractReplicator Maven / Gradle / Ivy

//
// Copyright (c) 2020, 2017 Couchbase, Inc All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package com.couchbase.lite;

import android.support.annotation.GuardedBy;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.net.URI;
import java.security.cert.Certificate;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import com.couchbase.lite.internal.CBLStatus;
import com.couchbase.lite.internal.CouchbaseLiteInternal;
import com.couchbase.lite.internal.ExecutionService;
import com.couchbase.lite.internal.SocketFactory;
import com.couchbase.lite.internal.core.C4Constants;
import com.couchbase.lite.internal.core.C4DocumentEnded;
import com.couchbase.lite.internal.core.C4Error;
import com.couchbase.lite.internal.core.C4ReplicationFilter;
import com.couchbase.lite.internal.core.C4Replicator;
import com.couchbase.lite.internal.core.C4ReplicatorListener;
import com.couchbase.lite.internal.core.C4ReplicatorMode;
import com.couchbase.lite.internal.core.C4ReplicatorStatus;
import com.couchbase.lite.internal.core.C4Socket;
import com.couchbase.lite.internal.core.InternalReplicator;
import com.couchbase.lite.internal.fleece.FLDict;
import com.couchbase.lite.internal.fleece.FLEncoder;
import com.couchbase.lite.internal.replicator.CBLCookieStore;
import com.couchbase.lite.internal.support.Log;
import com.couchbase.lite.internal.utils.ClassUtils;
import com.couchbase.lite.internal.utils.Fn;
import com.couchbase.lite.internal.utils.Preconditions;
import com.couchbase.lite.internal.utils.StringUtils;


/**
 * A replicator for replicating document changes between a local database and a target database.
 * The replicator can be bidirectional or either push or pull. The replicator can also be one-shot
 * or continuous. The replicator runs asynchronously, so observe the status to
 * be notified of progress.
 */
@SuppressWarnings({"PMD.GodClass", "PMD.TooManyFields", "PMD.CyclomaticComplexity"})
public abstract class AbstractReplicator extends InternalReplicator {
    private static final LogDomain DOMAIN = LogDomain.REPLICATOR;

    /**
     * Activity level of a replicator.
     */
    public enum ActivityLevel {
        /**
         * The replication is finished or hit a fatal error.
         */
        STOPPED,
        /**
         * The replicator is offline because the remote host is unreachable.
         */
        OFFLINE,
        /**
         * The replicator is connecting to the remote host.
         */
        CONNECTING,
        /**
         * The replication is inactive; either waiting for changes or offline
         * as the remote host is unreachable.
         */
        IDLE,
        /**
         * The replication is actively transferring data.
         */
        BUSY
    }


    /**
     * Progress of a replicator. If `total` is zero, the progress is indeterminate; otherwise,
     * dividing the two will produce a fraction that can be used to draw a progress bar.
     */
    public static final class Progress {
        //---------------------------------------------
        // member variables
        //---------------------------------------------

        // The number of completed changes processed.
        private final long completed;

        // The total number of changes to be processed.
        private final long total;

        //---------------------------------------------
        // Constructors
        //---------------------------------------------

        private Progress(long completed, long total) {
            this.completed = completed;
            this.total = total;
        }

        //---------------------------------------------
        // API - public methods
        //---------------------------------------------

        /**
         * The number of completed changes processed.
         */
        public long getCompleted() { return completed; }

        /**
         * The total number of changes to be processed.
         */
        public long getTotal() { return total; }

        @NonNull
        @Override
        public String toString() { return "Progress{" + "completed=" + completed + ", total=" + total + '}'; }

        Progress copy() { return new Progress(completed, total); }
    }


    /**
     * Combined activity level and progress of a replicator.
     */
    public static final class Status {
        //---------------------------------------------
        // member variables
        //---------------------------------------------
        @NonNull
        private final ActivityLevel activityLevel;
        @NonNull
        private final Progress progress;
        @Nullable
        private final CouchbaseLiteException error;

        //---------------------------------------------
        // Constructors
        //---------------------------------------------

        // Note: c4Status.level is current matched with CBLReplicatorActivityLevel:
        public Status(@NonNull C4ReplicatorStatus c4Status) {
            this(
                getActivityLevelFromC4(c4Status.getActivityLevel()),
                new Progress((int) c4Status.getProgressUnitsCompleted(), (int) c4Status.getProgressUnitsTotal()),
                (c4Status.getErrorCode() == 0) ? null : CBLStatus.convertC4Error(c4Status.getC4Error()));
        }

        private Status(
            @NonNull ActivityLevel activityLevel,
            @NonNull Progress progress,
            @Nullable CouchbaseLiteException error) {
            this.activityLevel = activityLevel;
            this.progress = progress;
            this.error = error;
        }

        //---------------------------------------------
        // API - public methods
        //---------------------------------------------

        /**
         * The current activity level.
         */
        @NonNull
        public ActivityLevel getActivityLevel() { return activityLevel; }

        /**
         * The current progress of the replicator.
         */
        @NonNull
        public Replicator.Progress getProgress() { return progress; }

        @Nullable
        public CouchbaseLiteException getError() { return error; }

        @NonNull
        @Override
        public String toString() {
            return "Status{" + "activityLevel=" + activityLevel + ", progress=" + progress + ", error=" + error + '}';
        }

        Status copy() { return new Status(activityLevel, progress.copy(), error); }
    }


    /**
     * An enum representing level of opt in on progress of replication
     * OVERALL: No additional replication progress callback
     * PER_DOCUMENT: >=1 Every document replication ended callback
     * PER_ATTACHMENT: >=2 Every blob replication progress callback
     */
    enum ReplicatorProgressLevel {
        OVERALL(0),
        PER_DOCUMENT(1),
        PER_ATTACHMENT(2);

        private final int value;

        ReplicatorProgressLevel(int value) { this.value = value; }
    }


    // just queue everything up for in-order processing.
    @SuppressFBWarnings("NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE")
    final class ReplicatorListener implements C4ReplicatorListener {
        @Override
        public void statusChanged(
            @Nullable C4Replicator repl,
            @Nullable C4ReplicatorStatus status,
            @Nullable Object context) {
            Log.i(DOMAIN, "C4ReplicatorListener.statusChanged, context: %s, status: %s", context, status);

            if (context == null) {
                Log.w(DOMAIN, "C4ReplicatorListener.statusChanged, context is null!");
                return;
            }

            if (status == null) {
                Log.w(DOMAIN, "C4ReplicatorListener.statusChanged, status is null!");
                return;
            }

            final AbstractReplicator replicator = (AbstractReplicator) context;
            // this guarantees that repl != null
            if (!replicator.isSameReplicator(repl)) { return; }

            dispatcher.execute(() -> replicator.c4StatusChanged(status));
        }

        @Override
        public void documentEnded(
            @NonNull C4Replicator repl,
            boolean pushing,
            @Nullable C4DocumentEnded[] documents,
            @Nullable Object context) {
            Log.i(DOMAIN, "C4ReplicatorListener.documentEnded, context: %s, pushing: %s", context, pushing);

            if (context == null) {
                Log.w(DOMAIN, "C4ReplicatorListener.documentEnded, context is null!");
                return;
            }

            final AbstractReplicator replicator = (AbstractReplicator) context;
            if (!replicator.isSameReplicator(repl)) { return; } // this handles repl == null

            if (documents == null) {
                Log.w(DOMAIN, "C4ReplicatorListener.documentEnded, documents is null!");
                return;
            }

            dispatcher.execute(() -> replicator.documentEnded(pushing, documents));
        }
    }

    private static final Map ACTIVITY_LEVEL_FROM_C4;
    static {
        final Map m = new HashMap<>();
        m.put(C4ReplicatorStatus.ActivityLevel.STOPPED, ActivityLevel.STOPPED);
        m.put(C4ReplicatorStatus.ActivityLevel.OFFLINE, ActivityLevel.OFFLINE);
        m.put(C4ReplicatorStatus.ActivityLevel.CONNECTING, ActivityLevel.CONNECTING);
        m.put(C4ReplicatorStatus.ActivityLevel.IDLE, ActivityLevel.IDLE);
        m.put(C4ReplicatorStatus.ActivityLevel.BUSY, ActivityLevel.BUSY);
        ACTIVITY_LEVEL_FROM_C4 = Collections.unmodifiableMap(m);
    }
    @NonNull
    private static ActivityLevel getActivityLevelFromC4(int c4ActivityLevel) {
        final ActivityLevel level = ACTIVITY_LEVEL_FROM_C4.get(c4ActivityLevel);
        if (level == null) { throw new IllegalStateException("Unrecognized activity level: " + c4ActivityLevel); }
        return level;
    }


    ////////////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////  R E P L I C A T O R   ////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////////////

    //---------------------------------------------
    // member variables
    //---------------------------------------------
    @NonNull
    final ReplicatorConfiguration config;

    private final Executor dispatcher = CouchbaseLiteInternal.getExecutionService().getSerialExecutor();

    @GuardedBy("lock")
    private final Set changeListenerTokens = new HashSet<>();
    @GuardedBy("lock")
    private final Set docEndedListenerTokens = new HashSet<>();

    @NonNull
    private final Set> pendingResolutions = new HashSet<>();
    @NonNull
    private final Deque pendingStatusNotifications = new LinkedList<>();
    @NonNull
    private final C4ReplicatorListener c4ReplListener = new ReplicatorListener();
    @NonNull
    private final SocketFactory socketFactory;

    @GuardedBy("lock")
    @NonNull
    private Status status = new Status(ActivityLevel.STOPPED, new Progress(0, 0), null);
    @GuardedBy("lock")
    @NonNull
    private ReplicatorProgressLevel progressLevel = ReplicatorProgressLevel.OVERALL;

    @GuardedBy("lock")
    private C4ReplicationFilter c4ReplPushFilter;
    @GuardedBy("lock")
    private C4ReplicationFilter c4ReplPullFilter;

    @GuardedBy("lock")
    private CouchbaseLiteException lastError;

    // Reset the replicator checkpoint.
    private volatile boolean resetCheckpoint;

    private volatile String desc;

    // Server certificates received from the server during the TLS handshake
    private final AtomicReference> serverCertificates = new AtomicReference<>();

    /**
     * Initializes a replicator with the given configuration.
     *
     * @param config replicator configuration
     */
    protected AbstractReplicator(@NonNull ReplicatorConfiguration config) {
        Preconditions.assertNotNull(config, "config");
        this.config = config.readonlyCopy();
        this.socketFactory = new SocketFactory(config, getCookieStore(), this::setServerCertificates);
    }

    /**
     * Start the replicator.
     * This method honors the flag set by the deprecated method resetCheckpoint().
     *
     * @deprecated Use start(boolean resetCheckpoint) instead.
     */
    @Deprecated
    public void start() { start(resetCheckpoint); }

    /**
     * Start the replicator.
     * This method does not wait for the replicator to start.
     * The replicator runs asynchronously and reports its progress
     * through replicator change notifications.
     */
    public void start(boolean resetCheckpoint) {
        Log.i(DOMAIN, "Replicator is starting");

        this.resetCheckpoint = false; // reset the (deprecated) flag

        getDatabase().addActiveReplicator(this);

        final C4Replicator repl = getOrCreateC4Replicator();
        synchronized (lock) {
            repl.start(resetCheckpoint);

            C4ReplicatorStatus status = repl.getStatus();
            if (status == null) {
                status = new C4ReplicatorStatus(
                    C4ReplicatorStatus.ActivityLevel.STOPPED,
                    C4Constants.ErrorDomain.LITE_CORE,
                    C4Constants.LiteCoreError.UNEXPECTED_ERROR);
            }

            status = updateStatus(status);

            c4ReplListener.statusChanged(repl, status, this);
        }
    }

    /**
     * Stop a running replicator.
     * This method does not wait for the replicator to stop.
     * When it does actually stop it will a new state, STOPPED, to change listeners.
     */
    public void stop() {
        final C4Replicator c4repl = getC4Replicator();
        Log.i(DOMAIN, "%s: Replicator is stopping (%s)", this, c4repl);
        if (c4repl == null) { return; }
        c4repl.stop();
    }

    /**
     * The replicator's configuration.
     *
     * @return this replicator's configuration
     */
    @NonNull
    public ReplicatorConfiguration getConfig() { return config.readonlyCopy(); }

    /**
     * The replicator's current status: its activity level and progress. Observable.
     *
     * @return this replicator's status
     */
    @NonNull
    public Status getStatus() {
        synchronized (lock) { return status.copy(); }
    }

    /**
     * The server certificates received from the server during the TLS handshake.
     *
     * @return this replicator's server certificates.
     */
    @Nullable
    public List getServerCertificates() {
        final List serverCerts = serverCertificates.get();
        return ((serverCerts == null) || serverCerts.isEmpty())
            ? null
            : new ArrayList<>(serverCerts);
    }

    /**
     * Get a best effort list of documents still pending replication.
     *
     * @return a set of ids for documents still awaiting replication.
     */
    @NonNull
    public Set getPendingDocumentIds() throws CouchbaseLiteException {
        if (config.getReplicatorType().equals(ReplicatorConfiguration.ReplicatorType.PULL)) {
            throw new CouchbaseLiteException(
                "PullOnlyPendingDocIDs",
                CBLError.Domain.CBLITE,
                CBLError.Code.UNSUPPORTED);
        }

        final Set pending;
        try { pending = getOrCreateC4Replicator().getPendingDocIDs(); }
        catch (LiteCoreException e) { throw CBLStatus.convertException(e, "Failed fetching pending documentIds"); }

        if (pending == null) { throw new IllegalStateException("Pending doc ids is unexpectedly null"); }

        return Collections.unmodifiableSet(pending);
    }

    /**
     * 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
     */
    public boolean isDocumentPending(@NonNull String docId) throws CouchbaseLiteException {
        Preconditions.assertNotNull(docId, "document ID");

        if (config.getReplicatorType().equals(ReplicatorConfiguration.ReplicatorType.PULL)) {
            throw new CouchbaseLiteException(
                "PullOnlyPendingDocIDs",
                CBLError.Domain.CBLITE,
                CBLError.Code.UNSUPPORTED);
        }

        try { return getOrCreateC4Replicator().isDocumentPending(docId); }
        catch (LiteCoreException e) { throw CBLStatus.convertException(e, "Failed getting document pending status"); }
    }

    /**
     * Adds a change listener for the changes in the replication status and progress.
     * 

* The changes will be delivered on the UI thread for the Android platform * On other Java platforms, the callback will occur on an arbitrary thread. *

* When developing a Java Desktop application using Swing or JavaFX that needs to update the UI after * receiving the changes, make sure to schedule the UI update on the UI thread by using * SwingUtilities.invokeLater(Runnable) or Platform.runLater(Runnable) respectively. * * @param listener callback */ @NonNull public ListenerToken addChangeListener(@NonNull ReplicatorChangeListener listener) { Preconditions.assertNotNull(listener, "listener"); return addChangeListener(null, listener); } /** * Adds a change listener for the changes in the replication status and progress 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 on Android platform and on an arbitrary thread on other Java platform. * * @param executor executor on which events will be delivered * @param listener callback */ @NonNull public ListenerToken addChangeListener(Executor executor, @NonNull ReplicatorChangeListener listener) { Preconditions.assertNotNull(listener, "listener"); synchronized (lock) { final ReplicatorChangeListenerToken token = new ReplicatorChangeListenerToken(executor, listener); changeListenerTokens.add(token); return token; } } /** * Remove the given ReplicatorChangeListener or DocumentReplicationListener from the this replicator. * * @param token returned by a previous call to addChangeListener or addDocumentListener. */ public void removeChangeListener(@NonNull ListenerToken token) { Preconditions.assertNotNull(token, "token"); synchronized (lock) { if (token instanceof ReplicatorChangeListenerToken) { changeListenerTokens.remove(token); return; } if (token instanceof DocumentReplicationListenerToken) { docEndedListenerTokens.remove(token); if (docEndedListenerTokens.isEmpty()) { progressLevel = ReplicatorProgressLevel.OVERALL; } return; } throw new IllegalArgumentException("unexpected token: " + token); } } /** * Adds a listener for receiving the replication status of the specified document. The status 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 status, make sure to schedule the UI update on the UI thread by using * SwingUtilities.invokeLater(Runnable) or Platform.runLater(Runnable) respectively. * * @param listener callback * @return A ListenerToken that can be used to remove the handler in the future. */ @NonNull public ListenerToken addDocumentReplicationListener(@NonNull DocumentReplicationListener listener) { Preconditions.assertNotNull(listener, "listener"); return addDocumentReplicationListener(null, listener); } /** * Adds a listener for receiving the replication status of the specified document with an executor on which * the status will be posted to the listener. If the executor is not specified, the status will be delivered * on the UI thread for the Android platform and on an arbitrary thread for the Java platform. * * @param executor executor on which events will be delivered * @param listener callback */ @NonNull public ListenerToken addDocumentReplicationListener( @Nullable Executor executor, @NonNull DocumentReplicationListener listener) { Preconditions.assertNotNull(listener, "listener"); synchronized (lock) { progressLevel = ReplicatorProgressLevel.PER_DOCUMENT; final DocumentReplicationListenerToken token = new DocumentReplicationListenerToken(executor, listener); docEndedListenerTokens.add(token); return token; } } /** * This method, exactly, sets a flag that will be used on the next call to * the deprecated method start() * * @throws IllegalStateException unless the Replicator is STOPPED. * @deprecated Use start(boolean resetCheckpoint) instead. */ @Deprecated public void resetCheckpoint() { if (!getState().equals(ActivityLevel.STOPPED)) { throw new IllegalStateException(Log.lookupStandardMessage("ReplicatorNotStopped")); } resetCheckpoint = true; } @NonNull @Override public String toString() { if (desc == null) { desc = description(); } return desc; } //--------------------------------------------- // Protected methods //--------------------------------------------- @GuardedBy("lock") protected abstract C4Replicator createReplicatorForTarget(Endpoint target) throws LiteCoreException; protected abstract void handleOffline(ActivityLevel prevState, boolean nowOnline); @SuppressWarnings("NoFinalizer") @Override protected void finalize() throws Throwable { try { final C4Replicator c4Repl = getC4Replicator(); if (c4Repl == null) { return; } c4Repl.close(); } finally { super.finalize(); } } /** * Create and return a c4Replicator targeting the passed URI * * @param remoteUri a URI for the replication target * @return the c4Replicator * @throws LiteCoreException on failure to create the replicator */ @GuardedBy("lock") @NonNull protected final C4Replicator getRemoteC4Replicator(@NonNull URI remoteUri) throws LiteCoreException { // Set up the port: core uses 0 for not set final int p = remoteUri.getPort(); final int port = Math.max(0, p); // get db name and path final Deque splitPath = splitPath(remoteUri.getPath()); final String dbName = (splitPath.size() <= 0) ? "" : splitPath.removeLast(); final String path = "/" + StringUtils.join("/", splitPath); final boolean continuous = config.isContinuous(); return getDatabase().createRemoteReplicator( (Replicator) this, remoteUri.getScheme(), remoteUri.getHost(), port, path, dbName, makeMode(config.isPush(), continuous), makeMode(config.isPull(), continuous), getFleeceOptions(), c4ReplListener, c4ReplPushFilter, c4ReplPullFilter, socketFactory, C4Socket.NO_FRAMING); } /** * Create and return a c4Replicator targeting the passed Database * * @param otherDb a local database for the replication target * @return the c4Replicator * @throws LiteCoreException on failure to create the replicator */ @GuardedBy("lock") @NonNull protected final C4Replicator getLocalC4Replicator(@NonNull Database otherDb) throws LiteCoreException { final boolean continuous = config.isContinuous(); return getDatabase().createLocalReplicator( (Replicator) this, otherDb, makeMode(config.isPush(), continuous), makeMode(config.isPull(), continuous), getFleeceOptions(), c4ReplListener, c4ReplPushFilter, c4ReplPullFilter); } /** * Create and return a c4Replicator. * The socket factory is responsible for setting up the target * * @param framing the framing mode (C4Socket.XXX_FRAMING) * @return the c4Replicator * @throws LiteCoreException on failure to create the replicator */ @GuardedBy("lock") @NonNull protected final C4Replicator getMessageC4Replicator(int framing) throws LiteCoreException { final boolean continuous = config.isContinuous(); return getDatabase().createRemoteReplicator( (Replicator) this, C4Replicator.MESSAGE_SCHEME, null, 0, null, null, makeMode(config.isPush(), continuous), makeMode(config.isPull(), continuous), getFleeceOptions(), c4ReplListener, c4ReplPushFilter, c4ReplPullFilter, socketFactory, framing); } //--------------------------------------------- // Package visible methods // // Some of these are package protected only to avoid a synthetic accessor //--------------------------------------------- CouchbaseLiteException getLastError() { return lastError; } @NonNull ActivityLevel getState() { synchronized (lock) { return status.getActivityLevel(); } } void c4StatusChanged(@NonNull C4ReplicatorStatus c4Status) { final ReplicatorChange change; final List tokens; synchronized (lock) { Log.i( DOMAIN, "%s: status changed: (%d, %d) @%s", this, pendingResolutions.size(), pendingStatusNotifications.size(), c4Status); if (config.isContinuous()) { handleOffline( status.getActivityLevel(), c4Status.getActivityLevel() != C4ReplicatorStatus.ActivityLevel.OFFLINE); } if (!pendingResolutions.isEmpty()) { pendingStatusNotifications.add(c4Status); } if (!pendingStatusNotifications.isEmpty()) { return; } // Update my properties: updateStatus(c4Status); // Post notification // Replicator.getStatus() creates a copy of Status. change = new ReplicatorChange((Replicator) this, this.getStatus()); tokens = new ArrayList<>(changeListenerTokens); } if (c4Status.getActivityLevel() == C4ReplicatorStatus.ActivityLevel.STOPPED) { // this will probably make this instance eligible for garbage collection... getDatabase().removeActiveReplicator((Replicator) this); } for (ReplicatorChangeListenerToken token: tokens) { token.notify(change); } } void documentEnded(boolean pushing, C4DocumentEnded... docEnds) { final List unconflictedDocs = new ArrayList<>(); for (C4DocumentEnded docEnd: docEnds) { final String docId = docEnd.getDocID(); final C4Error c4Error = docEnd.getC4Error(); CouchbaseLiteException error = null; if ((c4Error != null) && (c4Error.getCode() != 0)) { if (!pushing && docEnd.isConflicted()) { queueConflictResolution(docId, docEnd.getFlags()); continue; } error = CBLStatus.convertC4Error(c4Error); } unconflictedDocs.add(new ReplicatedDocument(docId, docEnd.getFlags(), error, docEnd.errorIsTransient())); } if (!unconflictedDocs.isEmpty()) { notifyDocumentEnded(pushing, unconflictedDocs); } } // callback from queueConflictResolution void onConflictResolved( Fn.Consumer task, String docId, int flags, CouchbaseLiteException err) { Log.i(DOMAIN, "Conflict resolved: %s", err, docId); List pendingNotifications = null; synchronized (lock) { pendingResolutions.remove(task); // if no more resolutions, deliver any outstanding status notifications if (pendingResolutions.isEmpty()) { pendingNotifications = new ArrayList<>(pendingStatusNotifications); pendingStatusNotifications.clear(); } } notifyDocumentEnded(false, Arrays.asList(new ReplicatedDocument(docId, flags, err, false))); if ((pendingNotifications != null) && (!pendingNotifications.isEmpty())) { for (C4ReplicatorStatus status: pendingNotifications) { dispatcher.execute(() -> c4StatusChanged(status)); } } } void notifyDocumentEnded(boolean pushing, List docs) { final DocumentReplication update = new DocumentReplication((Replicator) this, pushing, docs); final List tokens; synchronized (lock) { tokens = new ArrayList<>(docEndedListenerTokens); } for (DocumentReplicationListenerToken token: tokens) { token.notify(update); } Log.i(DOMAIN, "notifyDocumentEnded: %s" + update); } //--------------------------------------------- // Private methods //--------------------------------------------- @NonNull private C4Replicator getOrCreateC4Replicator() { // createReplicatorForTarget is going to seize this lock anyway: force in-order seizure synchronized (config.getDatabase().getLock()) { C4Replicator c4Repl = getC4Replicator(); if (c4Repl != null) { c4Repl.setOptions(getFleeceOptions()); return c4Repl; } setupFilters(); try { c4Repl = createReplicatorForTarget(config.getTarget()); setC4Replicator(c4Repl); } catch (LiteCoreException e) { throw new IllegalStateException("Could not create replicator", CBLStatus.convertException(e)); } return c4Repl; } } private boolean isSameReplicator(C4Replicator repl) { return repl == getC4Replicator(); } @GuardedBy("lock") private C4ReplicatorStatus updateStatus(@NonNull C4ReplicatorStatus c4Status) { final C4ReplicatorStatus c4ReplStatus = c4Status.copy(); CouchbaseLiteException error = null; if (c4Status.getErrorCode() != 0) { error = CBLStatus.toCouchbaseLiteException( c4Status.getErrorDomain(), c4Status.getErrorCode(), c4Status.getErrorInternalInfo()); lastError = error; } final ActivityLevel level = getActivityLevelFromC4(c4Status.getActivityLevel()); status = new Status( level, new Progress((int) c4Status.getProgressUnitsCompleted(), (int) c4Status.getProgressUnitsTotal()), error); Log.i(DOMAIN, "%s is %s, progress %d/%d, error: %s", this, level.toString(), c4Status.getProgressUnitsCompleted(), c4Status.getProgressUnitsTotal(), error); return c4ReplStatus; } private void queueConflictResolution(@NonNull String docId, int flags) { Log.i(DOMAIN, "%s: pulled conflicting version of '%s'", this, docId); final ExecutionService.CloseableExecutor executor = CouchbaseLiteInternal.getExecutionService().getConcurrentExecutor(); final Database db = getDatabase(); final ConflictResolver resolver = config.getConflictResolver(); final Fn.Consumer task = new Fn.Consumer() { public void accept(CouchbaseLiteException err) { onConflictResolved(this, docId, flags, err); } }; synchronized (lock) { executor.execute(() -> db.resolveReplicationConflict(resolver, docId, task)); pendingResolutions.add(task); } } private byte[] getFleeceOptions() { // Encode the options: final Map options = config.effectiveOptions(); synchronized (lock) { options.put(C4Replicator.REPLICATOR_OPTION_PROGRESS_LEVEL, progressLevel.value); } byte[] optionsFleece = null; if (!options.isEmpty()) { final FLEncoder enc = new FLEncoder(); try { enc.write(options); optionsFleece = enc.finish(); } catch (LiteCoreException e) { Log.e(DOMAIN, "Failed to encode", e); } finally { enc.free(); } } return optionsFleece; } private void setupFilters() { synchronized (lock) { if (config.getPushFilter() != null) { c4ReplPushFilter = (docID, revId, flags, dict, isPush, repl) -> repl.filterDocument(docID, revId, getDocumentFlags(flags), dict, isPush); } if (config.getPullFilter() != null) { c4ReplPullFilter = (docID, revId, flags, dict, isPush, repl) -> repl.filterDocument(docID, revId, getDocumentFlags(flags), dict, isPush); } } } private int makeMode(boolean active, boolean continuous) { final C4ReplicatorMode mode = (!active) ? C4ReplicatorMode.C4_DISABLED : ((continuous) ? C4ReplicatorMode.C4_CONTINUOUS : C4ReplicatorMode.C4_ONE_SHOT); return mode.getVal(); } private EnumSet getDocumentFlags(int flags) { final EnumSet documentFlags = EnumSet.noneOf(DocumentFlag.class); if ((flags & C4Constants.RevisionFlags.DELETED) == C4Constants.RevisionFlags.DELETED) { documentFlags.add(DocumentFlag.DocumentFlagsDeleted); } if ((flags & C4Constants.RevisionFlags.PURGED) == C4Constants.RevisionFlags.PURGED) { documentFlags.add(DocumentFlag.DocumentFlagsAccessRemoved); } return documentFlags; } private boolean filterDocument( String docId, String revId, EnumSet flags, long dict, boolean isPush) { final ReplicationFilter filter = (isPush) ? config.getPushFilter() : config.getPullFilter(); return (filter != null) && filter.filtered(new Document(getDatabase(), docId, revId, new FLDict(dict)), flags); } private Database getDatabase() { return config.getDatabase(); } // Decompose a path into its elements. private Deque splitPath(String fullPath) { final Deque path = new ArrayDeque<>(); for (String element: fullPath.split("/")) { if (element.length() > 0) { path.addLast(element); } } return path; } // CookieStore: @NonNull private CBLCookieStore getCookieStore() { return new CBLCookieStore() { @Override public void setCookie(@NonNull URI uri, @NonNull String header) { getDatabase().setCookie(uri, header); } @Nullable @Override public String getCookies(@NonNull URI uri) { return getDatabase().getCookies(uri); } }; } // Consumer callback to set the server certificates received during the TLS Handshake private void setServerCertificates(List certificates) { serverCertificates.set(certificates); } private String description() { return baseDesc() + "," + getDatabase() + " => " + config.getTarget() + "}"; } @SuppressWarnings("PMD.UnusedPrivateMethod") private String simpleDesc() { return baseDesc() + "}"; } private String baseDesc() { return "Replicator{" + ClassUtils.objId(this) + "(" + (config.isPull() ? "<" : "") + (config.isContinuous() ? "*" : "-") + (config.isPush() ? ">" : "") + ")"; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy