All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.scijava.script.DefaultScriptInterpreter Maven / Gradle / Ivy

/*
 * #%L
 * SciJava Common shared library for SciJava software.
 * %%
 * Copyright (C) 2009 - 2017 Board of Regents of the University of
 * Wisconsin-Madison, Broad Institute of MIT and Harvard, Max Planck
 * Institute of Molecular Cell Biology and Genetics, University of
 * Konstanz, and KNIME GmbH.
 * %%
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * #L%
 */
package org.scijava.script;

import java.lang.reflect.Method;

import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;

import org.scijava.log.LogService;
import org.scijava.plugin.Parameter;
import org.scijava.prefs.PrefService;
import org.scijava.util.LastRecentlyUsed;

/**
 * The default implementation of a {@link ScriptInterpreter}.
 * 

* Credit to Jason Sachs for the multi-line evaluation (see * his post on StackOverflow). *

* * @author Johannes Schindelin * @author Curtis Rueden */ public class DefaultScriptInterpreter implements ScriptInterpreter { private final ScriptLanguage language; private final ScriptEngine engine; private final History history; @Parameter(required = false) private PrefService prefs; @Parameter(required = false) private LogService log; private final StringBuilder buffer; private int pendingLineCount; private boolean expectingMoreInput; /** * @deprecated Use {@link #DefaultScriptInterpreter(ScriptLanguage)} instead. */ @Deprecated @SuppressWarnings("unused") public DefaultScriptInterpreter(final PrefService prefs, final ScriptService scriptService, final ScriptLanguage language) { this(language); } /** * Creates a new script interpreter for the given script language. * * @param language {@link ScriptLanguage} of the interpreter */ public DefaultScriptInterpreter(final ScriptLanguage language) { this(language, null); } /** * Creates a new script interpreter for the given script language, using the * specified script engine. * * @param language {@link ScriptLanguage} of the interpreter * @param engine {@link ScriptEngine} to use, or null for the specified * language's default engine */ public DefaultScriptInterpreter(final ScriptLanguage language, final ScriptEngine engine) { language.getContext().inject(this); this.language = language; this.engine = engine == null ? language.getScriptEngine() : engine; history = prefs == null ? null : new History(prefs, this.engine.getClass().getName()); readHistory(); buffer = new StringBuilder(); reset(); } // -- ScriptInterpreter methods -- @Override public synchronized void readHistory() { if (history == null) return; history.read(); } @Override public synchronized void writeHistory() { if (history == null) return; history.write(); } @Override public synchronized String walkHistory(final String currentCommand, final boolean forward) { if (history == null) return currentCommand; history.replace(currentCommand); return forward ? history.next() : history.previous(); } @Override public Object eval(final String command) throws ScriptException { addToHistory(command); return engine.eval(command); } /** * {@inheritDoc} *

* This implementation from Jason Sachs uses the following strategy: *

*
    *
  • Keep a pending list of input lines not yet evaluated.
  • *
  • Try compiling (but not evaluating) the pending input lines. *
      *
    • If the compilation is OK, we may be able to execute pending input * lines.
    • *
    • If the compilation throws an exception, and there is an indication of * the position (line + column number) of the error, and this matches the end * of the pending input, then that's a clue that we're expecting more input, * so swallow the exception and wait for the next line.
    • *
    • Otherwise, we either don't know where the error is, or it happened * prior to the end of the pending input, so rethrow the exception.
    • *
    *
  • *
  • If we are not expecting any more input lines, and we only have one line * of pending input, then evaluate it and restart.
  • *
  • If we are not expecting any more input lines, and the last one is a * blank one, and we have more than one line of pending input, then evaluate * it and restart. Python's interactive shell seems to do this.
  • *
  • Otherwise, keep reading input lines.
  • *
*

* This helps avoid certain problems: *

*
    *
  • users getting annoyed having to enter extra blank lines after * single-line inputs
  • *
  • users entering a long multi-line statement and only find out after the * fact that there was a syntax error in the 2nd line.
  • *
*

* For further details, see SO * #5584674. *

*/ @Override public Object interpret(final String line) throws ScriptException { if (line.isEmpty()) { if (!shouldEvaluatePendingInput(true)) return MORE_INPUT_PENDING; } if (pendingLineCount > 0) buffer.append("\n"); pendingLineCount++; buffer.append(line); final String command = buffer.toString(); if (!(engine instanceof Compilable)) { // Not a compilable language. // Evaluate directly, with no multi-line statements possible. try { return eval(command); } finally { reset(); } } final CompiledScript cs = tryCompiling(command, // getPendingLineCount(), line.length()); if (cs == null) { // Command did not compile. // Assume it is incomplete and wait for more input on the next line. return MORE_INPUT_PENDING; } if (!shouldEvaluatePendingInput(line.isEmpty())) { // We are still expecting more input. return MORE_INPUT_PENDING; } // Command is complete; evaluate the compiled script. try { addToHistory(command); return cs.eval(); } finally { reset(); } } @Override public void reset() { buffer.setLength(0); pendingLineCount = 0; expectingMoreInput = false; } @Override public ScriptLanguage getLanguage() { return language; } @Override public ScriptEngine getEngine() { return engine; } @Override public Bindings getBindings() { return engine.getBindings(ScriptContext.ENGINE_SCOPE); } @Override public boolean isReady() { return buffer.length() == 0; } @Override public boolean isExpectingMoreInput() { return expectingMoreInput; } // -- Helper methods -- private void addToHistory(final String command) { if (history != null) history.add(command); } /** * @return number of lines pending execution */ private int getPendingLineCount() { return pendingLineCount; } /** * @param lineIsEmpty whether the last line is empty * @return whether we should evaluate the pending input. The default behavior * is to evaluate if we only have one line of input, or if the user * enters a blank line. This behavior should be overridden where * appropriate. */ private boolean shouldEvaluatePendingInput(final boolean lineIsEmpty) { if (isExpectingMoreInput()) return false; return getPendingLineCount() == 1 || lineIsEmpty; } private CompiledScript tryCompiling(final String string, final int lineCount, final int lastLineLength) throws ScriptException { CompiledScript result = null; try { final Compilable c = (Compilable) engine; result = c.compile(string); } catch (final ScriptException se) { boolean rethrow = true; if (se.getCause() != null) { final Integer col = columnNumber(se); final Integer line = lineNumber(se); // swallow the exception if it occurs at the last character // of the input (we may need to wait for more lines) if (isLastCharacter(col, line, lineCount, lastLineLength)) { rethrow = false; } else if (log != null && log.isDebug()) { final String msg = se.getCause().getMessage(); log.debug("L" + line + " C" + col + "(" + lineCount + "," + lastLineLength + "): " + msg); log.debug("in '" + string + "'"); } } if (rethrow) { reset(); throw se; } } expectingMoreInput = result == null; return result; } private boolean isLastCharacter(final Integer col, final Integer line, final int lineCount, final int lastLineLength) { if (col == null || line == null) return false; final int colNo = col.intValue(), lineNo = line.intValue(); return lineNo == lineCount && colNo == lastLineLength || lineNo == lineCount + 1 && colNo == 0; } private Integer columnNumber(final ScriptException se) { if (se.getColumnNumber() >= 0) return se.getColumnNumber(); return callMethod(se.getCause(), "columnNumber", Integer.class); } private Integer lineNumber(final ScriptException se) { if (se.getLineNumber() >= 0) return se.getLineNumber(); return callMethod(se.getCause(), "lineNumber", Integer.class); } private static Method getMethod(final Object object, final String methodName) { try { return object.getClass().getMethod(methodName); } catch (final NoSuchMethodException e) { // gulp return null; } } private static T callMethod(final Object object, final String methodName, final Class cl) { try { final Method m = getMethod(object, methodName); if (m != null) { final Object result = m.invoke(object); return cl.cast(result); } } catch (final Exception e) { e.printStackTrace(); } return null; } // -- Helper classes -- /** Container for a script language's interpreter history. */ private static class History { @SuppressWarnings("unused") protected static final long serialVersionUID = 2L; private static final String PREFIX = "History."; private final int MAX_ENTRIES = 1000; private final PrefService prefs; private final String name; private final LastRecentlyUsed entries = new LastRecentlyUsed<>(MAX_ENTRIES); private String currentCommand = ""; private int position = -1; /** * Constructs a history object for a given scripting language. * * @param name the name of the scripting language */ public History(final PrefService prefs, final String name) { this.prefs = prefs; this.name = name; } /** * Read back a persisted history. */ public void read() { entries.clear(); for (final String item : prefs.getIterable(getClass(), PREFIX + name)) { entries.addToEnd(item); } } /** * Persist the history. * * @see PrefService */ public void write() { prefs.putIterable(getClass(), entries, PREFIX + name); } /** * Adds the most recently issued command. * * @param command the most recent command to add to the history */ public void add(final String command) { entries.add(command); position = -1; currentCommand = ""; } public boolean replace(final String command) { if (position < 0) { currentCommand = command; return false; } return entries.replace(position, command); } /** * Navigates to the next (more recent) command. *

* This method wraps around, i.e. it returns {@code null} when there is no * more-recent command in the history. *

* * @return the next command */ public String next() { position = entries.next(position); return position < 0 ? currentCommand : entries.get(position); } /** * Navigates to the previous (i.e less recent) command. *

* This method wraps around, i.e. it returns {@code null} when there is no * less-recent command in the history. *

* * @return the previous command */ public String previous() { position = entries.previous(position); return position < 0 ? currentCommand : entries.get(position); } @Override public String toString() { final StringBuilder builder = new StringBuilder(); int pos = -1; for (;;) { pos = entries.previous(pos); if (pos < 0) break; if (builder.length() > 0) builder.append(" -> "); if (this.position == pos) builder.append("["); builder.append(entries.get(pos)); if (this.position == pos) builder.append("]"); } return builder.toString(); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy