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

com.yahoo.vespa.curator.mock.MockCuratorFramework Maven / Gradle / Ivy

// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.curator.mock;

import com.yahoo.collections.Pair;
import com.yahoo.concurrent.Lock;
import com.yahoo.concurrent.Locks;
import com.yahoo.concurrent.UncheckedTimeoutException;
import com.yahoo.path.Path;
import com.yahoo.vespa.curator.CompletionTimeoutException;
import com.yahoo.vespa.curator.Curator;
import com.yahoo.vespa.curator.mock.MemoryFileSystem.Node;
import com.yahoo.vespa.curator.recipes.CuratorLockException;
import org.apache.curator.CuratorZookeeperClient;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.WatcherRemoveCuratorFramework;
import org.apache.curator.framework.api.ACLBackgroundPathAndBytesable;
import org.apache.curator.framework.api.ACLCreateModeBackgroundPathAndBytesable;
import org.apache.curator.framework.api.ACLCreateModePathAndBytesable;
import org.apache.curator.framework.api.ACLCreateModeStatBackgroundPathAndBytesable;
import org.apache.curator.framework.api.ACLPathAndBytesable;
import org.apache.curator.framework.api.ACLableExistBuilderMain;
import org.apache.curator.framework.api.BackgroundCallback;
import org.apache.curator.framework.api.BackgroundPathAndBytesable;
import org.apache.curator.framework.api.BackgroundPathable;
import org.apache.curator.framework.api.BackgroundVersionable;
import org.apache.curator.framework.api.ChildrenDeletable;
import org.apache.curator.framework.api.CreateBackgroundModeStatACLable;
import org.apache.curator.framework.api.CreateBuilder;
import org.apache.curator.framework.api.CreateBuilder2;
import org.apache.curator.framework.api.CreateBuilderMain;
import org.apache.curator.framework.api.CreateProtectACLCreateModePathAndBytesable;
import org.apache.curator.framework.api.CuratorListener;
import org.apache.curator.framework.api.CuratorWatcher;
import org.apache.curator.framework.api.DeleteBuilder;
import org.apache.curator.framework.api.DeleteBuilderMain;
import org.apache.curator.framework.api.ErrorListenerPathAndBytesable;
import org.apache.curator.framework.api.ErrorListenerPathable;
import org.apache.curator.framework.api.ExistsBuilder;
import org.apache.curator.framework.api.GetACLBuilder;
import org.apache.curator.framework.api.GetChildrenBuilder;
import org.apache.curator.framework.api.GetConfigBuilder;
import org.apache.curator.framework.api.GetDataBuilder;
import org.apache.curator.framework.api.GetDataWatchBackgroundStatable;
import org.apache.curator.framework.api.PathAndBytesable;
import org.apache.curator.framework.api.Pathable;
import org.apache.curator.framework.api.ProtectACLCreateModeStatPathAndBytesable;
import org.apache.curator.framework.api.ReconfigBuilder;
import org.apache.curator.framework.api.RemoveWatchesBuilder;
import org.apache.curator.framework.api.SetACLBuilder;
import org.apache.curator.framework.api.SetDataBackgroundVersionable;
import org.apache.curator.framework.api.SetDataBuilder;
import org.apache.curator.framework.api.SyncBuilder;
import org.apache.curator.framework.api.UnhandledErrorListener;
import org.apache.curator.framework.api.VersionPathAndBytesable;
import org.apache.curator.framework.api.WatchPathable;
import org.apache.curator.framework.api.Watchable;
import org.apache.curator.framework.api.WatchesBuilder;
import org.apache.curator.framework.api.transaction.CuratorMultiTransaction;
import org.apache.curator.framework.api.transaction.CuratorTransaction;
import org.apache.curator.framework.api.transaction.CuratorTransactionBridge;
import org.apache.curator.framework.api.transaction.CuratorTransactionFinal;
import org.apache.curator.framework.api.transaction.CuratorTransactionResult;
import org.apache.curator.framework.api.transaction.TransactionCheckBuilder;
import org.apache.curator.framework.api.transaction.TransactionCreateBuilder;
import org.apache.curator.framework.api.transaction.TransactionCreateBuilder2;
import org.apache.curator.framework.api.transaction.TransactionDeleteBuilder;
import org.apache.curator.framework.api.transaction.TransactionOp;
import org.apache.curator.framework.api.transaction.TransactionSetDataBuilder;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.framework.listen.Listenable;
import org.apache.curator.framework.recipes.atomic.AtomicStats;
import org.apache.curator.framework.recipes.atomic.AtomicValue;
import org.apache.curator.framework.recipes.atomic.DistributedAtomicLong;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.NodeCacheListener;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.curator.framework.recipes.locks.InterProcessLock;
import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex;
import org.apache.curator.framework.schema.SchemaSet;
import org.apache.curator.framework.state.ConnectionStateErrorPolicy;
import org.apache.curator.framework.state.ConnectionStateListener;
import org.apache.curator.retry.RetryForever;
import org.apache.curator.utils.EnsurePath;
import org.apache.curator.utils.ZookeeperCompatibility;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.data.ACL;
import org.apache.zookeeper.data.Stat;
import org.apache.zookeeper.server.quorum.flexible.QuorumVerifier;

import java.nio.file.FileSystem;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * A mock implementation of{@link CuratorFramework} for testing purposes.
 *
 * @author mpolden
 */
public class MockCuratorFramework implements CuratorFramework  {

    private final boolean shouldTimeoutOnEnter;
    private final boolean stableOrdering;
    private final Locks locks = new Locks<>(Long.MAX_VALUE, TimeUnit.DAYS);

    /** The file system used by this mock to store zookeeper files and directories */
    private final MemoryFileSystem fileSystem = new MemoryFileSystem();

    /** Atomic counters. A more accurate mock would store these as files in the file system */
    private final Map atomicCounters = new ConcurrentHashMap<>();

    /** Listeners to changes to a particular path */
    private final ListenerMap listeners = new ListenerMap();

    public final MockListenable connectionStateListeners = new MockListenable<>();

    private CuratorFrameworkState curatorState = CuratorFrameworkState.LATENT;
    private int monotonicallyIncreasingNumber = 0;

    public MockCuratorFramework(boolean stableOrdering, boolean shouldTimeoutOnEnter) {
        this.stableOrdering = stableOrdering;
        this.shouldTimeoutOnEnter = shouldTimeoutOnEnter;
    }

    public Map atomicCounters() {
        return Collections.unmodifiableMap(atomicCounters);
    }

    public FileSystem fileSystem() {
        return fileSystem;
    }

    @Override
    public void start() {
        curatorState = CuratorFrameworkState.STARTED;
    }

    @Override
    public void close() {
        curatorState = CuratorFrameworkState.STOPPED;
    }

    @Override
    public CuratorFrameworkState getState() {
        return curatorState;
    }

    @Override
    @Deprecated
    public boolean isStarted() {
        return curatorState == CuratorFrameworkState.STARTED;
    }

    @Override
    public CreateBuilder create() {
        return new MockCreateBuilder();
    }

    @Override
    public DeleteBuilder delete() {
        return new MockDeleteBuilder();
    }

    @Override
    public ExistsBuilder checkExists() {
        return new MockExistsBuilder();
    }

    @Override
    public GetDataBuilder getData() {
        return new MockGetDataBuilder();
    }

    @Override
    public SetDataBuilder setData() {
        return new MockSetDataBuilder();
    }

    @Override
    public GetChildrenBuilder getChildren() {
        return new MockGetChildrenBuilder();
    }

    @Override
    public GetACLBuilder getACL() {
        throw new UnsupportedOperationException("Not implemented in MockCurator");
    }

    @Override
    public SetACLBuilder setACL() {
        throw new UnsupportedOperationException("Not implemented in MockCurator");
    }


    @Override
    public ReconfigBuilder reconfig() { throw new UnsupportedOperationException("Not implemented in MockCurator"); }

    @Override
    public GetConfigBuilder getConfig() { throw new UnsupportedOperationException("Not implemented in MockCurator"); }

    @Deprecated
    @Override
    public CuratorTransaction inTransaction() {
        return new MockCuratorTransactionFinal();
    }

    @Override
    public CuratorMultiTransaction transaction() { throw new UnsupportedOperationException("Not implemented in MockCurator"); }

    @Override
    public TransactionOp transactionOp() { throw new UnsupportedOperationException("Not implemented in MockCurator"); }

    @Deprecated
    @Override
    public RemoveWatchesBuilder watches() { throw new UnsupportedOperationException("Not implemented in MockCurator"); }

    @Override
    public WatchesBuilder watchers() { throw new UnsupportedOperationException("Not implemented in MockCurator"); }

    @Override
    public WatcherRemoveCuratorFramework newWatcherRemoveCuratorFramework() {
        class MockWatcherRemoveCuratorFramework extends MockCuratorFramework implements WatcherRemoveCuratorFramework {

            public MockWatcherRemoveCuratorFramework(boolean stableOrdering, boolean shouldTimeoutOnEnter) {
                super(stableOrdering, shouldTimeoutOnEnter);
            }

            @Override
            public void removeWatchers() {}

            @Override
            public ZookeeperCompatibility getZookeeperCompatibility() {
                return ZookeeperCompatibility.LATEST;
            }
        }
        return new MockWatcherRemoveCuratorFramework(true, false);
    }

    @Override
    public ConnectionStateErrorPolicy getConnectionStateErrorPolicy() { throw new UnsupportedOperationException("Not implemented in MockCurator"); }

    @Override
    public QuorumVerifier getCurrentConfig() { throw new UnsupportedOperationException("Not implemented in MockCurator"); }

    @Override
    public SchemaSet getSchemaSet() { throw new UnsupportedOperationException("Not implemented in MockCurator"); }

    @Override
    public CompletableFuture postSafeNotify(Object monitorHolder) { throw new UnsupportedOperationException("Not implemented in MockCurator"); }

