<repo_name>alex-krash/jazoo<file_sep>/README.md
# jazoo - Zookeeper Client based on Spring-Shell

jazoo - is a modern cli for zookeeper base on spring-shell.

## Debug run (for developers)
```
mvn spring-boot:run -Dspring-boot.run.arguments=--server=localhost:2182
```

## Build
```
git clone https://github.com/vbabaev/jazoo.git
cd jazoo
mvn clean package
java -jar target/jazoo-*.jar --server=localhost:2181
jazoo@localhost:2181:/$ pwd
/
```

## Supported functionality

### cat $path
Prints contents of the given znode
```
jazoo@localhost:2181:/$ cat logs/log-001
log_line_1: err
log_line_1: inf
```

### cd $path
Sets current directory to $path
```
jazoo@localhost:2181:/$ pwd
/
jazoo@localhost:2181:/$ cd dir1
jazoo@localhost:2181:/dir1$ pwd
/dir1
```

### pwd (no arguments)
Returns current directory
```
jazoo@localhost:2181:/$ pwd
/
```

### ls [-l:--long] $path
Returns list of nodes by path
```
jazoo@localhost:2181:/$ ls -l
172	0	2019-08-30 15:54:37   /dir1
172	0	2019-08-30 15:54:37   /dir2
172	0	2019-08-30 15:54:37   /dir3


jazoo@localhost:2181:/$ ls
dir1
dir2
dir3
```
<file_sep>/src/main/java/dev/vbabaev/tools/jazoo/Client.java
package dev.vbabaev.tools.jazoo;

import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStyle;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.Banner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.jline.PromptProvider;

@SpringBootApplication
public class Client {
    public static void main(String[] args) {
        new SpringApplicationBuilder()
                .bannerMode(Banner.Mode.OFF)
                .sources(Client.class)
                .run(args);
    }

    @Bean
    public PromptProvider myPromptProvider(PathResolver pathResolver, @Value("${server:localhost}") String server) {
        return () -> new AttributedString("jazoo@" + server + ":" + pathResolver.getCurrent() + "$ ", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW));
    }

}
<file_sep>/src/main/java/dev/vbabaev/tools/jazoo/ConnectionPool.java
package dev.vbabaev.tools.jazoo;

import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

@Service
@Scope("singleton")
public class ConnectionPool {

    private final GenericObjectPool<ZooKeeperPoolable> pool;

    public ConnectionPool(@Value("${server:localhost}") String connectionString, @Value("${timeout:10000}") int timeOut, @Value("${max-connections:10}") int maxConnections) {
        PoolableConnectionFactory factory = new PoolableConnectionFactory(connectionString, timeOut);

        GenericObjectPoolConfig<ZooKeeperPoolable> config = new GenericObjectPoolConfig<>();
        config.setMaxTotal(maxConnections);

        GenericObjectPool<ZooKeeperPoolable> pool = new GenericObjectPool<>(factory, config);
        factory.setPool(pool);
        this.pool = pool;
    }

    public ZooKeeperPoolable getConnection() throws InterruptedException {
        try {
            return pool.borrowObject();
        } catch (Exception e) {
            throw new InterruptedException(e.getMessage());
        }
    }

}
<file_sep>/src/main/java/dev/vbabaev/tools/jazoo/PathResolver.java
package dev.vbabaev.tools.jazoo;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

import java.util.Stack;

@Service
@Scope("singleton")
public class PathResolver {
    private String current;

    public PathResolver() {
        this.current = "/";
    }

    public String resolveParts(String path) {
        String[] parts = path.split("/");
        Stack<String> stack = new Stack<>();
        for (String part: parts) {
            if (part.isEmpty()) continue;
            if (part.equals(".")) continue;
            if (part.equals("..")) {
                stack.pop();
                continue;
            }
            stack.push(part);
        }
        return "/" + String.join("/", stack);
    }

    public static String basename(String path) {
        String[] parts = path.split("/");
        StringBuilder stringBuilder = new StringBuilder("");
        for (int i = 1; i < parts.length - 1; i++) {
            stringBuilder.append("/").append(parts[i]);
        }
        return stringBuilder.toString();
    }

    public static String join(String basename, String filename)
    {
        return basename + (basename.equals("/") ? "": "/") + filename;
    }


    public static String filename(String path)
    {
        String result = path.replaceAll(basename(path), "");
        if (result.equals("/")) return result;
        return result.substring(1);
    }

    public String resolve(String path) {
        String resolved;
        if (path.startsWith("/")) {
            resolved = trim(path);
        } else {
            resolved = current + (current.equals("/") ? "" : "/") + path;
        }
        return resolveParts(resolved);
    }

    public String set(String path) {
        this.current = resolve(path);
        return this.current;
    }

    public String getCurrent() {
        return this.current;
    }

    private String trim(String path) {
        if (path.equals("/")) return path;
        if (path.endsWith("/")) return trim(path.substring(0, path.length() - 1));
        return path;
    }
}
<file_sep>/src/main/java/dev/vbabaev/tools/jazoo/command/CommandRemove.java
package dev.vbabaev.tools.jazoo.command;

