package restx.shell;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.io.ByteProcessor;
import com.google.common.io.ByteStreams;
import jline.console.ConsoleReader;
import jline.console.completer.Completer;
import restx.build.MavenSupport;
import restx.build.ModuleDescriptor;
import restx.build.RestxJsonSupport;
import restx.common.Version;
import restx.factory.Factory;
import restx.shell.commands.HelpCommand;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.util.Arrays.asList;
import static jline.console.ConsoleReader.RESET_LINE;
/**
* User: xavierhanin
* Date: 4/9/13
* Time: 9:42 PM
*/
public class RestxShell implements Appendable {
public static final String DEFAULT_PROMPT = "restx";
private final ConsoleReader consoleReader;
private final Factory factory;
private final ImmutableSet<ShellCommand> commands;
private WatchDir watcher;
private final List<WatchListener> listeners = new CopyOnWriteArrayList<>();
private final ExecutorService watcherExecutorService = Executors.newSingleThreadExecutor();
private Path currentLocation = Paths.get(System.getProperty("user.dir"));
private ExecMode execMode = ExecMode.INTERACTIVE;
private Optional<String> restXProjectName = Optional.absent();
public RestxShell(ConsoleReader consoleReader) {
this(consoleReader, Factory.getInstance());
}
public RestxShell(ConsoleReader consoleReader, Factory factory) {
this.consoleReader = consoleReader;
this.factory = factory;
this.commands = ImmutableSet.copyOf(findCommands());
initConsole(consoleReader);
}
public ConsoleReader getConsoleReader() {
return consoleReader;
}
public Factory getFactory() {
return factory;
}
public ImmutableSet<ShellCommand> getCommands() {
return commands;
}
public void start() throws IOException {
execMode = ExecMode.INTERACTIVE;
banner();
checkIsRestXProjectDirectory();
showPrompt(consoleReader);
try {
for (ShellCommand command : commands) {
command.start(this);
}
} catch (ExitShell e) {
terminate();
return;
}
installCompleters();
boolean exit = false;
while (!exit) {
String line = consoleReader.readLine();
exit = exec(line);
}
terminate();
}
private void terminate() throws IOException {
consoleReader.println("Bye.");
synchronized (this) {
if (watcher != null) {
watcherExecutorService.shutdownNow();
watcher = null;
}
}
consoleReader.shutdown();
}
public void exec(Iterable<String> commands) throws Exception {
execMode = ExecMode.BATCH;
banner();
try {
for (String command : commands) {
printIn("> " + command, AnsiCodes.ANSI_PURPLE);
println("");
doExec(command);
}
} finally {
terminate();
}
}
public static void printIn(Appendable appendable, String msg, String ansiCode) throws IOException {
appendable.append(ansiCode + msg + AnsiCodes.ANSI_RESET);
}
public static List<String> splitArgs(String line) {
return ImmutableList.copyOf(Splitter.on(" ").omitEmptyStrings().split(line));
}
public void printIn(String msg, String ansiCode) throws IOException {
consoleReader.print(ansiCode + msg + AnsiCodes.ANSI_RESET);
}
@Override
public Appendable append(CharSequence csq) throws IOException {
consoleReader.print(csq);
return this;
}
@Override
public Appendable append(CharSequence csq, int start, int end) throws IOException {
if (csq != null) {
append(csq.subSequence(start, end));
}
return this;
}
@Override
public Appendable append(char c) throws IOException {
consoleReader.print(String.valueOf(c));
return this;
}
public String ask(String msg, String defaultValue) throws IOException {
return ask(msg, defaultValue,
"No help provided for that question, sorry, try to figure it out or ask to the community...");
}
public String ask(String msg, String defaultValue, String help) throws IOException {
while (true) {
String value = consoleReader.readLine(String.format(msg, defaultValue));
if (value.trim().isEmpty()) {
return defaultValue;
} else if (value.trim().equals("??")) {
printIn(help, AnsiCodes.ANSI_YELLOW);
println("");
} else {
return value.trim();
}
}
}
public boolean askBoolean(String message, String defaultValue) throws IOException {
return asList("y", "yes", "true", "on").contains(ask(message, defaultValue).toLowerCase(Locale.ENGLISH));
}
public boolean askBoolean(String message, String defaultValue, String help) throws IOException {
return asList("y", "yes", "true", "on").contains(ask(message, defaultValue, help).toLowerCase(Locale.ENGLISH));
}
public void print(String s) throws IOException {
append(s);
}
public void println(String msg) throws IOException {
consoleReader.println(msg);
consoleReader.flush();
}
public void printError(String msg, Exception ex) {
System.err.println(msg);
ex.printStackTrace();
}
public Path currentLocation() {
return currentLocation;
}
public void cd(Path path) {
restXProjectName(Optional.<String>absent());
currentLocation = path;
checkIsRestXProjectDirectory();
}
private void checkIsRestXProjectDirectory() {
File currentDirectory = currentLocation.toFile();
Optional<ModuleDescriptor> moduleDescriptor = getModuleDescriptor(currentDirectory);
if (moduleDescriptor.isPresent()) {
restXProjectName(Optional.of(moduleDescriptor.get().getGav().getArtifactId()));
}
}
private Optional<ModuleDescriptor> getModuleDescriptor(File currentDirectory) {
File restXProjectDescriptor = new File(currentDirectory, "md.restx.json");
if (restXProjectDescriptor.exists()) {
RestxJsonSupport restxJsonSupport = new RestxJsonSupport();
try {
return Optional.of(restxJsonSupport.parse(restXProjectDescriptor.toPath()));
} catch (IOException e) {
printError("Failed to read md.restx.json", e);
}
}
File mavenProjectDescriptor = new File(currentDirectory, "pom.xml");
if (mavenProjectDescriptor.exists()) {
MavenSupport mavenSupport = new MavenSupport();
try {
return Optional.of(mavenSupport.parse(mavenProjectDescriptor.toPath()));
} catch (IOException e) {
printError("Failed to read pom.xml", e);
}
}
return Optional.absent();
}
public Path installLocation() {
return Paths.get(System.getProperty("restx.shell.home", ".")).normalize();
}
public void restart() {
try {
if (execMode == ExecMode.BATCH) {
printIn("TERMINATING SHELL [NO AUTO RESTART IN BATCH MODE]...", AnsiCodes.ANSI_GREEN);
println("");
throw new ExitShell();
} else {
new File(installLocation().toFile(), ".restart").createNewFile();
printIn("RESTARTING SHELL...", AnsiCodes.ANSI_RED);
println("");
throw new ExitShell();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void watchFile(WatchListener listener) {
synchronized (this) {
if (watcher == null) {
final Path dir = currentLocation();
watcherExecutorService.execute(new Runnable() {
@Override
public void run() {
try {
watcher = new WatchDir(dir, true) {
@Override
protected void onEvent(WatchEvent.Kind<?> kind, Path path) {
for (WatchListener watchListener : listeners) {
try {
watchListener.onEvent(RestxShell.this, kind, path);
} catch (Exception ex) {
printError("FS event propagation to " + watchListener +
" raised an exception: " + ex, ex);
}
}
}
};
watcher.processEvents();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
listeners.add(listener);
}
protected void initConsole(ConsoleReader consoleReader) {
consoleReader.setPrompt(DEFAULT_PROMPT + "> ");
consoleReader.setHistoryEnabled(true);
}
protected void banner() throws IOException {
consoleReader.println("===============================================================================");
consoleReader.println("== WELCOME TO RESTX SHELL - " + version()
+ (execMode == ExecMode.INTERACTIVE
? (" - type `help` for help on available commands")
: " - BATCH MODE"));
consoleReader.println("===============================================================================");
}
/**
* Executes the given command line.
*
* Note: this won't raise an exception except in case of IOException with the console itself.
*
* @param line the command line to execute
* @return true if the shell should exit after this command, false otherwise.
* @throws IOException
*/
protected boolean exec(String line) throws IOException {
try {
doExec(line);
return false;
} catch (CommandNotFoundException e) {
consoleReader.println("command not found. use `help` to get the list of commands.");
return false;
} catch (ExitShell e) {
return true;
} catch (Exception e) {
consoleReader.println("command " + line + " raised an exception: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* Executes the given command.
*
* Exception raised by command are propagated if any, including the ExitShell exception.
*
* If command is not found, it raises a CommandNotFoundException.
*
* @param line the command line to execute
* @throws Exception
*/
protected void doExec(String line) throws Exception {
for (ShellCommand command : commands) {
Optional<? extends ShellCommandRunner> match = command.match(line);
if (match.isPresent()) {
// store current completers and clean them, so that executing command can perform in a clean env
Collection<Completer> storedCompleters = ImmutableList.copyOf(consoleReader.getCompleters());
for (Completer completer : storedCompleters) {
consoleReader.removeCompleter(completer);
}
try {
match.get().run(this);
} finally {
for (Completer completer : ImmutableList.copyOf(consoleReader.getCompleters())) {
consoleReader.removeCompleter(completer);
}
for (Completer completer : storedCompleters) {
consoleReader.addCompleter(completer);
}
showPrompt(consoleReader);
}
return;
}
}
throw new CommandNotFoundException(line);
}
private void showPrompt(ConsoleReader consoleReader) {
if (restXProjectName.isPresent()) {
consoleReader.setPrompt(AnsiCodes.ANSI_CYAN + restXProjectName.get() + AnsiCodes.ANSI_RESET + "> ");
} else {
consoleReader.setPrompt(DEFAULT_PROMPT + "> ");
}
}
protected void installCompleters() {
for (ShellCommand command : commands) {
for (Completer completer : command.getCompleters()) {
consoleReader.addCompleter(completer);
}
}
}
protected Set<ShellCommand> findCommands() {
Set<ShellCommand> commands = factory.queryByClass(ShellCommand.class).findAsComponents();
HelpCommand helpCommand = new HelpCommand(commands);
commands = Sets.newLinkedHashSet(commands);
commands.add(helpCommand);
return commands;
}
public static void main(String[] args) throws Exception {
ConsoleReader consoleReader = new ConsoleReader();
RestxShell restxShell = new RestxShell(consoleReader);
if (args.length > 0) {
try {
restxShell.exec(Splitter.on("+").trimResults().split(Joiner.on(" ").join(args)));
} catch (CommandNotFoundException e) {
System.out.println("command not found: " + e.getLine());
System.exit(1);
}
} else {
restxShell.start();
}
}
public void restXProjectName(Optional<String> restXProjectName) {
this.restXProjectName = restXProjectName;
}
public Optional<String> restXProjectName() {
return restXProjectName;
}
public String version() {
return Version.getVersion("io.restx", "restx-shell");
}
public void download(final URL source, File destination) throws IOException {
final String name = source.toString();
println("downloading " + (name.length() <= 70 ? name : "[...]" + name.substring(name.length() - 65)));
URLConnection connection = source.openConnection();
final int total = connection.getContentLength();
final int[] progress = new int[]{0};
startProgress(name, total);
try (InputStream stream = connection.getInputStream();
final OutputStream out = new FileOutputStream(destination)) {
ByteStreams.readBytes(stream,
new ByteProcessor<Void>() {
public boolean processBytes(byte[] buffer, int offset, int length)
throws IOException {
out.write(buffer, offset, length);
progress[0] += length;
updateProgress(name, progress[0], total);
return true;
}
public Void getResult() {
return null;
}
});
endProgress(name);
}
}
public void endProgress(String name) throws IOException {
consoleReader.println();
}
public void startProgress(String name, long total) throws IOException {
updateProgress(name, 0, total);
}
public void updateProgress(String name, long progress, long total) throws IOException {
int barWidth = 70;
StringBuilder line = new StringBuilder();
if (progress >= total) {
line.append("[").append(Strings.repeat("=", barWidth)).append("]");
} else {
int p = (int) Math.min(progress * barWidth / total, barWidth - 1);
line.append("[").append(Strings.repeat("=", p)).append(">").append(Strings.repeat(" ", barWidth - p - 1)).append("]");
}
line.append(String.format(" %3d", (progress >= total) ? (100) : (progress * 100 / total))).append("%");
consoleReader.print("" + RESET_LINE + line);
}
public static final class ExitShell extends RuntimeException { }
public static class AnsiCodes {
public static final String ANSI_RESET = "\u001B[0m";
public static final String ANSI_BLACK = "\u001B[30m";
public static final String ANSI_RED = "\u001B[31m";
public static final String ANSI_GREEN = "\u001B[32m";
public static final String ANSI_YELLOW = "\u001B[33m";
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_PURPLE = "\u001B[35m";
public static final String ANSI_CYAN = "\u001B[36m";
public static final String ANSI_WHITE = "\u001B[37m";
}
public static interface WatchListener {
public void onEvent(RestxShell shell, WatchEvent.Kind<?> kind, Path path);
}
public static enum ExecMode {
INTERACTIVE, BATCH
}
private static class CommandNotFoundException extends RuntimeException {
private final String line;
public CommandNotFoundException(String line) {
super("command not found: " + line);
this.line = line;
}
public String getLine() {
return line;
}
@Override
public String toString() {
return "CommandNotFoundException{" +
"line='" + line + '\'' +
'}';
}
}
}