    @Override
    public CompletableFuture runSafe(Runnable runnable) { throw new UnsupportedOperationException("Not implemented in MockCurator"); }

    @Override
    @Deprecated
    public void sync(String path, Object backgroundContextObject) {
        throw new UnsupportedOperationException("Not implemented in MockCurator");
    }

    @Override
    public void createContainers(String s) {
        throw new UnsupportedOperationException("Not implemented in MockCurator");
    }

    @Override
    public Listenable getConnectionStateListenable() {
        return connectionStateListeners;
    }

    @Override
    public Listenable getCuratorListenable() {
        throw new UnsupportedOperationException("Not implemented in MockCurator");
    }

    @Override
    public Listenable getUnhandledErrorListenable() {
        throw new UnsupportedOperationException("Not implemented in MockCurator");
    }

    @Override
    @Deprecated
    public CuratorFramework nonNamespaceView() {
        throw new UnsupportedOperationException("Not implemented in MockCurator");
    }

    @Override
    public CuratorFramework usingNamespace(String newNamespace) {
        throw new UnsupportedOperationException("Not implemented in MockCurator");
    }

    @Override
    public String getNamespace() {
        throw new UnsupportedOperationException("Not implemented in MockCurator");
    }

    @Override
    public CuratorZookeeperClient getZookeeperClient() {
        throw new UnsupportedOperationException("Not implemented in MockCurator");
    }

    @Override
    public ZookeeperCompatibility getZookeeperCompatibility() {
        return ZookeeperCompatibility.LATEST;
    }

    @Deprecated
    @Override
    public EnsurePath newNamespaceAwareEnsurePath(String path) {
        return new EnsurePath(path);
    }

    @Deprecated
    @Override
    public void clearWatcherReferences(Watcher watcher) {
        throw new UnsupportedOperationException("Not implemented in MockCurator");
    }

    @Override
    public boolean blockUntilConnected(int i, TimeUnit timeUnit) {
        return true;
    }

    @Override
    public void blockUntilConnected() {
    }

    @Override
    public SyncBuilder sync() {
        throw new UnsupportedOperationException("Not implemented in MockCurator");
    }

    // ----- Factory methods for mocks */

    public InterProcessLock createMutex(String path) {
        return new MockLock(path);
    }

    public DistributedAtomicLong createAtomicCounter(String path) {
        return atomicCounters.computeIfAbsent(path, (k) -> new MockAtomicCounter(path));
    }

    public Curator.CompletionWaiter createCompletionWaiter() {
        return new MockCompletionWaiter();
    }

    public Curator.DirectoryCache createDirectoryCache(String path) {
        return new MockDirectoryCache(Path.fromString(path));
    }

    public Curator.FileCache createFileCache(String path) {
        return new MockFileCache(Path.fromString(path));
    }

    // ----- Start of adaptor methods from Curator to the mock file system -----

    /** Creates a node below the given directory root */
    private String createNode(String pathString, byte[] content, boolean createParents, Stat stat, CreateMode createMode, MemoryFileSystem.Node root, Listeners listeners, Long ttl)
            throws KeeperException.NodeExistsException, KeeperException.NoNodeException {
        validatePath(pathString);
        Path path = Path.fromString(pathString);
        if (path.isRoot()) return "/"; // the root already exists
        MemoryFileSystem.Node parent = root.getNode(Paths.get(path.getParentPath().toString()), createParents);
        String name = nodeName(path.getName(), createMode);

        if (parent == null)
            throw new KeeperException.NoNodeException(path.getParentPath().toString());
        if (parent.children().containsKey(path.getName()))
            throw new KeeperException.NodeExistsException(path.toString());

        MemoryFileSystem.Node node = parent.add(name);
        node.setContent(content);
        if (List.of(CreateMode.PERSISTENT_WITH_TTL, CreateMode.PERSISTENT_SEQUENTIAL_WITH_TTL).contains(createMode)
                && ttl != null)
            node.setTtl(ttl);
        String nodePath = "/" + path.getParentPath().toString() + "/" + name;
        listeners.notify(Path.fromString(nodePath), content, PathChildrenCacheEvent.Type.CHILD_ADDED);
        if (stat != null) stat.setVersion(node.version());
        return nodePath;
    }

    /** Deletes a node below the given directory root */
    private void deleteNode(String pathString, boolean deleteChildren, int version, MemoryFileSystem.Node root, Listeners listeners)
            throws KeeperException {
        validatePath(pathString);
        Path path = Path.fromString(pathString);
        MemoryFileSystem.Node parent = root.getNode(Paths.get(path.getParentPath().toString()), false);
        if (parent == null) throw new KeeperException.NoNodeException(path.toString());
        MemoryFileSystem.Node node = parent.children().get(path.getName());
        if (node == null) throw new KeeperException.NoNodeException(path.getName() + " under " + parent);
        if (version != -1 && version != node.version())
            throw new KeeperException.BadVersionException("expected version " + version + ", but was " + node.version());
        if ( ! node.children().isEmpty() && ! deleteChildren)
            throw new KeeperException.NotEmptyException(path.toString());
        parent.remove(path.getName());
        listeners.notify(path, new byte[0], PathChildrenCacheEvent.Type.CHILD_REMOVED);
    }