import dev.vbabaev.tools.jazoo.PathResolver;
import dev.vbabaev.tools.jazoo.ZooKeeperConnection2;
import org.apache.zookeeper.KeeperException;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.shell.standard.ShellOption;

import java.util.List;

@ShellComponent
public class CommandRemove {
    private PathResolver resolver;
    private ZooKeeperConnection2 connection;

    public CommandRemove(ZooKeeperConnection2 connection, PathResolver resolver) {
        this.connection = connection;
        this.resolver = resolver;
    }

    @ShellMethod(key = "rm", value = "Create new file or updates mtime for the existing file")
    public void remove(
            @ShellOption(value = {"-r", "--recursive"}, arity = 0, defaultValue = "false") boolean recursive,
            @ShellOption(value = {"-v", "--verbose"}, arity = 0, defaultValue = "false") boolean verbose,
            @ShellOption(value = "") String path
    ) throws KeeperException, InterruptedException {
        String resolvedPath = resolver.resolve(path);
        if (!recursive) {
            removeSingle(resolvedPath, verbose);
        } else {
            removeRecursive(resolvedPath, verbose);
        }
    }

    private void removeSingle(String resolvedPath, boolean verbose) throws KeeperException, InterruptedException {
        if (connection.listChildren(resolvedPath).size() > 0) {
            throw new RuntimeException("Node " + resolvedPath + " is not empty");
        }
        connection.delete(resolvedPath);
        if (verbose) {
            System.out.println("Deleted " + resolvedPath);
        }
    }

    private void removeRecursive(String resolvedPath, boolean verbose) throws KeeperException, InterruptedException {
        List<String> children = connection.listChildren(resolvedPath);
        for (String child: children) {
            removeRecursive(child, verbose);
        }
        removeSingle(resolvedPath, verbose);
    }

}
<file_sep>/src/main/java/dev/vbabaev/tools/jazoo/ListChildrenValueProvider.java
package dev.vbabaev.tools.jazoo;

import org.springframework.core.MethodParameter;
import org.springframework.shell.CompletionContext;
import org.springframework.shell.CompletionProposal;
import org.springframework.shell.standard.ValueProvider;
import org.springframework.shell.standard.ValueProviderSupport;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.stream.Collectors;

@Component
public class ListChildrenValueProvider extends ValueProviderSupport {

    private ConnectionPool pool;
    private PathResolver resolver;

    public ListChildrenValueProvider(ConnectionPool pool, PathResolver resolver) {
        this.pool = pool;
        this.resolver = resolver;
    }

    @Override
    public List<CompletionProposal> complete(MethodParameter parameter, CompletionContext completionContext, String[] hints) {

        String input = completionContext.currentWordUpToCursor();
        int lastSlash = input.lastIndexOf("/");
        String dir = lastSlash > -1 ? input.substring(0, lastSlash+1) : "";
        String prefix = input.substring(lastSlash + 1, input.length());

        try (ZooKeeperPoolable conn = pool.getConnection()) {
            String parentDir = resolver.resolve(dir);
            List<String> strings = conn.getChildren(parentDir, false);
            List<CompletionProposal> retval = strings.stream().filter(s -> s.startsWith(prefix)).map(s -> new CompletionProposal(dir + s)).collect(Collectors.toList());
            return retval;
        } catch (Exception err) {
            throw new RuntimeException(err);
        }
    }
}
<file_sep>/src/main/java/dev/vbabaev/tools/jazoo/PoolableConnectionFactory.java
package dev.vbabaev.tools.jazoo;

import org.apache.commons.pool2.ObjectPool;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.DefaultPooledObject;

class PoolableConnectionFactory implements PooledObjectFactory<ZooKeeperPoolable> {

    private ObjectPool<ZooKeeperPoolable> pool;

    private final String connectionString;
    private final int sessionTimeout;

    public PoolableConnectionFactory(String connectionString, int sessionTimeout) {
        this.connectionString = connectionString;
        this.sessionTimeout = sessionTimeout;
    }

    public void setPool(ObjectPool<ZooKeeperPoolable> pool) {
        this.pool = pool;
    }

    @Override
    public PooledObject<ZooKeeperPoolable> makeObject() throws Exception {
        return new DefaultPooledObject<>(new ZooKeeperPoolable(connectionString, sessionTimeout, pool));
    }

    @Override
    public void destroyObject(PooledObject<ZooKeeperPoolable> p) throws Exception {
        p.getObject().reallyClose();
    }

    @Override
    public boolean validateObject(PooledObject<ZooKeeperPoolable> p) {
        return true;
    }

    @Override
    public void activateObject(PooledObject<ZooKeeperPoolable> p) throws Exception {

    }

    @Override
    public void passivateObject(PooledObject<ZooKeeperPoolable> p) throws Exception {

    }
}
