Skip to content

Commit 0f3bf44

Browse files
committed
ScriptREPL: generalize input and output streams
The use of InputStream and OutputStream was natural, but those are heavy abstract classes, not lightweight interfaces. It is more flexible to allow specifying a Supplier from which to draw the REPL input, and/or a Consumer to receive the REPL output. This will be useful e.g. from Python via JPype, which allows to implement Java interfaces in Python backed by proxies. But you cannot proxy an abstract class like InputStream or OutputStream. Nonetheless, the ability to simply use InputStream/OutputStream like System.in+System.out is also nice, so we keep those method signatures as well.
1 parent 8b56cb2 commit 0f3bf44

File tree

2 files changed

+109
-67
lines changed

2 files changed

+109
-67
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</parent>
1111

1212
<artifactId>scijava-common</artifactId>
13-
<version>2.94.3-SNAPSHOT</version>
13+
<version>2.95.0-SNAPSHOT</version>
1414

1515
<name>SciJava Common</name>
1616
<description>SciJava Common is a shared library for SciJava software. It provides a plugin framework, with an extensible mechanism for service discovery, backed by its own annotation processor, so that plugins can be loaded dynamically. It is used by downstream projects in the SciJava ecosystem, such as ImageJ and SCIFIO.</description>

src/main/java/org/scijava/script/ScriptREPL.java

Lines changed: 108 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
package org.scijava.script;
3131

3232
import java.io.BufferedReader;
33+
import java.io.ByteArrayOutputStream;
3334
import java.io.IOException;
3435
import java.io.InputStream;
3536
import java.io.InputStreamReader;
@@ -38,6 +39,8 @@
3839
import java.lang.reflect.Constructor;
3940
import java.util.ArrayList;
4041
import java.util.List;
42+
import java.util.function.Consumer;
43+
import java.util.function.Supplier;
4144