    /** Returns the data of a node */
    private byte[] getData(String pathString, Stat stat, MemoryFileSystem.Node root) throws KeeperException.NoNodeException {
        validatePath(pathString);
        return getNode(pathString, stat, root).getContent();
    }

    /** sets the data of an existing node */
    private void setData(String pathString, byte[] content, int version, Stat stat, MemoryFileSystem.Node root, Listeners listeners)
            throws KeeperException {
        validatePath(pathString);
        Node node = getNode(pathString, null, root);
        if (version != -1 && version != node.version())
            throw new KeeperException.BadVersionException("expected version " + version + ", but was " + node.version());
        node.setContent(content);
        if (stat != null) stat.setVersion(node.version());
        listeners.notify(Path.fromString(pathString), content, PathChildrenCacheEvent.Type.CHILD_UPDATED);
    }

    private List getChildren(String path, MemoryFileSystem.Node root) throws KeeperException.NoNodeException {
        validatePath(path);
        MemoryFileSystem.Node node = root.getNode(Paths.get(path), false);
        if (node == null) throw new KeeperException.NoNodeException(path);
        List children = new ArrayList<>(node.children().keySet());
        if (! stableOrdering)
            Collections.shuffle(children);
        return children;
    }

    /** Returns a node or throws the appropriate exception if it doesn't exist */
    private MemoryFileSystem.Node getNode(String pathString, Stat stat, MemoryFileSystem.Node root) throws KeeperException.NoNodeException {
        validatePath(pathString);
        Path path = Path.fromString(pathString);
        MemoryFileSystem.Node parent = root.getNode(Paths.get(path.getParentPath().toString()), false);
        if (parent == null) throw new KeeperException.NoNodeException(path.toString());
        MemoryFileSystem.Node node = parent.children().get(path.getName());
        if (node == null) throw new KeeperException.NoNodeException(path.toString());
        if (stat != null) stat.setVersion(node.version());
        return node;
    }

    private String nodeName(String baseName, CreateMode createMode) {
        return switch (createMode) {
            case PERSISTENT, EPHEMERAL, PERSISTENT_WITH_TTL -> baseName;
            case PERSISTENT_SEQUENTIAL, EPHEMERAL_SEQUENTIAL -> baseName + monotonicallyIncreasingNumber++;
            default -> throw new UnsupportedOperationException(createMode + " support not implemented in MockCurator");
        };
    }

    /** Validates a path using the same rules as ZooKeeper */
    public static String validatePath(String path) throws IllegalArgumentException {
        if (path == null) throw new IllegalArgumentException("Path cannot be null");
        if (path.isEmpty()) throw new IllegalArgumentException("Path length must be > 0");
        if (path.charAt(0) != '/') throw new IllegalArgumentException("Path must start with / character");
        if (path.length() == 1) return path; // done checking - it's the root
        if (path.charAt(path.length() - 1) == '/')
            throw new IllegalArgumentException("Path must not end with / character");

        String reason = null;
        char lastc = '/';
        char[] chars = path.toCharArray();
        char c;
        for (int i = 1; i < chars.length; lastc = chars[i], i++) {
            c = chars[i];

            if (c == 0) {
                reason = "null character not allowed @" + i;
                break;
            } else if (c == '/' && lastc == '/') {
                reason = "empty node name specified @" + i;
                break;
            } else if (c == '.' && lastc == '.') {
                if (chars[i-2] == '/' && ((i + 1 == chars.length) || chars[i+1] == '/')) {
                    reason = "relative paths not allowed @" + i;
                    break;
                }
            } else if (c == '.') {
                if (chars[i-1] == '/' && ((i + 1 == chars.length) || chars[i+1] == '/')) {
                    reason = "relative paths not allowed @" + i;
                    break;
                }
            } else if (c > '\u0000' && c < '\u001f' || c > '\u007f' && c < '\u009F'
                       || c > '\ud800' && c < '\uf8ff' || c > '\ufff0' && c < '\uffff') {
                reason = "invalid character @" + i;
                break;
            }
        }

        if (reason != null)
            throw new IllegalArgumentException("Invalid path string \"" + path + "\" caused by " + reason);
        return path;
    }

    /**
     * Invocation of changes to the file system state is abstracted through this to allow transactional
     * changes to notify on commit
     */
    private abstract static class Listeners {

        /** Translating method */
        public final void notify(Path path, byte[] data, PathChildrenCacheEvent.Type type) {
            String pathString = "/" + path.toString(); // this silly path class strips the leading "/" :-/
            PathChildrenCacheEvent event = new PathChildrenCacheEvent(type, new ChildData(pathString, null, data));
            notify(path, event);
        }

        public abstract void notify(Path path, PathChildrenCacheEvent event);

    }

