diff --git a/java/google/registry/tools/CommandRunner.java b/java/google/registry/tools/CommandRunner.java
new file mode 100644
index 000000000..e82237a8e
--- /dev/null
+++ b/java/google/registry/tools/CommandRunner.java
@@ -0,0 +1,24 @@
+// Copyright 2018 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.tools;
+
+/**
+ * Interface for things that run commands.
+ *
+ *
This exists only to allow us to test ShellCommand.
+ */
+interface CommandRunner {
+ public void run(String[] args) throws Exception;
+}
diff --git a/java/google/registry/tools/RegistryCli.java b/java/google/registry/tools/RegistryCli.java
index 1c6975f56..89ad73d2e 100644
--- a/java/google/registry/tools/RegistryCli.java
+++ b/java/google/registry/tools/RegistryCli.java
@@ -35,7 +35,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
/** Container class to create and run remote commands against a Datastore instance. */
@Parameters(separators = " =", commandDescription = "Command-line interface to the registry")
-final class RegistryCli {
+final class RegistryCli implements AutoCloseable, CommandRunner {
@Parameter(
names = {"-e", "--environment"},
@@ -57,33 +57,54 @@ final class RegistryCli {
@ParametersDelegate
private LoggingParameters loggingParams = new LoggingParameters();
- // The extends Class extends Command>> wildcard looks a little funny, but is needed so that
- // we can accept maps with value types that are subtypes of Class extends Command> rather than
- // literally that type. For more explanation, see:
- // http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeArguments.html#FAQ104
- void run(
- String programName,
- String[] args,
- ImmutableMap> commands) throws Exception {
+ // These are created lazily on first use.
+ private AppEngineConnection connection;
+ private RemoteApiInstaller installer;
+
+
+ Map commandInstances;
+ Map> commands;
+ JCommander jcommander;
+
+ RegistryCli(
+ String programName, ImmutableMap> commands) {
+ this.commands = commands;
+
Security.addProvider(new BouncyCastleProvider());
- JCommander jcommander = new JCommander(this);
+ jcommander = new JCommander(this);
jcommander.addConverterFactory(new ParameterFactory());
jcommander.setProgramName(programName);
// Store the instances of each Command class here so we can retrieve the same one for the
// called command later on. JCommander could have done this for us, but it doesn't.
- Map commandInstances = new HashMap<>();
+ commandInstances = new HashMap<>();
HelpCommand helpCommand = new HelpCommand(jcommander);
jcommander.addCommand("help", helpCommand);
commandInstances.put("help", helpCommand);
- for (Map.Entry> entry : commands.entrySet()) {
- Command command = entry.getValue().getDeclaredConstructor().newInstance();
- jcommander.addCommand(entry.getKey(), command);
- commandInstances.put(entry.getKey(), command);
- }
+ // Add the shell command.
+ ShellCommand shellCommand = new ShellCommand(System.in, this);
+ jcommander.addCommand("shell", shellCommand);
+ commandInstances.put("shell", shellCommand);
+ try {
+ for (Map.Entry> entry : commands.entrySet()) {
+ Command command = entry.getValue().getDeclaredConstructor().newInstance();
+ jcommander.addCommand(entry.getKey(), command);
+ commandInstances.put(entry.getKey(), command);
+ }
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // The extends Class extends Command>> wildcard looks a little funny, but is needed so that
+ // we can accept maps with value types that are subtypes of Class extends Command> rather than
+ // literally that type. For more explanation, see:
+ // http://www.angelikalanger.com/GenericsFAQ/FAQSections/TypeArguments.html#FAQ104
+ @Override
+ public void run(String[] args) throws Exception {
try {
jcommander.parse(args);
} catch (ParameterException e) {
@@ -127,6 +148,14 @@ final class RegistryCli {
}
}
+ @Override
+ public void close() {
+ if (installer != null) {
+ installer.uninstall();
+ installer = null;
+ }
+ }
+
private void runCommand(Command command) throws Exception {
// Create the main component and use it to inject the command class.
RegistryToolComponent component = DaggerRegistryToolComponent.builder()
@@ -142,31 +171,35 @@ final class RegistryCli {
}
// Get the App Engine connection, advise the user if they are not currently logged in..
- AppEngineConnection connection = component.appEngineConnection();
+ if (connection == null) {
+ connection = component.appEngineConnection();
+ }
if (command instanceof ServerSideCommand) {
((ServerSideCommand) command).setConnection(connection);
}
// RemoteApiCommands need to have the remote api installed to work.
- RemoteApiInstaller installer = new RemoteApiInstaller();
- RemoteApiOptions options = new RemoteApiOptions();
- options.server(connection.getServer().getHost(), connection.getServer().getPort());
- if (connection.isLocalhost()) {
- // Use dev credentials for localhost.
- options.useDevelopmentServerCredential();
- } else {
- options.useApplicationDefaultCredential();
+ if (installer == null) {
+ installer = new RemoteApiInstaller();
+ RemoteApiOptions options = new RemoteApiOptions();
+ options.server(connection.getServer().getHost(), connection.getServer().getPort());
+ if (connection.isLocalhost()) {
+ // Use dev credentials for localhost.
+ options.useDevelopmentServerCredential();
+ } else {
+ options.useApplicationDefaultCredential();
+ }
+ installer.install(options);
}
- installer.install(options);
// Ensure that all entity classes are loaded before command code runs.
ObjectifyService.initOfy();
- try {
- command.run();
- } finally {
- installer.uninstall();
- }
+ command.run();
+ }
+
+ void setEnvironment(RegistryToolEnvironment environment) {
+ this.environment = environment;
}
}
diff --git a/java/google/registry/tools/RegistryTool.java b/java/google/registry/tools/RegistryTool.java
index f10044307..5fe576aa5 100644
--- a/java/google/registry/tools/RegistryTool.java
+++ b/java/google/registry/tools/RegistryTool.java
@@ -132,6 +132,8 @@ public final class RegistryTool {
public static void main(String[] args) throws Exception {
RegistryToolEnvironment.parseFromArgs(args).setup();
- new RegistryCli().run("nomulus", args, COMMAND_MAP);
+ try (RegistryCli cli = new RegistryCli("nomulus", COMMAND_MAP)) {
+ cli.run(args);
+ }
}
}
diff --git a/java/google/registry/tools/ShellCommand.java b/java/google/registry/tools/ShellCommand.java
new file mode 100644
index 000000000..0e8f3838b
--- /dev/null
+++ b/java/google/registry/tools/ShellCommand.java
@@ -0,0 +1,93 @@
+// Copyright 2018 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.tools;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import com.beust.jcommander.Parameters;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StreamTokenizer;
+import java.io.StringReader;
+
+/**
+ * Implements a tiny shell interpreter for the nomulus tool.
+ *
+ * Parses a very simple command grammar. Tokens are either whitespace delimited words or
+ * double-quoted strings.
+ */
+@Parameters(commandDescription = "Run an interactive shell")
+public class ShellCommand implements Command {
+
+ private final CommandRunner runner;
+ private final BufferedReader lineReader;
+
+ ShellCommand(InputStream in, CommandRunner runner) {
+ this.runner = runner;
+ this.lineReader = new BufferedReader(new InputStreamReader(in, US_ASCII));
+ }
+
+ /** Run the shell until the user presses "Ctrl-D". */
+ @Override
+ public void run() {
+ String line;
+ while ((line = getLine()) != null) {
+ String[] lineArgs = parseCommand(line);
+ try {
+ runner.run(lineArgs);
+ } catch (Exception e) {
+ System.err.println("Got an exception:\n" + e);
+ }
+ }
+ }
+
+ private String getLine() {
+ if (System.console() != null) {
+ System.err.print("nom> ");
+ }
+ try {
+ return lineReader.readLine();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ @VisibleForTesting
+ static String[] parseCommand(String line) {
+ ImmutableList.Builder resultBuilder = new ImmutableList.Builder<>();
+
+ // Create a tokenizer, make everything word characters except quoted strings and unprintable
+ // ascii chars and space (just treat them all as whitespace).
+ StreamTokenizer tokenizer = new StreamTokenizer(new StringReader(line));
+ tokenizer.resetSyntax();
+ tokenizer.whitespaceChars(0, ' ');
+ tokenizer.wordChars('!', '~');
+ tokenizer.quoteChar('"');
+
+ try {
+ while (tokenizer.nextToken() != StreamTokenizer.TT_EOF) {
+ resultBuilder.add(tokenizer.sval);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ return resultBuilder.build().toArray(new String[0]);
+ }
+}
diff --git a/javatests/google/registry/tools/RegistryToolTest.java b/javatests/google/registry/tools/RegistryToolTest.java
index 96b98b6fa..68204dcf3 100644
--- a/javatests/google/registry/tools/RegistryToolTest.java
+++ b/javatests/google/registry/tools/RegistryToolTest.java
@@ -90,8 +90,8 @@ public class RegistryToolTest {
/**
* Gets the set of all non-abstract classes implementing the {@link Command} interface (abstract
* class and interface subtypes of Command aren't expected to have cli commands). Note that this
- * also filters out HelpCommand, which has special handling in {@link RegistryCli} and isn't in
- * the command map.
+ * also filters out HelpCommand and ShellCommand, which have special handling in {@link
+ * RegistryCli} and aren't in the command map.
*
* @throws IOException if reading the classpath resources fails.
*/
@@ -105,7 +105,8 @@ public class RegistryToolTest {
if (Command.class.isAssignableFrom(clazz)
&& !Modifier.isAbstract(clazz.getModifiers())
&& !Modifier.isInterface(clazz.getModifiers())
- && !clazz.equals(HelpCommand.class)) {
+ && !clazz.equals(HelpCommand.class)
+ && !clazz.equals(ShellCommand.class)) {
builder.add((Class extends Command>) clazz);
}
}
diff --git a/javatests/google/registry/tools/ShellCommandTest.java b/javatests/google/registry/tools/ShellCommandTest.java
new file mode 100644
index 000000000..76581f5f7
--- /dev/null
+++ b/javatests/google/registry/tools/ShellCommandTest.java
@@ -0,0 +1,68 @@
+// Copyright 2018 The Nomulus Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package google.registry.tools;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static org.mockito.Mockito.mock;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.io.ByteArrayInputStream;
+import java.util.ArrayList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class ShellCommandTest {
+
+ CommandRunner cli = mock(CommandRunner.class);
+
+ public ShellCommandTest() {}
+
+ @Test
+ public void testParsing() {
+ assertThat(ShellCommand.parseCommand("foo bar 123 baz+ // comment \"string data\""))
+ .isEqualTo(new String[] {"foo", "bar", "123", "baz+", "//", "comment", "string data"});
+ assertThat(ShellCommand.parseCommand("\"got \\\" escapes?\""))
+ .isEqualTo(new String[] {"got \" escapes?"});
+ assertThat(ShellCommand.parseCommand("")).isEqualTo(new String[0]);
+ }
+
+ @Test
+ public void testCommandProcessing() {
+ String testData = "test1 foo bar\ntest2 foo bar\n";
+ ImmutableMap> commandMap = ImmutableMap.of();
+ MockCli cli = new MockCli();
+ ShellCommand shellCommand =
+ new ShellCommand(new ByteArrayInputStream(testData.getBytes(US_ASCII)), cli);
+ shellCommand.run();
+ assertThat(cli.calls)
+ .containsExactly(
+ ImmutableList.of("test1", "foo", "bar"), ImmutableList.of("test2", "foo", "bar"))
+ .inOrder();
+ }
+
+ static class MockCli implements CommandRunner {
+ public ArrayList> calls = new ArrayList<>();
+
+ @Override
+ public void run(String[] args)
+ throws Exception {
+ calls.add(ImmutableList.copyOf(args));
+ }
+ }
+}