4245
import javax.script.Bindings;
4346
import javax.script.ScriptException;
@@ -67,9 +70,9 @@ public class ScriptREPL {
6770
@Parameter(required = false)
6871
private PluginService pluginService;
6972

70-
private final PrintStream out;
73+
private final Consumer<String> out;
7174

72-
private String languagePreference = null;
75+
private String languagePreference;
7376

7477
/** List of interpreter-friendly script languages. */
7578
private List<ScriptLanguage> languages;
@@ -84,19 +87,26 @@ public ScriptREPL(final Context context) {
8487
this(context, System.out);
8588
}
8689

90+
public ScriptREPL(final Context context, final String language) {
91+
this(context, language, System.out);
92+
}
93+
8794
public ScriptREPL(final Context context, final OutputStream out) {
88-
context.inject(this);
89-
this.out = out instanceof PrintStream ?
90-
(PrintStream) out : new PrintStream(out);
95+
this(context, null, out);
9196
}
9297

93-
public ScriptREPL(final Context context, final String language) {
94-
this(context, language, System.out);
98+
public ScriptREPL(final Context context, final String language,
99+
final OutputStream out)
100+
{
101+
this(context, language, outputStreamConsumer(out));
95102
}
96103

97-
public ScriptREPL(final Context context, final String language, final OutputStream out) {
98-
this(context, out);
104+
public ScriptREPL(final Context context, final String language,
105+
final Consumer<String> out)
106+
{
107+
context.inject(this);
99108
languagePreference = language;
109+
this.out = out;
100110
}
101111

102112
/**
@@ -132,11 +142,39 @@ public void loop() throws IOException {
132142
* @param in Input stream from which commands are read.
133143
*/
134144
public void loop(final InputStream in) throws IOException {
135-
initialize();
136145
final BufferedReader bin = new BufferedReader(new InputStreamReader(in));
146+
try {
147+
loop(() -> {
148+
try {
149+
return bin.readLine();
150+
}
151+
catch (final IOException exc) {
152+
throw new RuntimeException(exc);
153+
}
154+
});
155+
}
156+
catch (final RuntimeException exc) {
157+
// NB: This convolution lets us throw IOException from inside a
158+
// Supplier.get implementation, by wrapping in a RuntimeException.
159+
// We then unwrap it again and throw it here, where we said we would.
160+
final Throwable cause = exc.getCause();
161+
if (cause instanceof IOException) throw (IOException) cause;
162+
else throw exc;
163+
}
164+
}
165+
166+
/**
167+
* Starts a Read-Eval-Print-Loop from the given source, returning when
168+
* the loop terminates.
169+
*
170+
* @param in Source from which commands are read.
171+
*/
172+
public void loop(final Supplier<?> in) {
173+
initialize();
137174
while (true) {
138175
prompt();
139-
final String line = bin.readLine();
176+
final Object input = in.get();
177+
final String line = input == null ? null : input.toString();
140178
if (line == null) break;
141179
if (!evaluate(line)) return;
142180
}
@@ -157,54 +195,40 @@ public void initialize() {
157195
*/
158196
public void initialize(final boolean verbose) {
159197
if (verbose) {
160-
out.println("Welcome to the SciJava REPL!");
161-
out.println();
198+
println("Welcome to the SciJava REPL!");
199+
println();
162200
help();
163201
}
164202
final List<ScriptLanguage> langs = getInterpretedLanguages();
165203
if (verbose) {
166204
if (langs.isEmpty()) {
167-
out.println("--------------------------------------------------------------");
168-
out.println("Uh oh! There are no SciJava script languages available!");
169-
out.println("Are any on your classpath? E.g.: org.scijava:scripting-groovy?");
170-
out.println("--------------------------------------------------------------");
171-
out.println();
205+
println("--------------------------------------------------------------");
206+
println("Uh oh! There are no SciJava script languages available!");
207+
println("Are any on your classpath? E.g.: org.scijava:scripting-groovy?");
208+
println("--------------------------------------------------------------");
209+
println();
172210
return;
173211
}
174-
out.println("Have fun!");
175-
out.println();
176-
177-
if(languagePreference != null) {
178-
selectPreferredLanguage(langs);
179-
} else {
180-
lang(langs.get(0).getLanguageName());
181-
}
212+
println("Have fun!");
213+
println();
182214
}
183-
else if (!langs.isEmpty()) {
184-
if(languagePreference != null) {
185-
selectPreferredLanguage(langs);
186-
} else {
187-
lang(langs.get(0));
188-
}
215+
if (!langs.isEmpty()) {
216+
if (languagePreference != null) selectPreferredLanguage(langs);
217+
else lang(langs.get(0));
189218
}
190-
191219
populateBindings(interpreter.getBindings());
192220
}
193221

194222
private void selectPreferredLanguage(List<ScriptLanguage> langs) {
195-
final ScriptLanguage preference = langs
196-
.stream().filter(lang -> languagePreference.equals(lang.getLanguageName()))
197-
.findAny().orElse(null);
198-
if(preference != null) {
199-
lang(preference);
200-
} else {
201-
lang(langs.get(0).getLanguageName());
202-
}
223+
final ScriptLanguage preference = langs.stream()
224+
.filter(lang -> languagePreference.equals(lang.getLanguageName()))
225+
.findFirst().orElse(langs.get(0));
226+
lang(preference);
203227
}
204228

205229
/** Outputs the prompt. */
206230
public void prompt() {
207-
out.print(interpreter == null || interpreter.isReady() ? "> " : "\\ ");
231+
print(interpreter == null || interpreter.isReady() ? "> " : "\\ ");
208232
}
209233

210234
/**
@@ -230,22 +254,22 @@ public boolean evaluate(final String line) {
230254
// pass the input to the current interpreter for evaluation
231255
final Object result = interpreter.interpret(line);
232256
if (result != ScriptInterpreter.MORE_INPUT_PENDING) {
233-
out.println(s(result));
257+
println(s(result));
234258
}
235259
}
236260
}
237261
catch (final ScriptException exc) {
238262
// NB: Something went wrong interpreting the line of code.
239263
// Let's just display the error message, unless we are in debug mode.
240-
if (debug) exc.printStackTrace(out);
264+
if (debug) printStackTrace(exc);
241265
else {
242266
final String msg = exc.getMessage();
243-
out.println(msg == null ? exc.getClass().getName() : msg);
267+
println(msg == null ? exc.getClass().getName() : msg);
244268
}
245269
}
246270
catch (final Throwable exc) {
247271
// NB: Something unusual went wrong. Dump the whole exception always.
248-
exc.printStackTrace(out);
272+
printStackTrace(exc);
249273
}
250274
return true;
251275
}
@@ -254,17 +278,17 @@ public boolean evaluate(final String line) {
254278

255279
/** Prints a usage guide. */
256280
public void help() {
257-
out.println("Available built-in commands:");
258-
out.println();
259-
out.println(" :help | this handy list of commands");
260-
out.println(" :vars | dump a list of variables");
261-
out.println(" :lang <name> | switch the active language");
262-
out.println(" :langs | list available languages");
263-
out.println(" :debug | toggle full stack traces");
264-
out.println(" :quit | exit the REPL");
265-
out.println();
266-
out.println("Or type a statement to evaluate it with the active language.");
267-
out.println();
281+
println("Available built-in commands:");
282+
println();
283+
println(" :help | this handy list of commands");
284+
println(" :vars | dump a list of variables");
285+
println(" :lang <name> | switch the active language");
286+
println(" :langs | list available languages");
287+
println(" :debug | toggle full stack traces");
288+
println(" :quit | exit the REPL");
289+
println();
290+
println("Or type a statement to evaluate it with the active language.");
291+
println();
268292
}
269293

270294
/** Lists variables in the script context. */
@@ -294,11 +318,11 @@ public void lang(final String langName) {
294318
// create the new interpreter
295319
final ScriptLanguage language = scriptService.getLanguageByName(langName);
296320
if (language == null) {
297-
out.println("No such language: " + langName);
321+
println("No such language: " + langName);
298322
return;
299323
}
300324
lang(language);
301-
out.println("language -> " + interpreter.getLanguage().getLanguageName());
325+
println("language -> " + interpreter.getLanguage().getLanguageName());
302326
}
303327

304328
/**
@@ -316,7 +340,7 @@ public void lang(final ScriptLanguage language) {
316340
copyBindings(interpreter, newInterpreter);
317341
}
318342
catch (final Throwable t) {
319-
t.printStackTrace(out);
343+
printStackTrace(t);
320344
}
321345
interpreter = newInterpreter;
322346
}
@@ -335,7 +359,7 @@ public void langs() {
335359

336360
public void debug() {
337361
debug = !debug;
338-
out.println("debug mode -> " + debug);
362+
println("debug mode -> " + debug);
339363
}
340364

341365
// -- Main method --
@@ -417,7 +441,7 @@ private List<Gateway> gateways() {
417441
gateways.add(gateway);
418442
}
419443
catch (final Throwable t) {
420-
t.printStackTrace(out);
444+
printStackTrace(t);
421445
}
422446
}
423447
return gateways;
@@ -442,6 +466,17 @@ private String type(final Object value) {
442466
return "[" + decoded.getClass().getName() + "]";
443467
}
444468

469+
private static final String NL = System.getProperty("line.separator");
470+
private void print(String s) { out.accept(s); }
471+
private void println() { print(NL); }
472+
private void println(final String s) { print(s + NL); }
473+
474+
private void printStackTrace(final Throwable t) {
475+
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
476+
t.printStackTrace(new PrintStream(baos));
477+
println(baos.toString());
478+
}
479+
445480
private void printColumns(final List<?>... columns) {
446481
final int pad = 2;
447482

@@ -456,18 +491,26 @@ private void printColumns(final List<?>... columns) {
456491
}
457492

458493
// output the columns
494+
final StringBuilder sb = new StringBuilder();
459495
for (int i = 0; i < columns[0].size(); i++) {
496+
sb.setLength(0);
460497
for (int c = 0; c < columns.length; c++) {
461498
final String s = s(columns[c].get(i));
462-
out.print(s);
499+
sb.append(s);
463500
for (int p = s.length(); p < widths[c] + pad; p++) {
464-
out.print(' ');
501+
sb.append(' ');
465502
}
466503
}
467-
out.println();
504+
println(sb.toString());
468505
}
469506
}
470507

508+
private static Consumer<String> outputStreamConsumer(final OutputStream out) {
509+
final PrintStream ps = out instanceof PrintStream ?
510+
(PrintStream) out : new PrintStream(out);
511+
return s -> { ps.print(s); ps.flush(); };
512+
}
513+
471514
private static String lowerCamelCase(final String s) {
472515
final StringBuilder sb = new StringBuilder(s);
473516
for (int i=0; i<sb.length(); i++) {
@@ -481,5 +524,4 @@ private static String lowerCamelCase(final String s) {
481524
private static String s(final Object o) {
482525
return o == null ? NULL : o.toString();
483526
}
484-
485527
}

0 commit comments

Comments
 (0)