    /** The regular listener implementation which notifies registered file and directory listeners */
    private class ListenerMap extends Listeners {

        private final Map directoryListeners = new ConcurrentHashMap<>();
        private final Map fileListeners = new ConcurrentHashMap<>();

        public void add(Path path, PathChildrenCacheListener listener) {
            directoryListeners.put(path, listener);
        }

        public void add(Path path, NodeCacheListener listener) {
            fileListeners.put(path, listener);
        }

        @Override
        public void notify(Path path, PathChildrenCacheEvent event) {
            try {
                // Snapshot directoryListeners in case notification leads to new directoryListeners added
                Set> directoryListenerSnapshot = new HashSet<>(directoryListeners.entrySet());
                for (Map.Entry listener : directoryListenerSnapshot) {
                    if (path.isChildOf(listener.getKey()))
                        listener.getValue().childEvent(MockCuratorFramework.this, event);
                }

                // Snapshot directoryListeners in case notification leads to new directoryListeners added
                Set> fileListenerSnapshot = new HashSet<>(fileListeners.entrySet());
                for (Map.Entry listener : fileListenerSnapshot) {
                    if (path.equals(listener.getKey()))
                        listener.getValue().nodeChanged();
                }
            }
            catch (Exception e) {
                e.printStackTrace(); // TODO: Remove
                throw new RuntimeException("Exception notifying listeners", e);
            }
        }

    }

    private class MockCompletionWaiter implements Curator.CompletionWaiter {

        @Override
        public void awaitCompletion(Duration timeout) {
            if (shouldTimeoutOnEnter) {
                throw new CompletionTimeoutException("");
            }
        }

        @Override
        public void notifyCompletion() {
        }

    }

    /** A lock which works inside a single vm */
    private class MockLock extends InterProcessSemaphoreMutex {

        public boolean timeoutOnLock = false;
        public boolean throwExceptionOnLock = false;

        private final String path;

        private Lock lock = null;

        public MockLock(String path) {
            super(MockCuratorFramework.this, path);
            this.path = path;
        }

        @Override
        public boolean acquire(long timeout, TimeUnit unit) {
            if (throwExceptionOnLock)
                throw new CuratorLockException("Thrown by mock");
            if (timeoutOnLock) return false;

            try {
                lock = locks.lock(path, timeout, unit);
                return true;
            }
            catch (UncheckedTimeoutException e) {
                return false;
            }
        }

        @Override
        public void acquire() {
            if (throwExceptionOnLock)
                throw new CuratorLockException("Thrown by mock");

            lock = locks.lock(path);
        }

        @Override
        public void release() {
            if (lock != null)
                lock.close();
        }

    }

    private class MockAtomicCounter extends DistributedAtomicLong {

        private boolean initialized = false;
        private MockLongValue value = new MockLongValue(0); // yes, uninitialized returns 0  :-/

        public MockAtomicCounter(String path) {
            super(MockCuratorFramework.this, path, new RetryForever(1_000));
        }

        @Override
        public boolean initialize(Long value) {
            if (initialized) return false;
            this.value = new MockLongValue(value);
            initialized = true;
            return true;
        }

        @Override
        public AtomicValue get() {
            if (value == null) return new MockLongValue(0);
            return value;
        }

        public AtomicValue add(Long delta) {
            return trySet(value.postValue() + delta);
        }

        public AtomicValue subtract(Long delta) {
            return trySet(value.postValue() - delta);
        }

        @Override
        public AtomicValue increment() {
            return trySet(value.postValue() + 1);
        }

        public AtomicValue decrement() {
            return trySet(value.postValue() - 1);
        }

        @Override
        public AtomicValue trySet(Long longval) {
            value = new MockLongValue(longval);
            return value;
        }

        public void forceSet(Long newValue) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        public AtomicValue compareAndSet(Long expectedValue, Long newValue) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

    }

    private static class MockLongValue implements AtomicValue {

        private final AtomicLong value = new AtomicLong();

        public MockLongValue(long value) {
            this.value.set(value);
        }

        @Override
        public boolean succeeded() {
            return true;
        }

        @Override
        public Long preValue() {
            return value.get();
        }

        @Override
        public Long postValue() {
            return value.get();
        }

        @Override
        public AtomicStats getStats() {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

    }

    private class MockDirectoryCache implements Curator.DirectoryCache {

        /** The path this is caching and listening to */
        private final Path path;

        public MockDirectoryCache(Path path) {
            this.path = path;
        }

        @Override
        public void start() {}

        @Override
        public void addListener(PathChildrenCacheListener listener) {
            listeners.add(path, listener);
        }

        @Override
        public List getCurrentData() {
            List childData = new ArrayList<>();
            for (String childName : getChildren(path)) {
                Path childPath = path.append(childName);
                childData.add(new ChildData(childPath.getAbsolute(), null, getData(childPath).get()));
            }
            return childData;
        }

