You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ratis.apache.org by sz...@apache.org on 2021/11/10 07:27:17 UTC

[ratis] branch master updated: RATIS-1425. Add ratis-shell sub commands framework (#528)

This is an automated email from the ASF dual-hosted git repository.

szetszwo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ratis.git


The following commit(s) were added to refs/heads/master by this push:
     new 8fb9f7a  RATIS-1425. Add ratis-shell sub commands framework (#528)
8fb9f7a is described below

commit 8fb9f7a64228a442af0a539614b03aa951ce1240
Author: maobaolong <30...@qq.com>
AuthorDate: Wed Nov 10 15:27:13 2021 +0800

    RATIS-1425. Add ratis-shell sub commands framework (#528)
---
 ratis-shell/pom.xml                                |  25 ++++
 .../org/apache/ratis/shell/cli/AbstractShell.java  | 149 +++++++++++++++++++++
 .../java/org/apache/ratis/shell/cli/Command.java   | 122 +++++++++++++++++
 .../org/apache/ratis/shell/cli/sh/RatisShell.java  |  62 ++++++++-
 .../apache/ratis/shell/cli/sh/command/Context.java |  54 ++++++++
 5 files changed, 410 insertions(+), 2 deletions(-)

diff --git a/ratis-shell/pom.xml b/ratis-shell/pom.xml
index e6305dc..1b3a26f 100644
--- a/ratis-shell/pom.xml
+++ b/ratis-shell/pom.xml
@@ -24,9 +24,34 @@
   <name>Apache Ratis Shell</name>
 
   <properties>
+    <guava.version>29.0-jre</guava.version>
   </properties>
 
   <dependencies>
+    <dependency>
+      <groupId>org.apache.ratis</groupId>
+      <artifactId>ratis-common</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>commons-cli</groupId>
+      <artifactId>commons-cli</artifactId>
+      <version>1.5.0</version>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-log4j12</artifactId>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.reflections</groupId>
+      <artifactId>reflections</artifactId>
+      <version>0.10.2</version>
+    </dependency>
   </dependencies>
   <build>
     <plugins>
diff --git a/ratis-shell/src/main/java/org/apache/ratis/shell/cli/AbstractShell.java b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/AbstractShell.java
new file mode 100644
index 0000000..7a32c30
--- /dev/null
+++ b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/AbstractShell.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ratis.shell.cli;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.ratis.thirdparty.com.google.common.io.Closer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * Abstract class for handling command line inputs.
+ */
+public abstract class AbstractShell implements Closeable {
+  private static final Logger LOG = LoggerFactory.getLogger(AbstractShell.class);
+
+  private final Map<String, Command> mCommands;
+  private final Closer closer;
+
+  /**
+   * Creates a new instance of {@link AbstractShell}.
+   */
+  public AbstractShell() {
+    closer = Closer.create();
+    mCommands = loadCommands();
+    // Register all loaded commands under closer.
+    mCommands.values().forEach(closer::register);
+  }
+
+  /**
+   * Handles the specified shell command request, displaying usage if the command format is invalid.
+   *
+   * @param argv [] Array of arguments given by the user's input from the terminal
+   * @return 0 if command is successful, -1 if an error occurred
+   */
+  public int run(String... argv) {
+    if (argv.length == 0) {
+      printUsage();
+      return -1;
+    }
+
+    // Sanity check on the number of arguments
+    String cmd = argv[0];
+    Command command = mCommands.get(cmd);
+
+    if (command == null) {
+      // Unknown command (we didn't find the cmd in our dict)
+      System.err.printf("%s is an unknown command.%n", cmd);
+      printUsage();
+      return -1;
+    }
+
+    // Find the inner-most command and its argument line.
+    CommandLine cmdline;
+    try {
+      String[] currArgs = Arrays.copyOf(argv, argv.length);
+      while (command.hasSubCommand()) {
+        if (currArgs.length < 2) {
+          throw new IllegalArgumentException("No sub-command is specified");
+        }
+        if (!command.getSubCommands().containsKey(currArgs[1])) {
+          throw new IllegalArgumentException("Unknown sub-command: " + currArgs[1]);
+        }
+        command = command.getSubCommands().get(currArgs[1]);
+        currArgs = Arrays.copyOfRange(currArgs, 1, currArgs.length);
+      }
+      currArgs = Arrays.copyOfRange(currArgs, 1, currArgs.length);
+
+      cmdline = command.parseAndValidateArgs(currArgs);
+    } catch (IllegalArgumentException e) {
+      // It outputs a prompt message when passing wrong args to CLI
+      System.out.println(e.getMessage());
+      System.out.println("Usage: " + command.getUsage());
+      System.out.println(command.getDescription());
+      LOG.error("Invalid arguments for command {}:", command.getCommandName(), e);
+      return -1;
+    }
+
+    // Handle the command
+    try {
+      return command.run(cmdline);
+    } catch (Exception e) {
+      System.out.println(e.getMessage());
+      LOG.error("Error running" + Arrays.stream(argv).reduce("", (a, b) -> a + " " + b), e);
+      return -1;
+    }
+  }
+
+  /**
+   * @return all commands provided by this shell
+   */
+  public Collection<Command> getCommands() {
+    return mCommands.values();
+  }
+
+  @Override
+  public void close() throws IOException {
+    closer.close();
+  }
+
+  /**
+   * @return name of the shell
+   */
+  protected abstract String getShellName();
+
+  /**
+   * Map structure: Command name => {@link Command} instance.
+   *
+   * @return a set of commands which can be executed under this shell
+   */
+  protected abstract Map<String, Command> loadCommands();
+
+  protected Closer getCloser() {
+    return closer;
+  }
+
+  /**
+   * Prints usage for all commands.
+   */
+  protected void printUsage() {
+    System.out.println("Usage: ratis " + getShellName() + " [generic options]");
+    SortedSet<String> sortedCmds = new TreeSet<>(mCommands.keySet());
+    for (String cmd : sortedCmds) {
+      System.out.format("%-60s%n", "\t [" + mCommands.get(cmd).getUsage() + "]");
+    }
+  }
+}
diff --git a/ratis-shell/src/main/java/org/apache/ratis/shell/cli/Command.java b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/Command.java
new file mode 100644
index 0000000..31b8688
--- /dev/null
+++ b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/Command.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ratis.shell.cli;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * An interface for all the commands that can be run from a shell.
+ */
+public interface Command extends Closeable {
+
+  /**
+   * Gets the command name as input from the shell.
+   *
+   * @return the command name
+   */
+  String getCommandName();
+
+  /**
+   * @return the supported {@link Options} of the command
+   */
+  default Options getOptions() {
+    return new Options();
+  }
+
+  /**
+   * If a command has sub-commands, the first argument should be the sub-command's name,
+   * all arguments and options will be parsed for the sub-command.
+   *
+   * @return whether this command has sub-commands
+   */
+  default boolean hasSubCommand() {
+    return false;
+  }
+
+  /**
+   * @return a map from sub-command names to sub-command instances
+   */
+  default Map<String, Command> getSubCommands() {
+    return Collections.emptyMap();
+  }
+
+  /**
+   * Parses and validates the arguments.
+   *
+   * @param args the arguments for the command, excluding the command name
+   * @return the parsed command line object
+   * @throws IllegalArgumentException when arguments are not valid
+   */
+  default CommandLine parseAndValidateArgs(String... args) throws IllegalArgumentException {
+    CommandLine cmdline;
+    Options opts = getOptions();
+    CommandLineParser parser = new DefaultParser();
+    try {
+      cmdline = parser.parse(opts, args);
+    } catch (ParseException e) {
+      throw new IllegalArgumentException(
+          String.format("Failed to parse args for %s: %s", getCommandName(), e.getMessage()), e);
+    }
+    validateArgs(cmdline);
+    return cmdline;
+  }
+
+  /**
+   * Checks if the arguments are valid or throw InvalidArgumentException.
+   *
+   * @param cl the parsed command line for the arguments
+   * @throws IllegalArgumentException when arguments are not valid
+   */
+  default void validateArgs(CommandLine cl) throws IllegalArgumentException {}
+
+  /**
+   * Runs the command.
+   *
+   * @param cl the parsed command line for the arguments
+   * @return the result of running the command
+   */
+  default int run(CommandLine cl) throws IOException {
+    return 0;
+  }
+
+  /**
+   * @return the usage information of the command
+   */
+  String getUsage();
+
+  /**
+   * @return the description information of the command
+   */
+  String getDescription();
+
+  /**
+   * Used to close resources created by commands.
+   *
+   * @throws IOException if closing resources fails
+   */
+  default void close() throws IOException {}
+}
diff --git a/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/RatisShell.java b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/RatisShell.java
index 0a26b5e..eebef9e 100644
--- a/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/RatisShell.java
+++ b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/RatisShell.java
@@ -17,8 +17,66 @@
  */
 package org.apache.ratis.shell.cli.sh;
 
-public class RatisShell {
+import org.apache.ratis.shell.cli.AbstractShell;
+import org.apache.ratis.shell.cli.Command;
+import org.apache.ratis.shell.cli.sh.command.Context;
+import org.apache.ratis.util.ReflectionUtils;
+import org.reflections.Reflections;
+
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Shell for manage ratis group.
+ */
+public class RatisShell extends AbstractShell {
+
+  /**
+   * Manage ratis shell command.
+   *
+   * @param args array of arguments given by the user's input from the terminal
+   */
   public static void main(String[] args) {
-    System.out.println("Hello " + RatisShell.class.getSimpleName());
+    RatisShell extensionShell = new RatisShell();
+    System.exit(extensionShell.run(args));
+  }
+
+  @Override
+  protected String getShellName() {
+    return "sh";
+  }
+
+  @Override
+  protected Map<String, Command> loadCommands() {
+    Context adminContext = new Context(System.out);
+    return loadCommands(RatisShell.class.getPackage().getName(),
+        new Class[] {Context.class},
+        new Object[] {getCloser().register(adminContext)});
+  }
+
+  /**
+   * Get instances of all subclasses of {@link Command} in a sub-package called "command" the given
+   * package.
+   *
+   * @param pkgName package prefix to look in
+   * @param classArgs type of args to instantiate the class
+   * @param objectArgs args to instantiate the class
+   * @return a mapping from command name to command instance
+   */
+  public static Map<String, Command> loadCommands(String pkgName, Class[] classArgs,
+      Object[] objectArgs) {
+    Map<String, Command> commandsMap = new HashMap<>();
+    Reflections reflections = new Reflections(pkgName);
+    for (Class<? extends Command> cls : reflections.getSubTypesOf(Command.class)) {
+      // Add commands from <pkgName>.command.*
+      if (cls.getPackage().getName().equals(pkgName + ".command")
+          && !Modifier.isAbstract(cls.getModifiers())) {
+        // Only instantiate a concrete class
+        final Command cmd = ReflectionUtils.newInstance(cls, classArgs, objectArgs);
+        commandsMap.put(cmd.getCommandName(), cmd);
+      }
+    }
+    return commandsMap;
   }
 }
diff --git a/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/command/Context.java b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/command/Context.java
new file mode 100644
index 0000000..bae98dc
--- /dev/null
+++ b/ratis-shell/src/main/java/org/apache/ratis/shell/cli/sh/command/Context.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ratis.shell.cli.sh.command;
+
+import org.apache.ratis.thirdparty.com.google.common.io.Closer;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Objects;
+
+/**
+ * A context for ratis-shell.
+ */
+public final class Context implements Closeable {
+  private final PrintStream mPrintStream;
+  private final Closer mCloser;
+
+  /**
+   * Build a context.
+   * @param printStream the print stream
+   */
+  public Context(PrintStream printStream) {
+    mCloser = Closer.create();
+    mPrintStream = mCloser.register(Objects.requireNonNull(printStream, "printStream == null"));
+  }
+
+  /**
+   * @return the print stream to write to
+   */
+  public PrintStream getPrintStream() {
+    return mPrintStream;
+  }
+
+  @Override
+  public void close() throws IOException {
+    mCloser.close();
+  }
+}