blob: aeec63ab5533bf4d7d9f6ac720875ecd3eca60df [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.BytecodeCommand
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.groovy.loaders.GremlinLoader
import org.apache.tinkerpop.gremlin.jsr223.CoreGremlinPlugin
import org.apache.tinkerpop.gremlin.jsr223.GremlinPlugin
import org.apache.tinkerpop.gremlin.jsr223.ImportCustomizer
import org.apache.tinkerpop.gremlin.jsr223.console.RemoteException
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalExplanation
import org.apache.tinkerpop.gremlin.structure.Edge
import org.apache.tinkerpop.gremlin.structure.T
import org.apache.tinkerpop.gremlin.structure.Vertex
import org.apache.tinkerpop.gremlin.util.Gremlin
import org.apache.tinkerpop.gremlin.util.iterator.ArrayIterator
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.fusesource.jansi.Ansi
import sun.misc.Signal
import sun.misc.SignalHandler
import java.util.concurrent.atomic.AtomicBoolean
/**
* @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")
Colorizer.installAnsi()
}
private static final String ELLIPSIS = "..."
private Iterator tempIterator = Collections.emptyIterator()
private final IO io
private final Groovysh groovy
private final boolean interactive
public Console(final IO io, final List<List<String>> scriptsAndArgs, final boolean interactive) {
this.io = io
this.interactive = interactive
if (!io.quiet) {
io.out.println()
io.out.println(" " + Colorizer.render(Preferences.gremlinColor, "\\,,,/"))
io.out.println(" " + Colorizer.render(Preferences.gremlinColor, "(o o)"))
io.out.println("" + Colorizer.render(Preferences.gremlinColor, "-----oOOo-(3)-oOOo-----"))
}
final Mediator mediator = new Mediator(this)
// make sure that remotes are closed on jvm shutdown
addShutdownHook { mediator.close() }
// try to grab ctrl+c to interrupt an evaluation.
final Thread main = Thread.currentThread()
Signal.handle(new Signal("INT"), new SignalHandler() {
@Override
void handle(final Signal signal) {
if (mediator.evaluating.get()) {
io.out.println("Execution interrupted by ctrl+c")
main.interrupt()
}
}
})
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))
groovy.register(new BytecodeCommand(groovy, mediator))
// hide output temporarily while imports execute
showShellEvaluationOutput(false)
def imports = (ImportCustomizer) CoreGremlinPlugin.instance().getCustomizers("gremlin-groovy").get()[0]
imports.getClassPackages().collect { Mediator.IMPORT_SPACE + it.getName() + Mediator.IMPORT_WILDCARD }.each { groovy.execute(it) }
imports.getMethodClasses().collect { Mediator.IMPORT_STATIC_SPACE + it.getCanonicalName() + Mediator.IMPORT_WILDCARD}.each{ groovy.execute(it) }
imports.getEnumClasses().collect { Mediator.IMPORT_STATIC_SPACE + it.getCanonicalName() + Mediator.IMPORT_WILDCARD}.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(Colorizer.render(Preferences.errorColor, "Unable to create history file: " + ConsoleFs.HISTORY_FILE))
}
GremlinLoader.load()
// check for available plugins on the path and track them by plugin class name
def activePlugins = Mediator.readPluginState()
ServiceLoader.load(GremlinPlugin, groovy.getInterp().getClassLoader()).each { plugin ->
if (!mediator.availablePlugins.containsKey(plugin.class.name)) {
def pluggedIn = new PluggedIn((GremlinPlugin) plugin, groovy, io, false)
mediator.availablePlugins.put(plugin.class.name, pluggedIn)
}
}
// if there are active plugins then initialize them in the order that they are listed
activePlugins.each { pluginName ->
def pluggedIn = mediator.availablePlugins[pluginName]
pluggedIn.activate()
if (!io.quiet)
io.out.println(Colorizer.render(Preferences.infoColor, "plugin activated: " + pluggedIn.getPlugin().getName()))
}
// remove any "uninstalled" plugins from plugin state as it means they were installed, activated, but not
// deactivated, and are thus hanging about (e.g. user deleted the plugin directories to uninstall). checking
// the number of expected active plugins from the plugins.txt file against the number activated on startup
// should be enough to tell if something changed which would justify that the file be re-written
if (activePlugins.size() != mediator.activePlugins().size())
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 (scriptsAndArgs != null && !scriptsAndArgs.isEmpty()) executeInShell(scriptsAndArgs)
// 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 = {
if (interactive) {
int lineNo = groovy.buffers.current().size()
if (lineNo > 0 ) {
String lineStr = lineNo.toString() + ">"
int pad = Preferences.inputPrompt.length()
return Colorizer.render(Preferences.inputPromptColor, lineStr.toString().padLeft(pad, '.') + ' ')
} else {
return Colorizer.render(Preferences.inputPromptColor, Preferences.inputPrompt + ' ')
}
} else {
return ""
}
}
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) {
// give ctrl+c a chance
Thread.yield()
// if this is true then ctrl+c was triggered
if (Thread.interrupted()) {
this.tempIterator = Collections.emptyIterator()
return null
}
if (this.tempIterator.hasNext()) {
int counter = 0
while (this.tempIterator.hasNext() && (Preferences.maxIteration == -1 || counter < Preferences.maxIteration)) {
// give ctrl+c a chance
Thread.yield()
// if this is true then ctrl+c was triggered
if (Thread.interrupted()) {
this.tempIterator = Collections.emptyIterator()
return null
}
printResult(tempIterator.next())
counter++
}
if (this.tempIterator.hasNext())
io.out.println(Colorizer.render(Preferences.resultPromptColor,ELLIPSIS))
this.tempIterator = Collections.emptyIterator()
break
} 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(Colorizer.render(Preferences.resultPromptColor,(buildResultPrompt() + result.prettyPrint(width < 20 ? 80 : width))))
return null
} else {
printResult(result)
return null
}
} catch (final Exception e) {
this.tempIterator = Collections.emptyIterator()
throw e
}
}
}
}
def printResult(def object) {
final String prompt = Colorizer.render(Preferences.resultPromptColor, buildResultPrompt())
// if preference is set to empty string then don't print any result
if (object != null) {
io.out.println(prompt + colorizeResult(object))
} else {
if (!Preferences.emptyResult.isEmpty()) {
io.out.println(prompt + Preferences.emptyResult)
}
}
}
def colorizeResult = { object ->
if (object instanceof Vertex) {
return Colorizer.render(Preferences.vertexColor, object.toString())
} else if (object instanceof Edge) {
return Colorizer.render(Preferences.edgeColor, object.toString())
} else if (object instanceof Iterable) {
List<String> buf = new ArrayList<>()
def pathIter = object.iterator()
while (pathIter.hasNext()) {
Object n = pathIter.next()
buf.add(colorizeResult(n))
}
return ("[" + buf.join(",") + "]")
} else if (object instanceof Map) {
List<String> buf = new ArrayList<>()
object.each{k, v ->
buf.add(colorizeResult(k) + ":" + colorizeResult(v))
}
return ("[" + buf.join(",") + "]")
} else if (object instanceof String) {
return Colorizer.render(Preferences.stringColor, object)
} else if (object instanceof Number) {
return Colorizer.render(Preferences.numberColor, object)
} else if (object instanceof T) {
return Colorizer.render(Preferences.tColor, object)
} else {
return object.toString()
}
}
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(Colorizer.render(Preferences.errorColor, message.trim()))
} else {
io.err.println(Colorizer.render(Preferences.errorColor,e))
}
if (interactive) {
io.err.println(Colorizer.render(Preferences.infoColor,"Type ':help' or ':h' for help."))
io.err.print(Colorizer.render(Preferences.errorColor, "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")) {
if (err instanceof RemoteException && err.remoteStackTrace.isPresent()) {
io.err.print(err.remoteStackTrace.get())
io.err.flush()
} else {
e.printStackTrace(io.err)
}
}
} else {
e.printStackTrace(io.err)
System.exit(1)
}
} catch (Exception ignored) {
io.err.println(Colorizer.render(Preferences.errorColor, "An undefined error has occurred: " + err))
if (!interactive) System.exit(1)
}
} else {
io.err.println(Colorizer.render(Preferences.errorColor, "An undefined error has occurred: " + err.toString()))
if (!interactive) System.exit(1)
}
groovy.buffers.current().clear()
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 Preferences.resultPrompt
}
private void executeInShell(final List<List<String>> scriptsAndArgs) {
scriptsAndArgs.eachWithIndex { scriptAndArgs, idx ->
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 = []")
}
File file = new File(scriptFile)
if (!file.exists() && !file.isAbsolute()) {
final String userWorkingDir = System.getProperty("user.working_dir")
if (userWorkingDir != null) {
file = new File(userWorkingDir, scriptFile)
}
}
int lineNumber = 0
def lines = file.readLines()
for (String line : lines) {
try {
lineNumber++
groovy.execute(line)
} catch (Exception ex) {
io.err.println(Colorizer.render(Preferences.errorColor, "Error in $scriptFile at [$lineNumber: $line] - ${ex.message}"))
if (interactive)
break
else {
ex.printStackTrace(io.err)
System.exit(1)
}
}
}
} catch (FileNotFoundException ignored) {
io.err.println(Colorizer.render(Preferences.errorColor, "Gremlin file not found at [$scriptFile]."))
if (!interactive) System.exit(1)
} catch (Exception ex) {
io.err.println(Colorizer.render(Preferences.errorColor, "Failure processing Gremlin script [$scriptFile] - ${ex.message}"))
if (!interactive) System.exit(1)
}
}
if (!interactive) System.exit(0)
}
public static void main(final String[] args) {
Preferences.expandoMagic()
IO io = new IO(System.in, System.out, System.err)
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")
C(longOpt: 'color', "Disable use of ANSI colors")
}
OptionAccessor options = cli.parse(args)
if (options == null) {
// CliBuilder prints error, but does not exit
System.exit(22) // Invalid Args
}
if (options.C) {
Ansi.enabled = false
}
if (options.h) {
cli.usage()
System.exit(0)
}
if (options.v) {
if (args.length == 1 && !args[0].startsWith("-"))
new Console(io, [args[0]], true)
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)
}
def scriptAndArgs = parseArgs(options.e ? ["-e", "--execute"] : ["-i", "--interactive"], args, cli)
new Console(io, scriptAndArgs, !options.e)
}
/**
* Provides a bit of a hack around the limitations of the {@code CliBuilder}. This method directly parses the
* argument list to allow for multiple {@code -e} and {@code -i} values and parses such parameters into a list
* of lists where the inner list is a script file and its arguments.
*/
private static List<List<String>> parseArgs(final List<String> options, final String[] args, final CliBuilder cli) {
def parsed = []
def normalizedArgs = normalizeArgs(options, args)
for (int ix = 0; ix < normalizedArgs.length; ix++) {
if (normalizedArgs[ix] in options) {
// increment the counter to move past the option that was found. should now be positioned on the
// first argument to that option
ix++
def parsedSet = []
for (ix; ix < normalizedArgs.length; ix++) {
// this is a do nothing as there's no arguments to the option or it's the start of a new option
if (cli.options.options.any { "-" + it.opt == normalizedArgs[ix] || "--" + it.longOpt == normalizedArgs[ix] }) {
// rollback the counter now that we hit the next option
ix--
break
}
parsedSet << normalizedArgs[ix]
}
if (!parsedSet.isEmpty()) {
// check if the params were passed in with double quotes such that they arrive as a single arg
if (parsedSet.size() == 1)
parsed << parsedSet[0].toString().split(" ").toList()
else
parsed << parsedSet
}
}
}
return parsed
}
/**
* The {@code args} value contains the individual flagged parameters provided on the command line which may come
* with or without an "=" to separate the flag from the argument to the flag. This method normalizes these values
* to split the flag from the argument so that it can be evaluated in a consistent way by {@code parseArgs()}.
*/
private static def normalizeArgs(final List<String> options, final String[] args) {
return args.collect{ arg ->
// arguments that match -i/-e options should be normalized where long forms need to be split on "="
// and short forms need to have the "=" included with the argument
if (options.any{ arg.startsWith(it) }) {
return arg.matches("^-[e,i]=.*") ? [arg.substring(0, 2), arg.substring(2)] : arg.split("=", 2)
}
return arg
}.flatten().toArray()
}
}