        @Override
        public ChildData getCurrentData(Path fullPath) {
            if (!fullPath.getParentPath().equals(path)) {
                throw new IllegalArgumentException("Path '" + fullPath + "' is not a child path of '" + path + "'");
            }

            return getData(fullPath).map(bytes -> new ChildData(fullPath.getAbsolute(), null, bytes)).orElse(null);
        }

        @Override
        public void close() {}

        private List getChildren(Path path) {
            try {
                return MockCuratorFramework.this.getChildren().forPath(path.getAbsolute());
            } catch (KeeperException.NoNodeException e) {
                return List.of();
            } catch (Exception e) {
                throw new RuntimeException("Could not get children of " + path.getAbsolute(), e);
            }
        }

        private Optional getData(Path path) {
            try {
                return Optional.of(MockCuratorFramework.this.getData().forPath(path.getAbsolute()));
            }
            catch (KeeperException.NoNodeException e) {
                return Optional.empty();
            }
            catch (Exception e) {
                throw new RuntimeException("Could not get data at " + path.getAbsolute(), e);
            }
        }

    }

    private class MockFileCache implements Curator.FileCache {

        /** The path this is caching and listening to */
        private final Path path;

        public MockFileCache(Path path) {
            this.path = path;
        }

        @Override
        public void start() {}

        @Override
        public void addListener(NodeCacheListener listener) {
            listeners.add(path, listener);
        }

        @Override
        public ChildData getCurrentData() {
            MemoryFileSystem.Node node = fileSystem.root().getNode(Paths.get(path.toString()), false);
            if (node == null) return null;
            return new ChildData("/" + path.toString(), null, node.getContent());
        }

        @Override
        public void close() {}

    }


