blob: 8dcb272e15bc630ff83311f0fa91fbb8f37e979d [file] [log] [blame]
/*
* 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.tinkerpop.gremlin.console
import jline.TerminalFactory
import jline.console.history.FileHistory
import org.apache.commons.cli.Option
import org.apache.tinkerpop.gremlin.console.commands.GremlinSetCommand
import org.apache.tinkerpop.gremlin.console.commands.InstallCommand
import org.apache.tinkerpop.gremlin.console.commands.PluginCommand
import org.apache.tinkerpop.gremlin.console.commands.RemoteCommand
import org.apache.tinkerpop.gremlin.console.commands.SubmitCommand
import org.apache.tinkerpop.gremlin.console.commands.UninstallCommand
import org.apache.tinkerpop.gremlin.console.plugin.PluggedIn
import org.apache.tinkerpop.gremlin.groovy.loaders.GremlinLoader
import org.apache.tinkerpop.gremlin.groovy.plugin.GremlinPlugin
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalExplanation
import org.apache.tinkerpop.gremlin.util.Gremlin
import org.apache.tinkerpop.gremlin.util.iterator.ArrayIterator
import org.codehaus.groovy.tools.shell.AnsiDetector
import org.codehaus.groovy.tools.shell.ExitNotification
import org.codehaus.groovy.tools.shell.Groovysh
import org.codehaus.groovy.tools.shell.IO
import org.codehaus.groovy.tools.shell.InteractiveShellRunner
import org.codehaus.groovy.tools.shell.commands.SetCommand
import org.codehaus.groovy.tools.shell.util.HelpFormatter
import org.codehaus.groovy.tools.shell.util.Preferences
import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiConsole
import java.util.prefs.PreferenceChangeEvent
import java.util.prefs.PreferenceChangeListener
/**
* @author Stephen Mallette (http://stephen.genoprime.com)
*/
class Console {
static {
// this is necessary so that terminal doesn't lose focus to AWT
System.setProperty("java.awt.headless", "true")
// must be called before IO(), since it modifies System.in
// Install the system adapters, replaces System.out and System.err
// Must be called before using IO(), because IO stores refs to System.out and System.err
AnsiConsole.systemInstall()
// Register jline ansi detector
Ansi.setDetector(new AnsiDetector())
Ansi.enabled = true
}
public static final String PREFERENCE_ITERATION_MAX = "max-iteration"
private static final int DEFAULT_ITERATION_MAX = 100
private static int maxIteration = DEFAULT_ITERATION_MAX
private static final String STANDARD_INPUT_PROMPT = "gremlin> "
private static final String STANDARD_RESULT_PROMPT = "==>"
private static final String IMPORT_SPACE = "import "
private static final String IMPORT_STATIC_SPACE = "import static "
private static final String NULL = "null"
private static final String ELLIPSIS = "..."
private Iterator tempIterator = Collections.emptyIterator()
private final IO io
private final Groovysh groovy
private final boolean interactive
/**
* @deprecated As of release 3.2.1.
*/
@Deprecated
public Console(final String initScriptFile) {
this(new IO(System.in, System.out, System.err), initScriptFile.size() != null ? [initScriptFile] : null, true)
}
public Console(final IO io, final List<String> scriptAndArgs, final boolean interactive) {
this.io = io
this.interactive = interactive
if (!io.quiet) {
io.out.println()
io.out.println(" \\,,,/")
io.out.println(" (o o)")
io.out.println("-----oOOo-(3)-oOOo-----")
}
maxIteration = Integer.parseInt(Preferences.get(PREFERENCE_ITERATION_MAX, Integer.toString(DEFAULT_ITERATION_MAX)))
Preferences.addChangeListener(new PreferenceChangeListener() {
@Override
void preferenceChange(PreferenceChangeEvent evt) {
if (evt.key == PREFERENCE_ITERATION_MAX)
maxIteration = Integer.parseInt(evt.newValue)
}
})
final Mediator mediator = new Mediator(this)
// make sure that remotes are closed if console takes a ctrl-c
addShutdownHook { mediator.close() }
groovy = new GremlinGroovysh(mediator)
def commandsToRemove = groovy.getRegistry().commands().findAll { it instanceof SetCommand }
commandsToRemove.each { groovy.getRegistry().remove(it) }
groovy.register(new GremlinSetCommand(groovy))
groovy.register(new UninstallCommand(groovy, mediator))
groovy.register(new InstallCommand(groovy, mediator))
groovy.register(new PluginCommand(groovy, mediator))
groovy.register(new RemoteCommand(groovy, mediator))
groovy.register(new SubmitCommand(groovy, mediator))
// hide output temporarily while imports execute
showShellEvaluationOutput(false)
// add the default imports
new ConsoleImportCustomizerProvider().getCombinedImports().stream()
.collect { IMPORT_SPACE + it }.each { groovy.execute(it) }
new ConsoleImportCustomizerProvider().getCombinedStaticImports().stream()
.collect { IMPORT_STATIC_SPACE + it }.each { groovy.execute(it) }
final InteractiveShellRunner runner = new InteractiveShellRunner(groovy, handlePrompt)
runner.setErrorHandler(handleError)
try {
final FileHistory history = new FileHistory(new File(ConsoleFs.HISTORY_FILE))
groovy.setHistory(history)
runner.setHistory(history)
} catch (IOException ignored) {
io.err.println("Unable to create history file: " + ConsoleFs.HISTORY_FILE)
}
GremlinLoader.load()
// check for available plugins. if they are in the "active" plugins strategies then "activate" them
def activePlugins = Mediator.readPluginState()
ServiceLoader.load(GremlinPlugin.class, groovy.getInterp().getClassLoader()).each { plugin ->
if (!mediator.availablePlugins.containsKey(plugin.class.name)) {
def pluggedIn = new PluggedIn(plugin, groovy, io, false)
mediator.availablePlugins.put(plugin.class.name, pluggedIn)
if (activePlugins.contains(plugin.class.name)) {
pluggedIn.activate()
if (!io.quiet)
io.out.println("plugin activated: " + plugin.getName())
}
}
}
// remove any "uninstalled" plugins from plugin state as it means they were installed, activated, but not
// deactivated, and are thus hanging about
mediator.writePluginState()
try {
// if the init script contains :x command it will throw an ExitNotification so init script execution
// needs to appear in the try/catch
if (scriptAndArgs != null && scriptAndArgs.size() > 0) executeInShell(scriptAndArgs)
// start iterating results to show as output
showShellEvaluationOutput(true)
runner.run()
} catch (ExitNotification ignored) {
// occurs on exit
} catch (Throwable t) {
t.printStackTrace()
} finally {
// shutdown hook defined above will kill any open remotes
System.exit(0)
}
}
def showShellEvaluationOutput(final boolean show) {
if (show)
groovy.setResultHook(handleResultIterate)
else
groovy.setResultHook(handleResultShowNothing)
}
private def handlePrompt = { interactive ? STANDARD_INPUT_PROMPT : "" }
private def handleResultShowNothing = { args -> null }
private def handleResultIterate = { result ->
try {
// necessary to save persist history to file
groovy.getHistory().flush()
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e)
}
while (true) {
if (this.tempIterator.hasNext()) {
int counter = 0;
while (this.tempIterator.hasNext() && (maxIteration == -1 || counter < maxIteration)) {
final Object object = this.tempIterator.next()
io.out.println(buildResultPrompt() + ((null == object) ? NULL : object.toString()))
counter++;
}
if (this.tempIterator.hasNext())
io.out.println(ELLIPSIS);
this.tempIterator = Collections.emptyIterator();
return null
} else {
try {
// if the result is an empty iterator then the tempIterator needs to be set to one, as a
// future assignment to the strategies that produced the iterator will maintain that reference
// and try to iterate it above. in other words, this:
//
// x =[]
// x << "test"
//
// would throw a ConcurrentModificationException because the assignment of x to the tempIterator
// on the first line would maintain a reference on the next result iteration call and would
// drop into the other part of this if statement and throw.
if (result instanceof Iterator) {
this.tempIterator = (Iterator) result
if (!this.tempIterator.hasNext()) {
this.tempIterator = Collections.emptyIterator();
return null
}
} else if (result instanceof Iterable) {
this.tempIterator = ((Iterable) result).iterator()
if (!this.tempIterator.hasNext()) {
this.tempIterator = Collections.emptyIterator();
return null
}
} else if (result instanceof Object[]) {
this.tempIterator = new ArrayIterator((Object[]) result)
if (!this.tempIterator.hasNext()) {
this.tempIterator = Collections.emptyIterator();
return null
}
} else if (result instanceof Map) {
this.tempIterator = ((Map) result).entrySet().iterator()
if (!this.tempIterator.hasNext()) {
this.tempIterator = Collections.emptyIterator();
return null
}
} else if (result instanceof TraversalExplanation) {
final int width = TerminalFactory.get().getWidth();
io.out.println(buildResultPrompt() + result.prettyPrint(width < 20 ? 80 : width))
return null
} else {
io.out.println(buildResultPrompt() + ((null == result) ? NULL : result.toString()))
return null
}
} catch (final Exception e) {
this.tempIterator = Collections.emptyIterator()
throw e
}
}
}
}
private def handleError = { err ->
this.tempIterator = Collections.emptyIterator();
if (err instanceof Throwable) {
try {
final Throwable e = (Throwable) err
String message = e.getMessage()
if (null != message) {
message = message.replace("startup failed:", "")
io.err.println(message.trim())
} else {
io.err.println(e)
}
if (interactive) {
io.err.print("Display stack trace? [yN] ")
io.err.flush()
String line = new BufferedReader(io.in).readLine()
if (null == line)
line = ""
io.err.print(line.trim())
io.err.println()
if (line.trim().equals("y") || line.trim().equals("Y")) {
e.printStackTrace(io.err)
}
} else {
e.printStackTrace(io.err)
System.exit(1)
}
} catch (Exception ignored) {
io.err.println("An undefined error has occurred: " + err)
if (!interactive) System.exit(1)
}
} else {
io.err.println("An undefined error has occurred: " + err.toString())
if (!interactive) System.exit(1)
}
return null
}
private static String buildResultPrompt() {
final String groovyshellProperty = System.getProperty("gremlin.prompt")
if (groovyshellProperty != null)
return groovyshellProperty
final String groovyshellEnv = System.getenv("GREMLIN_PROMPT")
if (groovyshellEnv != null)
return groovyshellEnv
return STANDARD_RESULT_PROMPT
}
private void executeInShell(final List<String> scriptAndArgs) {
final String scriptFile = scriptAndArgs[0]
try {
// check if this script comes with arguments. if so then set them up in an "args" bundle
if (scriptAndArgs.size() > 1) {
List<String> args = scriptAndArgs.subList(1, scriptAndArgs.size())
groovy.execute("args = [\"" + args.join('\",\"') + "\"]")
} else {
groovy.execute("args = []")
}
final File file = new File(scriptFile)
int lineNumber = 0
def lines = file.readLines()
for (String line : lines) {
try {
lineNumber++
groovy.execute(line)
} catch (Exception ex) {
io.err.println("Error in $scriptFile at [$lineNumber: $line] - ${ex.message}")
if (interactive)
break
else {
ex.printStackTrace(io.err)
System.exit(1)
}
}
}
if (!interactive) System.exit(0)
} catch (FileNotFoundException ignored) {
io.err.println("Gremlin file not found at [$scriptFile].")
if (!interactive) System.exit(1)
} catch (Exception ex) {
io.err.println("Failure processing Gremlin script [$scriptFile] - ${ex.message}")
if (!interactive) System.exit(1)
}
}
public static void main(final String[] args) {
// need to do some up front processing to try to support "bin/gremlin.sh init.groovy" until this deprecated
// feature can be removed. ultimately this should be removed when a breaking change can go in
IO io = new IO(System.in, System.out, System.err)
if (args.length == 1 && !args[0].startsWith("-"))
new Console(io, [args[0]], true)
final CliBuilder cli = new CliBuilder(usage: 'gremlin.sh [options] [...]', formatter: new HelpFormatter(), stopAtNonOption: false)
// note that the inclusion of -l is really a setting handled by gremlin.sh and not by Console class itself.
// it is mainly listed here for informational purposes when the user starts things up with -h
cli.with {
h(longOpt: 'help', "Display this help message")
v(longOpt: 'version', "Display the version")
l("Set the logging level of components that use standard logging output independent of the Console")
V(longOpt: 'verbose', "Enable verbose Console output")
Q(longOpt: 'quiet', "Suppress superfluous Console output")
D(longOpt: 'debug', "Enabled debug Console output")
i(longOpt: 'interactive', argName: "SCRIPT ARG1 ARG2 ...", args: Option.UNLIMITED_VALUES, valueSeparator: ' ' as char, "Execute the specified script and leave the console open on completion")
e(longOpt: 'execute', argName: "SCRIPT ARG1 ARG2 ...", args: Option.UNLIMITED_VALUES, valueSeparator: ' ' as char, "Execute the specified script (SCRIPT ARG1 ARG2 ...) and close the console on completion")
}
OptionAccessor options = cli.parse(args)
if (options == null) {
// CliBuilder prints error, but does not exit
System.exit(22) // Invalid Args
}
if (options.h) {
cli.usage()
System.exit(0)
}
if (options.v) {
println("gremlin " + Gremlin.version())
System.exit(0)
}
if (options.V) io.verbosity = IO.Verbosity.VERBOSE
if (options.D) io.verbosity = IO.Verbosity.DEBUG
if (options.Q) io.verbosity = IO.Verbosity.QUIET
// override verbosity if not explicitly set and -e is used
if (options.e && (!options.V && !options.D && !options.Q))
io.verbosity = IO.Verbosity.QUIET
if (options.i && options.e) {
println("-i and -e options are mutually exclusive - provide one or the other")
System.exit(0)
}
List<String> scriptAndArgs = options.e ?
(options.es != null && options.es ? options.es : null) :
(options.is != null && options.is ? options.is : null)
new Console(io, scriptAndArgs, !options.e)
}
}