3030 */
3131package org .scijava .script ;
3232
33+ import java .lang .reflect .Method ;
34+
35+ import javax .script .Bindings ;
36+ import javax .script .Compilable ;
37+ import javax .script .CompiledScript ;
38+ import javax .script .ScriptContext ;
3339import javax .script .ScriptEngine ;
3440import javax .script .ScriptException ;
3541
42+ import org .scijava .log .LogService ;
43+ import org .scijava .plugin .Parameter ;
3644import org .scijava .prefs .PrefService ;
3745
3846/**
3947 * The default implementation of a {@link ScriptInterpreter}.
48+ * <p>
49+ * Credit to Jason Sachs for the multi-line evaluation (see
50+ * <a href="http://stackoverflow.com/a/5598207">his post on StackOverflow</a>).
51+ * </p>
4052 *
4153 * @author Johannes Schindelin
54+ * @author Curtis Rueden
4255 */
4356public class DefaultScriptInterpreter implements ScriptInterpreter {
4457
4558 private final ScriptLanguage language ;
4659 private final ScriptEngine engine ;
4760 private final History history ;
4861
62+ @ Parameter (required = false )
63+ private PrefService prefs ;
64+
65+ @ Parameter (required = false )
66+ private LogService log ;
67+
68+ private final StringBuilder buffer ;
69+ private int pendingLineCount ;
70+ private boolean expectingMoreInput ;
71+
4972 /**
50- * Constructs a new {@link DefaultScriptInterpreter}.
51- *
52- * @param scriptService the script service
53- * @param language the script language
73+ * @deprecated Use {@link #DefaultScriptInterpreter(ScriptLanguage)} instead.
5474 */
75+ @ Deprecated
76+ @ SuppressWarnings ("unused" )
5577 public DefaultScriptInterpreter (final PrefService prefs ,
5678 final ScriptService scriptService , final ScriptLanguage language )
5779 {
80+ this (language );
81+ }
82+
83+ /**
84+ * Creates a new script interpreter for the given script language.
85+ *
86+ * @param language {@link ScriptLanguage} of the interpreter
87+ */
88+ public DefaultScriptInterpreter (final ScriptLanguage language ) {
89+ this (language , null );
90+ }
91+
92+ /**
93+ * Creates a new script interpreter for the given script language, using the
94+ * specified script engine.
95+ *
96+ * @param language {@link ScriptLanguage} of the interpreter
97+ * @param engine {@link ScriptEngine} to use, or null for the specified
98+ * language's default engine
99+ */
100+ public DefaultScriptInterpreter (final ScriptLanguage language ,
101+ final ScriptEngine engine )
102+ {
103+ language .getContext ().inject (this );
58104 this .language = language ;
59- engine = language .getScriptEngine ();
60- history = new History (prefs , engine .getClass ().getName ());
105+ this .engine = engine == null ? language .getScriptEngine () : engine ;
106+ history = prefs == null ? null :
107+ new History (prefs , this .engine .getClass ().getName ());
61108 readHistory ();
109+ buffer = new StringBuilder ();
110+ reset ();
62111 }
63112
113+ // -- ScriptInterpreter methods --
114+
64115 @ Override
65116 public synchronized void readHistory () {
66117 if (history == null ) return ;
@@ -83,10 +134,100 @@ public synchronized String walkHistory(final String currentCommand,
83134 }
84135
85136 @ Override
86- public void eval (final String command ) throws ScriptException {
87- if (history != null ) history .add (command );
88- if (engine == null ) throw new java .lang .IllegalArgumentException ();
89- engine .eval (command );
137+ public Object eval (final String command ) throws ScriptException {
138+ addToHistory (command );
139+ return engine .eval (command );
140+ }
141+
142+ /**
143+ * {@inheritDoc}
144+ * <p>
145+ * This implementation from Jason Sachs uses the following strategy:
146+ * </p>
147+ * <ul>
148+ * <li>Keep a pending list of input lines not yet evaluated.</li>
149+ * <li>Try compiling (but not evaluating) the pending input lines.
150+ * <ul>
151+ * <li>If the compilation is OK, we may be able to execute pending input
152+ * lines.</li>
153+ * <li>If the compilation throws an exception, and there is an indication of
154+ * the position (line + column number) of the error, and this matches the end
155+ * of the pending input, then that's a clue that we're expecting more input,
156+ * so swallow the exception and wait for the next line.</li>
157+ * <li>Otherwise, we either don't know where the error is, or it happened
158+ * prior to the end of the pending input, so rethrow the exception.</li>
159+ * </ul>
160+ * </li>
161+ * <li>If we are not expecting any more input lines, and we only have one line
162+ * of pending input, then evaluate it and restart.</li>
163+ * <li>If we are not expecting any more input lines, and the last one is a
164+ * blank one, and we have more than one line of pending input, then evaluate
165+ * it and restart. Python's interactive shell seems to do this.</li>
166+ * <li>Otherwise, keep reading input lines.</li>
167+ * </ul>
168+ * <p>
169+ * This helps avoid certain problems:
170+ * </p>
171+ * <ul>
172+ * <li>users getting annoyed having to enter extra blank lines after
173+ * single-line inputs</li>
174+ * <li>users entering a long multi-line statement and only find out after the
175+ * fact that there was a syntax error in the 2nd line.</li>
176+ * </ul>
177+ * <p>
178+ * For further details, see <a href="http://stackoverflow.com/a/5598207">SO
179+ * #5584674</a>.
180+ * </p>
181+ * </p>
182+ */
183+ @ Override
184+ public Object interpret (final String line ) throws ScriptException {
185+ if (line .isEmpty ()) {
186+ if (!shouldEvaluatePendingInput (true )) return MORE_INPUT_PENDING ;
187+ }
188+
189+ pendingLineCount ++;
190+ buffer .append (line );
191+ buffer .append ("\n " );
192+
193+ if (!(engine instanceof Compilable )) {
194+ // Not a compilable language.
195+ // Evaluate directly, with no multi-line statements possible.
196+ try {
197+ return eval (buffer .toString ());
198+ }
199+ finally {
200+ reset ();
201+ }
202+ }
203+
204+ final CompiledScript cs = tryCompiling (buffer .toString (),
205+ getPendingLineCount (), line .length ());
206+
207+ if (cs == null ) {
208+ // Command did not compile.
209+ // Assume it is incomplete and wait for more input on the next line.
210+ return MORE_INPUT_PENDING ;
211+ }
212+ if (!shouldEvaluatePendingInput (line .isEmpty ())) {
213+ // We are still expecting more input.
214+ return MORE_INPUT_PENDING ;
215+ }
216+ // Command is complete; evaluate the compiled script.
217+ try {
218+ addToHistory (buffer .toString ());
219+ return cs .eval ();
220+ }
221+ finally {
222+ reset ();
223+ }
224+ }
225+
226+ @ Override
227+ public void reset () {
228+ buffer .setLength (0 );
229+ pendingLineCount = 0 ;
230+ expectingMoreInput = false ;
90231 }
91232
92233 @ Override
@@ -99,4 +240,127 @@ public ScriptEngine getEngine() {
99240 return engine ;
100241 }
101242
243+ @ Override
244+ public Bindings getBindings () {
245+ return engine .getBindings (ScriptContext .ENGINE_SCOPE );
246+ }
247+
248+ @ Override
249+ public boolean isReady () {
250+ return buffer .length () == 0 ;
251+ }
252+
253+ @ Override
254+ public boolean isExpectingMoreInput () {
255+ return expectingMoreInput ;
256+ }
257+
258+ // -- Helper methods --
259+
260+ private void addToHistory (final String command ) {
261+ if (history != null ) history .add (command );
262+ }
263+
264+ /**
265+ * @return number of lines pending execution
266+ */
267+ private int getPendingLineCount () {
268+ return pendingLineCount ;
269+ }
270+
271+ /**
272+ * @param lineIsEmpty whether the last line is empty
273+ * @return whether we should evaluate the pending input. The default behavior
274+ * is to evaluate if we only have one line of input, or if the user
275+ * enters a blank line. This behavior should be overridden where
276+ * appropriate.
277+ */
278+ private boolean shouldEvaluatePendingInput (final boolean lineIsEmpty ) {
279+ if (isExpectingMoreInput ()) return false ;
280+ return getPendingLineCount () == 1 || lineIsEmpty ;
281+ }
282+
283+ private CompiledScript tryCompiling (final String string , final int lineCount ,
284+ final int lastLineLength ) throws ScriptException
285+ {
286+ CompiledScript result = null ;
287+ try {
288+ final Compilable c = (Compilable ) engine ;
289+ result = c .compile (string );
290+ }
291+ catch (final ScriptException se ) {
292+ boolean rethrow = true ;
293+ if (se .getCause () != null ) {
294+ final Integer col = columnNumber (se );
295+ final Integer line = lineNumber (se );
296+ // swallow the exception if it occurs at the last character
297+ // of the input (we may need to wait for more lines)
298+ if (isLastCharacter (col , line , lineCount , lastLineLength )) {
299+ rethrow = false ;
300+ }
301+ else if (log != null && log .isDebug ()) {
302+ final String msg = se .getCause ().getMessage ();
303+ log .debug ("L" + line + " C" + col + "(" + lineCount + "," +
304+ lastLineLength + "): " + msg );
305+ log .debug ("in '" + string + "'" );
306+ }
307+ }
308+
309+ if (rethrow ) {
310+ reset ();
311+ throw se ;
312+ }
313+ }
314+
315+ expectingMoreInput = result == null ;
316+ return result ;
317+ }
318+
319+ private boolean isLastCharacter (final Integer col , final Integer line ,
320+ final int lineCount , final int lastLineLength )
321+ {
322+ if (col == null || line == null ) return false ;
323+ final int colNo = col .intValue (), lineNo = line .intValue ();
324+ return lineNo == lineCount && colNo == lastLineLength ||
325+ lineNo == lineCount + 1 && colNo == 0 ;
326+ }
327+
328+ private Integer columnNumber (final ScriptException se ) {
329+ if (se .getColumnNumber () >= 0 ) return se .getColumnNumber ();
330+ return callMethod (se .getCause (), "columnNumber" , Integer .class );
331+ }
332+
333+ private Integer lineNumber (final ScriptException se ) {
334+ if (se .getLineNumber () >= 0 ) return se .getLineNumber ();
335+ return callMethod (se .getCause (), "lineNumber" , Integer .class );
336+ }
337+
338+ private static Method getMethod (final Object object ,
339+ final String methodName )
340+ {
341+ try {
342+ return object .getClass ().getMethod (methodName );
343+ }
344+ catch (final NoSuchMethodException e ) {
345+ // gulp
346+ return null ;
347+ }
348+ }
349+
350+ private static <T > T callMethod (final Object object , final String methodName ,
351+ final Class <T > cl )
352+ {
353+ try {
354+ final Method m = getMethod (object , methodName );
355+ if (m != null ) {
356+ final Object result = m .invoke (object );
357+ return cl .cast (result );
358+ }
359+ }
360+ catch (final Exception e ) {
361+ e .printStackTrace ();
362+ }
363+ return null ;
364+ }
365+
102366}
0 commit comments