    // ----- The rest of this file is adapting the Curator (non-recipe) API to the  -----
    // ----- file system methods above.                                             -----
    // ----- There's nothing to see unless you are interested in an illustration of -----
    // ----- the folly of fluent API's or, more generally, mankind.                 -----
    private abstract static class MockProtectACLCreateModeStatPathAndBytesable
            implements ProtectACLCreateModeStatPathAndBytesable {

        Stat stat = null;
        public BackgroundPathAndBytesable withACL(List list) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        public BackgroundPathAndBytesable withACL(List list, boolean b) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        public ProtectACLCreateModeStatPathAndBytesable withMode(CreateMode createMode) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ACLCreateModeBackgroundPathAndBytesable withProtection() {
            return null;
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground() {
            return null;
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(Object o) {
            return null;
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(BackgroundCallback backgroundCallback) {
            return null;
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(BackgroundCallback backgroundCallback, Object o) {
            return null;
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(BackgroundCallback backgroundCallback, Executor executor) {
            return null;
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(BackgroundCallback backgroundCallback, Object o, Executor executor) {
            return null;
        }

        @Override
        public ACLBackgroundPathAndBytesable storingStatIn(Stat stat) {
            this.stat = stat;
            return this;
        }

    }

    private class MockCreateBuilder implements CreateBuilder {

        private boolean createParents = false;
        private CreateMode createMode = CreateMode.PERSISTENT;
        private Long ttl;

        @Override
        public ProtectACLCreateModeStatPathAndBytesable creatingParentsIfNeeded() {
            createParents = true;
            return new MockProtectACLCreateModeStatPathAndBytesable<>() {

                @Override
                public String forPath(String s, byte[] bytes) throws Exception {
                    return createNode(s, bytes, createParents, stat, createMode, fileSystem.root(), listeners, ttl);
                }

                @Override
                public String forPath(String s) throws Exception {
                    return createNode(s, new byte[0], createParents, stat, createMode, fileSystem.root(), listeners, ttl);
                }

            };
        }

        @Override
        public ProtectACLCreateModeStatPathAndBytesable creatingParentContainersIfNeeded() {
            return new MockProtectACLCreateModeStatPathAndBytesable<>() {

                @Override
                public String forPath(String s, byte[] bytes) throws Exception {
                    return createNode(s, bytes, createParents, stat, createMode, fileSystem.root(), listeners, ttl);
                }

                @Override
                public String forPath(String s) throws Exception {
                    return createNode(s, new byte[0], createParents, stat, createMode, fileSystem.root(), listeners, ttl);
                }

            };
        }

        @Override
        @Deprecated
        public ACLPathAndBytesable withProtectedEphemeralSequential() {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ACLCreateModeStatBackgroundPathAndBytesable withProtection() {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        public String forPath(String s) throws Exception {
            return createNode(s, new byte[0], createParents, null, createMode, fileSystem.root(), listeners, ttl);
        }

        public String forPath(String s, byte[] bytes) throws Exception {
            return createNode(s, bytes, createParents, null, createMode, fileSystem.root(), listeners, ttl);
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground() {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(Object o) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(BackgroundCallback backgroundCallback) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(BackgroundCallback backgroundCallback, Object o) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(BackgroundCallback backgroundCallback, Executor executor) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(BackgroundCallback backgroundCallback, Object o, Executor executor) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public CreateBuilderMain withTtl(long l) { this.ttl = l; return this; }

        @Override
        public CreateBuilder2 orSetData() {
            return null;
        }

        @Override
        public CreateBuilder2 orSetData(int i) {
            return null;
        }

        @Override
        public CreateBackgroundModeStatACLable compressed() {
            return null;
        }

        @Override
        public CreateProtectACLCreateModePathAndBytesable storingStatIn(Stat stat) {
            return null;
        }

        @Override
        public BackgroundPathAndBytesable withACL(List list) {
            return this;
        }

        @Override
        public ACLBackgroundPathAndBytesable withMode(CreateMode createMode) {
            this.createMode = createMode;
            return this;
        }

        @Override
        public BackgroundPathAndBytesable withACL(List list, boolean b) {
            return this;
        }

        @Override
        public CreateBuilder2 idempotent() { return null; }

    }

    private static class MockBackgroundPathableBuilder implements BackgroundPathable, Watchable> {

        @Override
        public ErrorListenerPathable inBackground() {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathable inBackground(Object o) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathable inBackground(BackgroundCallback backgroundCallback) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathable inBackground(BackgroundCallback backgroundCallback, Object o) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathable inBackground(BackgroundCallback backgroundCallback, Executor executor) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathable inBackground(BackgroundCallback backgroundCallback, Object o, Executor executor) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public T forPath(String s) throws Exception {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public BackgroundPathable watched() {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public BackgroundPathable usingWatcher(Watcher watcher) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public BackgroundPathable usingWatcher(CuratorWatcher curatorWatcher) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }
    }

    private class MockGetChildrenBuilder extends MockBackgroundPathableBuilder> implements GetChildrenBuilder {

        @Override
        public WatchPathable> storingStatIn(Stat stat) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public List forPath(String path) throws Exception {
            return getChildren(path, fileSystem.root());
        }

    }

    private class MockExistsBuilder extends MockBackgroundPathableBuilder implements ExistsBuilder {

        @Override
        public Stat forPath(String path) throws Exception {
            try {
                Stat stat = new Stat();
                getNode(path, stat, fileSystem.root());
                return stat;
            }
            catch (KeeperException.NoNodeException e) {
                return null;
            }
        }

        @Override
        public ACLableExistBuilderMain creatingParentsIfNeeded() {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ACLableExistBuilderMain creatingParentContainersIfNeeded() {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }
    }

    private class MockDeleteBuilder extends MockBackgroundPathableBuilder implements DeleteBuilder {

        private boolean deleteChildren = false;
        private int version = -1;

        @Override
        public BackgroundVersionable deletingChildrenIfNeeded() {
            deleteChildren = true;
            return this;
        }

        @Override
        public ChildrenDeletable guaranteed() {
            return this;
        }

        @Override
        public BackgroundPathable withVersion(int i) {
            version = i;
            return this;
        }

        public Void forPath(String pathString) throws Exception {
            deleteNode(pathString, deleteChildren, version, fileSystem.root(), listeners);
            return null;
        }

        @Override
        public DeleteBuilderMain quietly() {
            return this;
        }

        @Override
        public DeleteBuilderMain idempotent() { return this; }

    }

    private class MockGetDataBuilder extends MockBackgroundPathableBuilder implements GetDataBuilder {

        @Override
        public GetDataWatchBackgroundStatable decompressed() {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        public byte[] forPath(String path) throws Exception {
            return getData(path, null, fileSystem.root());
        }

        @Override
        public ErrorListenerPathable inBackground() {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathable inBackground(Object o) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathable inBackground(BackgroundCallback backgroundCallback) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathable inBackground(BackgroundCallback backgroundCallback, Object o) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathable inBackground(BackgroundCallback backgroundCallback, Executor executor) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathable inBackground(BackgroundCallback backgroundCallback, Object o, Executor executor) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public WatchPathable storingStatIn(Stat stat) {
            return new WatchPathable() {
                @Override
                public byte[] forPath(String path) throws Exception {
                    return getData(path, stat, fileSystem.root());
                }

                @Override
                public Pathable watched() {
                    return null;
                }

                @Override
                public Pathable usingWatcher(Watcher watcher) {
                    return null;
                }

                @Override
                public Pathable usingWatcher(CuratorWatcher watcher) {
                    return null;
                }
            };
        }
    }

    // extends MockBackgroundACLPathAndBytesableBuilder
    private class MockSetDataBuilder implements SetDataBuilder {

        int version = -1;
        @Override
        public SetDataBackgroundVersionable compressed() {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public BackgroundPathAndBytesable withVersion(int i) {
            version = i;
            return this;
        }

        @Override
        public Stat forPath(String path, byte[] bytes) throws Exception {
            Stat stat = new Stat();
            setData(path, bytes, version, stat, fileSystem.root(), listeners);
            return stat;
        }

        @Override
        public Stat forPath(String path) throws Exception {
            Stat stat = new Stat();
            setData(path, new byte[0], version, stat, fileSystem.root(), listeners);
            return stat;
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground() {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(Object o) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(BackgroundCallback backgroundCallback) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(BackgroundCallback backgroundCallback, Object o) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(BackgroundCallback backgroundCallback, Executor executor) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public ErrorListenerPathAndBytesable inBackground(BackgroundCallback backgroundCallback, Object o, Executor executor) {
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        @Override
        public SetDataBuilder idempotent() { return this; }

    }

    /** Allows addition of directoryListeners which are never called */
    public static class MockListenable implements Listenable {

        public final List listeners = new ArrayList<>();

        @Override
        public void addListener(T t) {
            listeners.add(t);
        }

        @Override
        public void addListener(T t, Executor executor) { throw new UnsupportedOperationException("not supported in mock curator"); }

        @Override
        public void removeListener(T t) {
            listeners.remove(t);
        }

    }

    private class MockCuratorTransactionFinal implements CuratorTransactionFinal {

        /** The new directory root in which the transactional changes are made */
        private final MemoryFileSystem.Node newRoot;

        private boolean committed = false;

        private final DelayedListener delayedListener = new DelayedListener();

        public MockCuratorTransactionFinal() {
            newRoot = fileSystem.root().clone();
        }

        @Override
        public Collection commit() throws Exception {
            fileSystem.replaceRoot(newRoot);
            committed = true;
            delayedListener.commit();
            return null; // TODO
        }

        @Override
        public TransactionCreateBuilder create() {
            ensureNotCommitted();
            return new MockTransactionCreateBuilder();
        }

        @Override
        public TransactionDeleteBuilder delete() {
            ensureNotCommitted();
            return new MockTransactionDeleteBuilder();
        }

        @Override
        public TransactionSetDataBuilder setData() {
            ensureNotCommitted();
            return new MockTransactionSetDataBuilder();
        }

        @Override
        public TransactionCheckBuilder check() {
            ensureNotCommitted();
            throw new UnsupportedOperationException("Not implemented in MockCurator");
        }

        private void ensureNotCommitted() {
            if (committed) throw new IllegalStateException("transaction already committed");
        }

        private class MockTransactionCreateBuilder implements TransactionCreateBuilder {

            private CreateMode createMode = CreateMode.PERSISTENT;

            @Override
            public ACLCreateModePathAndBytesable compressed() {
                throw new UnsupportedOperationException("Not implemented in MockCurator");
            }

            @Override
            public ACLPathAndBytesable withMode(CreateMode createMode) {
                this.createMode = createMode;
                return this;
            }

            @Override
            public CuratorTransactionBridge forPath(String s, byte[] bytes) throws Exception {
                createNode(s, bytes, false, null, createMode, newRoot, delayedListener, null);
                return new MockCuratorTransactionBridge();
            }

            @Override
            public CuratorTransactionBridge forPath(String s) throws Exception {
                createNode(s, new byte[0], false, null, createMode, newRoot, delayedListener, null);
                return new MockCuratorTransactionBridge();
            }

            @Override
            public TransactionCreateBuilder2 withTtl(long l) {
                return this;
            }

            @Override
            public MockTransactionCreateBuilder withACL(List list, boolean b) {
                return this;
            }

            @Override
            public MockTransactionCreateBuilder withACL(List list) {
                return this;
            }
        }

        private class MockTransactionDeleteBuilder implements TransactionDeleteBuilder {

            int version = -1;
            @Override
            public Pathable withVersion(int i) {
                version = i;
                return this;
            }

            @Override
            public CuratorTransactionBridge forPath(String path) throws Exception {
                deleteNode(path, false, version, newRoot, delayedListener);
                return new MockCuratorTransactionBridge();
            }

        }

        private class MockTransactionSetDataBuilder implements TransactionSetDataBuilder {

            int version = -1;
            @Override
            public VersionPathAndBytesable compressed() {
                throw new UnsupportedOperationException("Not implemented in MockCurator");
            }

            @Override
            public PathAndBytesable withVersion(int i) {
                version = i;
                return this;
            }

            @Override
            public CuratorTransactionBridge forPath(String s, byte[] bytes) throws Exception {
                MockCuratorFramework.this.setData(s, bytes, version, null, newRoot, delayedListener);
                return new MockCuratorTransactionBridge();
            }

            @Override
            public CuratorTransactionBridge forPath(String s) throws Exception {
                MockCuratorFramework.this.setData(s, new byte[0], version, null, newRoot, delayedListener);
                return new MockCuratorTransactionBridge();
            }

        }

        private class MockCuratorTransactionBridge implements CuratorTransactionBridge {

            @Override
            public CuratorTransactionFinal and() {
                return MockCuratorTransactionFinal.this;
            }

        }

        /** A class which collects listen events and forwards them to the regular directoryListeners on commit */
        private class DelayedListener extends Listeners {

            private final List> events = new ArrayList<>();

            @Override
            public void notify(Path path, PathChildrenCacheEvent event) {
                events.add(new Pair<>(path, event));
            }

            public void commit() {
                for (Pair event : events)
                    listeners.notify(event.getFirst(), event.getSecond());
            }

        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy