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

com.goeuro.sync4j.sync.Synchronizer Maven / Gradle / Ivy

package com.goeuro.sync4j.sync;

import com.goeuro.sync4j.fs.*;
import com.goeuro.sync4j.fs.Detail.Kind;
import com.goeuro.sync4j.fs.FileSystem.WriteListener;
import com.goeuro.sync4j.sync.ElementFilter.Action;
import com.goeuro.sync4j.sync.Lock.LockResult;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.math.BigInteger;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.logging.Logger;
import java.util.stream.Stream;

import static com.goeuro.sync4j.fs.Detail.Kind.directory;
import static com.goeuro.sync4j.fs.Detail.Kind.file;
import static com.goeuro.sync4j.fs.Paths.hasSameContent;
import static com.goeuro.sync4j.sync.ElementFilter.Action.*;
import static com.goeuro.sync4j.sync.Lock.lock;
import static com.goeuro.sync4j.sync.SynchronizeStep.step;
import static com.goeuro.sync4j.sync.Synchronizer.Result.*;
import static com.goeuro.sync4j.utils.IOStreams.closeQuietly;
import static java.util.Optional.empty;

@Immutable
public class Synchronizer {

    @Nonnull
    private static final Logger LOG = Logger.getLogger(Synchronizer.class.getName());
    @Nonnull
    protected static final ProgressListener DEFAULT_LISTENER = new ProgressListener() {
    };

    @Nonnull
    public static Builder synchronizer() {
        return new Builder();
    }

    @Nonnull
    private final ProgressListener progressListener;

    protected Synchronizer(
        @Nonnull ProgressListener progressListener
    ) {
        this.progressListener = progressListener;
    }

    public  void execute(@Nonnull SynchronizeTask task) {
        final AtomicReference result = new AtomicReference<>(failed);
        final SynchronizeStep step = step(task).build();
        final Optional> writeLock = createWriteLockInstanceIfRequired(task);
        if (!writeLock.map(instance -> tryLock(step, instance)).orElse(true)) {
            result.set(rejected);
            return;
        }
        writeLock.ifPresent(candidate -> progressListener().onAfterWriteLocked(step, candidate));
        try {
            final Optional> readLock = createReadLockInstanceIfRequired(task);
            if (!readLock.map(instance -> tryLock(step, instance)).orElse(true)) {
                result.set(rejected);
                return;
            }
            readLock.ifPresent(candidate -> progressListener().onAfterReadLocked(step, candidate));
            try {
                execute(step, true);
                result.set(success);
            } finally {
                try {
                    readLock.ifPresent(candidate -> progressListener().onBeforeReadUnlock(step, candidate, result.get()));
                } finally {
                    closeQuietly(readLock);
                }
            }
        } finally {
            try {
                writeLock.ifPresent(candidate -> progressListener().onBeforeWriteUnlock(step, candidate, result.get()));
            } finally {
                closeQuietly(writeLock);
            }
        }
    }

    @Nonnull
    protected  Optional> createReadLockInstanceIfRequired(@Nonnull SynchronizeTask task) {
        return task.readLockFilename().map(lockFilename -> {
            final Path lockFile = task.retriablePathOperations().resolve(task.from(), lockFilename);
            return lock(lockFile)
                .whichRefreshesEvery(task.readLockRefreshEvery())
                .withTimeoutAfter(task.readLockTimeout())
                .withRetryEvery(task.readLockRetryEvery())
                .withOwner(task.lockOwner().orElse(null))
                .build();
        });
    }

    @Nonnull
    protected  Optional> createWriteLockInstanceIfRequired(@Nonnull SynchronizeTask task) {
        return task.writeLockFilename().map(lockFilename -> {
            final Path lockFile = task.retriablePathOperations().resolve(task.to(), lockFilename);
            return lock(lockFile)
                .whichRefreshesEvery(task.writeLockRefreshEvery())
                .withTimeoutAfter(task.writeLockTimeout())
                .withRetryEvery(task.writeLockRetryEvery())
                .withOwner(task.lockOwner().orElse(null))
                .build();
        });
    }

    protected  boolean tryLock(@Nonnull SynchronizeStep step, @Nonnull Lock lock) {
        final LockResult result = lock.tryLock(
            step.task().writeLockAcquireTimeout().orElse(null),
            (l, elapsed, timeout) -> progressListener().nextAcquireLockRetryAllowed(step, elapsed, timeout)
        );
        progressListener().onAfterWriteLockAcquireAttempt(step, result);
        return result.success();
    }

    @Nonnull
    protected   Optional> detailsOf(@Nonnull SynchronizeStep step, @Nonnull Path path) {
        return step.task().retriablePathOperations().detailsOf(path);
    }

