3030 */
3131package org .scijava .script ;
3232
33+ import java .lang .reflect .Method ;
34+
3335import javax .script .Bindings ;
36+ import javax .script .Compilable ;
37+ import javax .script .CompiledScript ;
3438import javax .script .ScriptContext ;
3539import javax .script .ScriptEngine ;
3640import javax .script .ScriptException ;
3741
42+ import org .scijava .log .LogService ;
43+ import org .scijava .plugin .Parameter ;
3844import org .scijava .prefs .PrefService ;
3945
4046/**
4147 * 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>
4252 *
4353 * @author Johannes Schindelin
54+ * @author Curtis Rueden
4455 */
4556public class DefaultScriptInterpreter implements ScriptInterpreter {
4657
@@ -51,6 +62,13 @@ public class DefaultScriptInterpreter implements ScriptInterpreter {
5162 @ Parameter (required = false )
5263 private PrefService prefs ;
5364
65+ @ Parameter (required = false )
66+ private LogService log ;
67+
68+ private final StringBuilder buffer ;
69+ private int pendingLineCount ;
70+ private boolean expectingMoreInput ;
71+
5472 /**
5573 * @deprecated Use {@link #DefaultScriptInterpreter(ScriptLanguage)} instead.
5674 */
@@ -88,6 +106,8 @@ public DefaultScriptInterpreter(final ScriptLanguage language,
88106 history = prefs == null ? null :
89107 new History (prefs , this .engine .getClass ().getName ());
90108 readHistory ();
109+ buffer = new StringBuilder ();
110+ reset ();
91111 }
92112
93113 // -- ScriptInterpreter methods --
@@ -115,11 +135,101 @@ public synchronized String walkHistory(final String currentCommand,
115135
116136 @ Override
117137 public Object eval (final String command ) throws ScriptException {
118- if (history != null ) history .add (command );
119- if (engine == null ) throw new java .lang .IllegalArgumentException ();
138+ addToHistory (command );
120139 return engine .eval (command );
121140 }
122141
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 ;
231+ }
232+
123233 @ Override
124234 public ScriptLanguage getLanguage () {
125235 return language ;
@@ -135,4 +245,122 @@ public Bindings getBindings() {
135245 return engine .getBindings (ScriptContext .ENGINE_SCOPE );
136246 }
137247
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+
138366}
0 commit comments