/* * Joinery -- Data frames for Java * Copyright (c) 2014, 2015 IBM Corp. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package joinery.impl; import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.List; import jline.console.ConsoleReader; import jline.console.completer.Completer; import joinery.DataFrame; import joinery.impl.js.DataFrameAdapter; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import org.mozilla.javascript.NativeJavaArray; import org.mozilla.javascript.NativeJavaClass; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.WrappedException; public class Shell { public static Object repl(final List<DataFrame<Object>> frames) throws IOException { return repl(System.in, frames); } public static Object repl(final InputStream input, final List<DataFrame<Object>> frames) throws IOException { return new Repl(input, frames).run(); } private static class Repl extends ScriptableObject { private static final long serialVersionUID = 1L; private static final String PROMPT = "> "; private static final String PROMPT_CONTINUE = " "; private static final String LAST_VALUE_NAME = "_"; private static final String FILENAME = "<shell>"; private final String NEWLINE = System.getProperty("line.separator"); private final InputStream input; private final List<DataFrame<Object>> frames; private final boolean interactive = System.console() != null; private transient boolean quit = false; private transient int statement = 1; private Repl(final InputStream input, final List<DataFrame<Object>> frames) { this.input = input; this.frames = frames; } @Override public String getClassName() { return "shell"; } public Object run() throws IOException { Object result = null; final Console console = console(input); final Context ctx = Context.enter(); if (interactive) { final Package pkg = DataFrame.class.getPackage(); System.out.printf("# %s -- %s, %s-%s\n# %s, %s, %s\n# %s\n", pkg.getSpecificationTitle(), pkg.getImplementationTitle(), pkg.getSpecificationVersion(), pkg.getImplementationVersion(), System.getProperty("java.vm.name"), System.getProperty("java.vendor"), System.getProperty("java.version"), ctx.getImplementationVersion() ); } try { ctx.initStandardObjects(this); // add functions defineFunctionProperties( new String[] { "print", "quit", "source" }, getClass(), ScriptableObject.DONTENUM ); // make data frame easily available try { ScriptableObject.defineClass(this, DataFrameAdapter.class); } catch (IllegalAccessException | InstantiationException | InvocationTargetException ex) { throw new RuntimeException(ex); } // make data frame classes available as well for (final Class<?> cls : DataFrame.class.getDeclaredClasses()) { put(cls.getSimpleName(), this, new NativeJavaClass(this, cls)); } // make argument frames available final DataFrameAdapter[] array = new DataFrameAdapter[frames.size()]; for (int i = 0; i < frames.size(); i++) { final DataFrame<Object> df = frames.get(i); array[i] = new DataFrameAdapter(ctx.newObject(this, df.getClass().getSimpleName()), df); } put("frames", this, new NativeJavaArray(this, array)); String expr = null; while (!quit && (expr = read(console)) != null) { try { result = eval(expr); if (result != Context.getUndefinedValue()) { // store last value for reference put(LAST_VALUE_NAME, this, result); if (interactive) { System.out.println(Context.toString(result)); } } } catch (final Exception ex) { if (interactive) { if (ex instanceof WrappedException) { WrappedException.class.cast(ex).getCause().printStackTrace();; } else { ex.printStackTrace(); } } result = ex; } } } finally { Context.exit(); } return Context.jsToJava(result, Object.class); } public String read(final Console console) throws IOException { final Context ctx = Context.getCurrentContext(); final StringBuilder buffer = new StringBuilder(); String line = null; if ((line = console.readLine(PROMPT)) != null) { // apply continued lines to last value if (line.startsWith(".") && has(LAST_VALUE_NAME, this)) { buffer.append(LAST_VALUE_NAME); } // read lines a complete statement is found or eof buffer.append(line); while (!ctx.stringIsCompilableUnit(buffer.toString()) && (line = console.readLine(PROMPT_CONTINUE)) != null) { buffer.append(NEWLINE).append(line); } return buffer.toString(); } return null; } public Object eval(final String source) { final Context ctx = Context.getCurrentContext(); return ctx.evaluateString(this, source, FILENAME, statement++, null); } @SuppressWarnings("unused") public static void print(final Context ctx, final Scriptable object, final Object[] args, final Function func) { for (int i = 0; i < args.length; i++) { if (i > 0) { System.out.print(" "); } System.out.print(Context.toString(args[i])); } System.out.println(); } @SuppressWarnings("unused") public void quit() { quit = true; } @SuppressWarnings("unused") public static void source(final Context ctx, final Scriptable object, final Object[] args, final Function func) throws Exception { final Repl repl = Repl.class.cast(object); for (int i = 0; i < args.length; i++) { final String file = Context.toString(args[i]); final SourceReader source = repl.new SourceReader(file); String expr = null; while ((expr = repl.read(source)) != null) { final Object result = repl.eval(expr); if (result != Context.getUndefinedValue()) { // store last value for reference repl.put(LAST_VALUE_NAME, repl, result); if (repl.interactive) { System.out.println(Context.toString(result)); } } } } } private Console console(final InputStream input) throws IOException { if (interactive) { try { return new JLineConsole(); } catch (final NoClassDefFoundError ignored) { } } return new Console(new BufferedReader(new InputStreamReader(input))); } private class Console { private final BufferedReader reader; private Console() throws IOException { this.reader = null; } private Console(final BufferedReader reader) throws IOException { this.reader = reader; } public String readLine(final String prompt) throws IOException { if (interactive) { System.out.print(prompt); } return reader.readLine(); } } private class SourceReader extends Console { private SourceReader(final String file) throws IOException { super(new BufferedReader(new FileReader(file))); } @Override public String readLine(final String prompt) throws IOException { final String line = super.readLine(""); if (interactive && line != null) { System.out.printf("%s%s\n", prompt, line, NEWLINE); } return line; } } private class JLineConsole extends Console implements Runnable, Completer { private final ConsoleReader console; private JLineConsole() throws IOException { console = new ConsoleReader(); console.addCompleter(this); console.setExpandEvents(false); Runtime.getRuntime().addShutdownHook(new Thread(this)); } @Override public String readLine(final String prompt) throws IOException { console.setPrompt(prompt); return console.readLine(); } @Override public void run() { try { console.getTerminal().restore(); } catch (final Exception ignored) { } } @Override public int complete(final String buffer, final int cursor, final List<CharSequence> candidates) { final String expr = buffer.substring(buffer.lastIndexOf(' ') + 1); final int dot = expr.lastIndexOf('.') + 1; if (dot > 1) { final String var = expr.substring(0, dot - 1); final String prefix = expr.substring(dot); final Object value = get(var, Repl.this); if (value != NOT_FOUND) { if (value instanceof ScriptableObject) { final Object[] ids = ScriptableObject.class.cast(value).getAllIds(); Arrays.sort(ids); for (final Object id : ids) { if (id instanceof String && String.class.cast(id).startsWith(prefix)) { candidates.add(String.class.cast(id)); } } } if (!candidates.isEmpty()) { return dot; } } } return buffer.length(); } } } }