    protected  void execute(@Nonnull SynchronizeStep step, boolean isRootElement) {
        detailsOf(step, step.from()).ifPresent(candidate -> {
            if (shouldFromBeRespected(step) != include) {
                return;
            }
            if (candidate.kind() == directory) {
                executeForDirectory(step);
            } else {
                if (isRootElement) {
                    if (detailsOf(step, step.to())
                        .flatMap(to -> hasSameContent(candidate, to))
                        .orElse(false)) {
                        return;
                    }
                }
                executeForFile(step, (File) candidate);
            }
        });
    }

    protected  void executeForDirectory(@Nonnull SynchronizeStep step) {
        final Map, Detail> notHandledToFiles = listOf(step, step.to());
        createToDirectory(step);

        try (final Stream> list = step.task().retriablePathOperations().list(step.from())) {
            list
                .forEach(subFromDetails -> executeForSpecificPathWhichShouldResolveTo(step.forActualFrom(subFromDetails), notHandledToFiles));
        }

        cleanOrphanedPathsOnRemote(step, notHandledToFiles);
    }

    protected  void executeForFile(@Nonnull SynchronizeStep step, @Nonnull File file) {
        final boolean wantTransferProgress = progressListener().wantTransferProgress(step.to().loose());
        try {
            step.task().retriablePathOperations().copy(step.from(), step.to(), new WriteListener() {
                @Override
                public void onTransferProgress(@Nonnull LoosePath loosePath, @Nonnegative @Nonnull BigInteger transferred) {
                    if (wantTransferProgress) {
                        progressListener().onTransferProgress(step, file.size(), transferred);
                    }
                }

                @Override
                public void onTransferDone(@Nonnull LoosePath loosePath, @Nonnegative @Nonnull BigInteger total) {
                    progressListener().onTransferDone(step, total);
                }
            }).orElseThrow(() -> new IllegalStateException("Source file '" + step.from() + "' does not longer exists but just a second before?"));
        } catch (final IOException e) {
            throw new UncheckedIOException("Could not copy '" + step.from() + "' -> '" + step.to() + "'.", e);
        }
    }

    protected  void executeForSpecificPathWhichShouldResolveTo(@Nonnull SynchronizeStep step, @Nonnull Map, Detail> notHandledToFiles) {
        kindOfFrom(step);
        final String relativePath = step.task().retriablePathOperations().relativize(step.task().from(), step.from());
        final Path relativeToFromTo = step.task().retriablePathOperations().resolve(step.task().to(), relativePath);
        final SynchronizeStep newTask = step.forActualTo(relativeToFromTo);
        executeSpecific(newTask, notHandledToFiles);
    }

    protected  void executeSpecific(@Nonnull SynchronizeStep step, @Nonnull Map, Detail> notHandledToFiles) {
        final Kind kind = kindOfFrom(step);
        final Action action = shouldFromBeRespected(step);
        if (action == ignore) {
            notHandledToFiles.remove(step.to());
            return;
        }
        if (action == exclude) {
            return;
        }
        final Path existingToDetail = notHandledToFiles.remove(step.to());
        if (existingToDetail == null) {
            executeForAbsent(step);
        } else {
            if (hasSameContent(existingToDetail, step.from()).orElse(false)) {
                executeForAlreadyUpToDate(step);
            } else if (kind == directory) {
                execute(step, false);
            } else {
                executeForExistingThatNeedsUpdate(step);
            }
        }
    }

    protected  void executeForAbsent(@Nonnull SynchronizeStep step) {
        if (kindOfFrom(step) == file) {
            progressListener().onCreatingNewFile(step);
        }
        execute(step, false);
    }

    protected  void executeForAlreadyUpToDate(@Nonnull SynchronizeStep step) {
        progressListener().onSkippedBecauseAlreadyUpToDate(step);
    }

    protected  void executeForExistingThatNeedsUpdate(@Nonnull SynchronizeStep step) {
        progressListener().onExistingWillBeUpdated(step);
        execute(step, false);
    }

    @Nonnull
    protected  Kind kindOfFrom(@Nonnull SynchronizeStep step) {
        final Path from = step.from();
        if (from instanceof Detail) {
            return ((Detail) from).kind();
        }
        throw new IllegalArgumentException("The to part of a step provided to this method should be detailed but got: " + step.getClass());
    }

    protected  void createToDirectory(@Nonnull SynchronizeStep step) {
        final Path target = step.to();
        if (!detailsOf(step, target).isPresent()) {
            progressListener().onCreatingNewDirectory(step);
            try {
                Paths.createDirectory(target);
            } catch (final IOException e) {
                throw new UncheckedIOException("Could not create directory '" + target + "'.", e);
            }
        }
    }

    @Nonnull
    protected  Action shouldFromBeRespected(@Nonnull SynchronizeStep step) {
        return shouldBeRespected(step, SynchronizeTask::from, SynchronizeStep::from);
    }

    @Nonnull
    protected  Action shouldToBeRespected(@Nonnull SynchronizeStep step) {
        return shouldBeRespected(step, SynchronizeTask::to, SynchronizeStep::to);
    }

    @Nonnull
    protected  Action shouldBeRespected(
        @Nonnull SynchronizeStep step,
        @Nonnull Function, Path> resolveBase,
        @Nonnull Function, Path> resolveActual
    ) {
        final SynchronizeTask task = step.task();
        final Path base = resolveBase.apply(task);
        final Path actual = resolveActual.apply(step);
        final String relativized = step.task().retriablePathOperations().relativize(base, actual);
        if (relativized.isEmpty()) {
            return include;
        }
        final Path resolved = step.task().retriablePathOperations().resolve(base, relativized);
        final Action action = task.filter().handle(resolved, relativized);
        if (action == exclude) {
            progressListener().onIgnoredBecauseNotIncluded(step);
        }
        return action;
    }

    protected  void cleanOrphanedPathsOnRemote(@Nonnull SynchronizeStep step, @Nonnull Map, Detail> notHandledToFiles) {
        cleanOrphanedPathsOnRemote(step, notHandledToFiles.values());
    }

    protected  void cleanOrphanedPathsOnRemote(@Nonnull SynchronizeStep step, @Nonnull Collection> notHandledToFiles) {
        notHandledToFiles.forEach(path -> step.task().retriablePathOperations().delete(path, candidate -> {
            final SynchronizeStep newStep = step.forActualTo(candidate);
            if (shouldToBeRespected(newStep) == ignore) {
                return false;
            }
            progressListener().onDeletingOldOne(newStep);
            return true;
        }));
    }

    @Nonnull
    protected  Map, Detail> listOf(@Nonnull SynchronizeStep step, @Nonnull Path path) {
        final Map, Detail> results = new TreeMap<>();
        try (final Stream> paths = step.task().retriablePathOperations().list(path)) {
            paths
                .forEach(detail -> results.put(detail, detail));
        }
        return results;
    }

    @Nonnull
    protected ProgressListener progressListener() {
        return progressListener;
    }

    public static class Builder {

        @Nonnull
        private Optional progressListener = empty();

        protected Builder() {}

        @Nonnull
        public Builder withProgressListener(@Nonnull ProgressListener progressListener) {
            this.progressListener = Optional.of(progressListener);
            return this;
        }

        @Nonnull
        public Synchronizer build() {
            return new Synchronizer(
                progressListener.orElse(DEFAULT_LISTENER)
            );
        }
    }

    public interface ProgressListener {

        default  boolean nextAcquireLockRetryAllowed(@Nonnull SynchronizeStep step, @Nonnull Duration elapsed, @Nonnull Duration timeout) {
            LOG.info(() -> "Could not acquire lock since " + elapsed + " because blocked by other instance. Retry until " + timeout + ".");
            return true;
        }

        default  void onAfterWriteLockAcquireAttempt(@Nonnull SynchronizeStep step, @Nonnull LockResult lockResult) {
            if (!lockResult.success()) {
                throw new IllegalStateException("Could not create write lock. Got: " + lockResult);
            }
        }

        default  void onAfterReadLocked(@Nonnull SynchronizeStep step, @Nonnull Lock lock) {
        }

        default  void onBeforeReadUnlock(@Nonnull SynchronizeStep step, @Nonnull Lock lock, @Nonnull Result result) {
        }

        default  void onAfterWriteLocked(@Nonnull SynchronizeStep step, @Nonnull Lock lock) {
        }

        default  void onBeforeWriteUnlock(@Nonnull SynchronizeStep step, @Nonnull Lock lock, @Nonnull Result result) {
        }

        default  void onIgnoredBecauseNotIncluded(@Nonnull SynchronizeStep step) {
        }

        default  void onSkippedBecauseAlreadyUpToDate(@Nonnull SynchronizeStep step) {
        }

        default  void onExistingWillBeUpdated(@Nonnull SynchronizeStep step) {
        }

        default  void onCreatingNewDirectory(@Nonnull SynchronizeStep step) {
        }

        default  void onCreatingNewFile(@Nonnull SynchronizeStep step) {
        }

        default  void onDeletingOldOne(@Nonnull SynchronizeStep step) {
        }

        default  void onTransferProgress(@Nonnull SynchronizeStep step, @Nonnegative @Nonnull BigInteger total, @Nonnegative @Nonnull BigInteger transferred) {
        }

        default  void onTransferDone(@Nonnull SynchronizeStep step, @Nonnegative @Nonnull BigInteger total) {
        }

        default boolean wantTransferProgress(@Nonnull LoosePath target) {
            return false;
        }
    }

    public enum Result {
        success,
        failed,
        rejected
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy