001package jmri.script;
002
003import java.io.File;
004import java.io.FileInputStream;
005import java.io.FileNotFoundException;
006import java.io.IOException;
007import java.io.InputStream;
008import java.io.InputStreamReader;
009import java.nio.charset.StandardCharsets;
010import java.util.HashMap;
011import java.util.MissingResourceException;
012import java.util.Properties;
013
014import javax.annotation.CheckForNull;
015import javax.annotation.Nonnull;
016import javax.script.Bindings;
017import javax.script.ScriptContext;
018import javax.script.ScriptEngine;
019import javax.script.ScriptEngineFactory;
020import javax.script.ScriptEngineManager;
021import javax.script.ScriptException;
022import javax.script.SimpleBindings;
023import javax.script.SimpleScriptContext;
024import jmri.AddressedProgrammerManager;
025import jmri.AudioManager;
026import jmri.BlockManager;
027import jmri.CommandStation;
028import jmri.GlobalProgrammerManager;
029import jmri.IdTagManager;
030import jmri.InstanceManager;
031import jmri.InstanceManagerAutoDefault;
032import jmri.Light;
033import jmri.LightManager;
034import jmri.MemoryManager;
035import jmri.NamedBean;
036import jmri.NamedBeanHandleManager;
037import jmri.PowerManager;
038import jmri.ReporterManager;
039import jmri.RouteManager;
040import jmri.SectionManager;
041import jmri.Sensor;
042import jmri.SensorManager;
043import jmri.ShutDownManager;
044import jmri.SignalHead;
045import jmri.SignalHeadManager;
046import jmri.SignalMastManager;
047import jmri.TransitManager;
048import jmri.Turnout;
049import jmri.TurnoutManager;
050import jmri.jmrit.display.layoutEditor.LayoutBlockManager;
051import jmri.jmrit.logix.WarrantManager;
052import jmri.util.FileUtil;
053import jmri.util.FileUtilSupport;
054import org.apache.commons.io.FilenameUtils;
055import org.python.core.PySystemState;
056import org.python.util.PythonInterpreter;
057
058/**
059 * Provide a manager for {@link javax.script.ScriptEngine}s. The following
060 * methods are the only mechanisms for evaluating a Python script that respect
061 * the <code>jython.exec</code> property in the <em>python.properties</em> file:
062 * <ul>
063 * <li>{@link #eval(java.io.File)}</li>
064 * <li>{@link #eval(java.io.File, javax.script.Bindings)}</li>
065 * <li>{@link #eval(java.io.File, javax.script.ScriptContext)}</li>
066 * <li>{@link #eval(java.lang.String, javax.script.ScriptEngine)}</li>
067 * <li>{@link #runScript(java.io.File)}</li>
068 * </ul>
069 * Evaluating a script using <code>getEngine*(java.lang.String).eval(...)</code>
070 * methods will not respect the <code>jython.exec</code> property, although all
071 * methods will respect all other properties of that file.
072 *
073 * @author Randall Wood
074 */
075public final class JmriScriptEngineManager implements InstanceManagerAutoDefault {
076
077    private final ScriptEngineManager manager = new ScriptEngineManager();
078    private final HashMap<String, String> names = new HashMap<>();
079    private final HashMap<String, ScriptEngineFactory> factories = new HashMap<>();
080    private final HashMap<String, ScriptEngine> engines = new HashMap<>();
081    private final ScriptContext context;
082
083    // should be replaced with default context
084    // package private for unit testing
085    static final String JYTHON_DEFAULTS = "jmri_defaults.py";
086    private static final String EXTENSION = "extension";
087    public static final String JYTHON = "jython";
088    private PythonInterpreter jython = null;
089
090    /**
091     * Create a JmriScriptEngineManager. In most cases, it is preferable to use
092     * {@link #getDefault()} to get existing {@link javax.script.ScriptEngine}
093     * instances.
094     */
095    public JmriScriptEngineManager() {
096        this.manager.getEngineFactories().stream().forEach(factory -> {
097            if (factory.getEngineVersion() != null) {
098                log.trace("{} {} is provided by {} {}",
099                        factory.getLanguageName(),
100                        factory.getLanguageVersion(),
101                        factory.getEngineName(),
102                        factory.getEngineVersion());
103                String engineName = factory.getEngineName();
104                factory.getExtensions().stream().forEach(extension -> {
105                    names.put(extension, engineName);
106                    log.trace("\tExtension: {}", extension);
107                });
108                factory.getMimeTypes().stream().forEach(mimeType -> {
109                    names.put(mimeType, engineName);
110                    log.trace("\tMime type: {}", mimeType);
111                });
112                factory.getNames().stream().forEach(name -> {
113                    names.put(name, engineName);
114                    log.trace("\tNames: {}", name);
115                });
116                this.names.put(factory.getLanguageName(), engineName);
117                this.names.put(engineName, engineName);
118                this.factories.put(engineName, factory);
119            } else {
120                log.debug("Skipping {} due to null version, i.e. not operational; do you have GraalVM installed?", factory.getEngineName());
121            }
122        });
123
124        // this should agree with help/en/html/tools/scripting/Start.shtml
125        Bindings bindings = new SimpleBindings();
126        
127        bindings.put("sensors", InstanceManager.getNullableDefault(SensorManager.class));
128        bindings.put("turnouts", InstanceManager.getNullableDefault(TurnoutManager.class));
129        bindings.put("lights", InstanceManager.getNullableDefault(LightManager.class));
130        bindings.put("signals", InstanceManager.getNullableDefault(SignalHeadManager.class));
131        bindings.put("masts", InstanceManager.getNullableDefault(SignalMastManager.class));
132        bindings.put("routes", InstanceManager.getNullableDefault(RouteManager.class));
133        bindings.put("blocks", InstanceManager.getNullableDefault(BlockManager.class));
134        bindings.put("reporters", InstanceManager.getNullableDefault(ReporterManager.class));
135        bindings.put("idtags", InstanceManager.getNullableDefault(IdTagManager.class));
136        bindings.put("memories", InstanceManager.getNullableDefault(MemoryManager.class));
137        bindings.put("powermanager", InstanceManager.getNullableDefault(PowerManager.class));
138        bindings.put("addressedProgrammers", InstanceManager.getNullableDefault(AddressedProgrammerManager.class));
139        bindings.put("globalProgrammers", InstanceManager.getNullableDefault(GlobalProgrammerManager.class));
140        bindings.put("dcc", InstanceManager.getNullableDefault(CommandStation.class));
141        bindings.put("audio", InstanceManager.getNullableDefault(AudioManager.class));
142        bindings.put("shutdown", InstanceManager.getNullableDefault(ShutDownManager.class));
143        bindings.put("layoutblocks", InstanceManager.getNullableDefault(LayoutBlockManager.class));
144        bindings.put("warrants", InstanceManager.getNullableDefault(WarrantManager.class));
145        bindings.put("sections", InstanceManager.getNullableDefault(SectionManager.class));
146        bindings.put("transits", InstanceManager.getNullableDefault(TransitManager.class));
147        bindings.put("beans", InstanceManager.getNullableDefault(NamedBeanHandleManager.class));
148        
149        bindings.put("CLOSED", Turnout.CLOSED);
150        bindings.put("THROWN", Turnout.THROWN);
151        bindings.put("CABLOCKOUT", Turnout.CABLOCKOUT);
152        bindings.put("PUSHBUTTONLOCKOUT", Turnout.PUSHBUTTONLOCKOUT);
153        bindings.put("UNLOCKED", Turnout.UNLOCKED);
154        bindings.put("LOCKED", Turnout.LOCKED);
155        bindings.put("ACTIVE", Sensor.ACTIVE);
156        bindings.put("INACTIVE", Sensor.INACTIVE);
157        bindings.put("ON", Light.ON);
158        bindings.put("OFF", Light.OFF);
159        bindings.put("UNKNOWN", NamedBean.UNKNOWN);
160        bindings.put("INCONSISTENT", NamedBean.INCONSISTENT);
161        bindings.put("DARK", SignalHead.DARK);
162        bindings.put("RED", SignalHead.RED);
163        bindings.put("YELLOW", SignalHead.YELLOW);
164        bindings.put("GREEN", SignalHead.GREEN);
165        bindings.put("LUNAR", SignalHead.LUNAR);
166        bindings.put("FLASHRED", SignalHead.FLASHRED);
167        bindings.put("FLASHYELLOW", SignalHead.FLASHYELLOW);
168        bindings.put("FLASHGREEN", SignalHead.FLASHGREEN);
169        bindings.put("FLASHLUNAR", SignalHead.FLASHLUNAR);
170        
171        bindings.put("FileUtil", FileUtilSupport.getDefault());
172        
173        this.context = new SimpleScriptContext();
174        this.context.setBindings(bindings, ScriptContext.GLOBAL_SCOPE);
175        this.context.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
176        log.trace("end init context {} bindings {}", context, bindings);
177    }
178
179    /**
180     * Get the default instance of a JmriScriptEngineManager. Using the default
181     * instance ensures that a script retains the context of the prior script.
182     *
183     * @return the default JmriScriptEngineManager
184     */
185    @Nonnull
186    public static JmriScriptEngineManager getDefault() {
187        return InstanceManager.getDefault(JmriScriptEngineManager.class);
188    }
189
190    /**
191     * Get the Java ScriptEngineManager that this object contains.
192     *
193     * @return the ScriptEngineManager
194     */
195    @Nonnull
196    public ScriptEngineManager getManager() {
197        return this.manager;
198    }
199
200    /**
201     * Given a file extension, get the ScriptEngine registered to handle that
202     * extension.
203     *
204     * @param extension a file extension
205     * @return a ScriptEngine or null
206     * @throws ScriptException if unable to get a matching ScriptEngine
207     */
208    @Nonnull
209    public ScriptEngine getEngineByExtension(String extension) throws ScriptException {
210        return getEngine(extension, EXTENSION);
211    }
212
213    /**
214     * Given a mime type, get the ScriptEngine registered to handle that mime
215     * type.
216     *
217     * @param mimeType a mimeType for a script
218     * @return a ScriptEngine or null
219     * @throws ScriptException if unable to get a matching ScriptEngine
220     */
221    @Nonnull
222    public ScriptEngine getEngineByMimeType(String mimeType) throws ScriptException {
223        return getEngine(mimeType, "mime type");
224    }
225
226    /**
227     * Given a short name, get the ScriptEngine registered by that name.
228     *
229     * @param shortName the short name for the ScriptEngine
230     * @return a ScriptEngine or null
231     * @throws ScriptException if unable to get a matching ScriptEngine
232     */
233    @Nonnull
234    public ScriptEngine getEngineByName(String shortName) throws ScriptException {
235        return getEngine(shortName, "name");
236    }
237
238    @Nonnull
239    private ScriptEngine getEngine(@CheckForNull String engineName, @Nonnull String type) throws ScriptException {
240        String name = names.get(engineName);
241        ScriptEngine engine = getEngine(name);
242        if (name == null || engine == null) {
243            throw scriptEngineNotFound(engineName, type, false);
244        }
245        return engine;
246    }
247
248    /**
249     * Get a ScriptEngine by its name(s), mime type, or supported extensions.
250     *
251     * @param name the complete name, mime type, or extension for the
252     *             ScriptEngine
253     * @return a ScriptEngine or null if matching engine not found
254     */
255    @CheckForNull
256    public ScriptEngine getEngine(@CheckForNull String name) {
257        log.debug("getEngine(\"{}\")", name);
258        if (!engines.containsKey(name)) {
259            name = names.get(name);
260            ScriptEngineFactory factory;
261            if (JYTHON.equals(name)) {
262                // Setup the default python engine to use the JMRI python
263                // properties
264                log.trace("   initializePython");
265                initializePython();
266            } else if ((factory = factories.get(name)) != null) {
267                log.trace("   Create engine for {} context {}", name, context);
268                ScriptEngine engine = factory.getScriptEngine();
269                engine.setContext(context);
270                engines.put(name, engine);
271            }
272        }
273        return engines.get(name);
274    }
275
276    /**
277     * Evaluate a script using the given ScriptEngine.
278     *
279     * @param script The script.
280     * @param engine The script engine.
281     * @return The results of evaluating the script.
282     * @throws javax.script.ScriptException if there is an error in the script.
283     */
284    public Object eval(String script, ScriptEngine engine) throws ScriptException {
285
286        if (engine.getFactory().getEngineName().equals("Oracle Nashorn")) {
287            warnJavaScriptUsers();
288        }
289
290        if (JYTHON.equals(engine.getFactory().getEngineName()) && this.jython != null) {
291            this.jython.exec(script);
292            return null;
293        }
294        return engine.eval(script);
295    }
296
297    /**
298     * Evaluate a script contained in a file. Uses the extension of the file to
299     * determine which ScriptEngine to use.
300     *
301     * @param file the script file to evaluate.
302     * @return the results of the evaluation.
303     * @throws javax.script.ScriptException  if there is an error evaluating the
304     *                                       script.
305     * @throws java.io.FileNotFoundException if the script file cannot be found.
306     * @throws java.io.IOException           if the script file cannot be read.
307     */
308    public Object eval(File file) throws ScriptException, IOException {
309        return eval(file, null, null);
310    }
311
312    /**
313     * Evaluate a script contained in a file given a set of
314     * {@link javax.script.Bindings} to add to the script's context. Uses the
315     * extension of the file to determine which ScriptEngine to use.
316     *
317     * @param file     the script file to evaluate.
318     * @param bindings script bindings to evaluate against.
319     * @return the results of the evaluation.
320     * @throws javax.script.ScriptException  if there is an error evaluating the
321     *                                       script.
322     * @throws java.io.FileNotFoundException if the script file cannot be found.
323     * @throws java.io.IOException           if the script file cannot be read.
324     */
325    public Object eval(File file, Bindings bindings) throws ScriptException, IOException {
326        return eval(file, null, bindings);
327    }
328
329    /**
330     * Evaluate a script contained in a file given a special context for the
331     * script. Uses the extension of the file to determine which ScriptEngine to
332     * use.
333     *
334     * @param file    the script file to evaluate.
335     * @param context script context to evaluate within.
336     * @return the results of the evaluation.
337     * @throws javax.script.ScriptException  if there is an error evaluating the
338     *                                       script.
339     * @throws java.io.FileNotFoundException if the script file cannot be found.
340     * @throws java.io.IOException           if the script file cannot be read.
341     */
342    public Object eval(File file, ScriptContext context) throws ScriptException, IOException {
343        return eval(file, context, null);
344    }
345
346    /**
347     * Evaluate a script contained in a file given a set of
348     * {@link javax.script.Bindings} to add to the script's context. Uses the
349     * extension of the file to determine which ScriptEngine to use.
350     *
351     * @param file     the script file to evaluate.
352     * @param context  script context to evaluate within.
353     * @param bindings script bindings to evaluate against.
354     * @return the results of the evaluation.
355     * @throws javax.script.ScriptException  if there is an error evaluating the
356     *                                       script.
357     * @throws java.io.FileNotFoundException if the script file cannot be found.
358     * @throws java.io.IOException           if the script file cannot be read.
359     */
360    @CheckForNull
361    private Object eval(File file, @CheckForNull ScriptContext context, @CheckForNull Bindings bindings)
362            throws ScriptException, IOException {
363        ScriptEngine engine;
364        Object result = null;
365        if ((engine = getEngineOrEval(file)) != null) {
366            try (InputStreamReader reader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
367                if (context != null) {
368                    result = engine.eval(reader, context);
369                } else if (bindings != null) {
370                    result = engine.eval(reader, bindings);
371                } else {
372                    result = engine.eval(reader);
373                }
374            }
375        }
376        return result;
377    }
378
379    /**
380     * Get the ScriptEngine to evaluate the file with; if not using a
381     * ScriptEngine to evaluate Python files, evaluate the file with a
382     * {@link org.python.util.PythonInterpreter} and do not return a
383     * ScriptEngine.
384     *
385     * @param file the script file to evaluate.
386     * @return the ScriptEngine or null if evaluated with a PythonInterpreter.
387     * @throws javax.script.ScriptException  if there is an error evaluating the
388     *                                       script.
389     * @throws java.io.FileNotFoundException if the script file cannot be found.
390     * @throws java.io.IOException           if the script file cannot be read.
391     */
392    @CheckForNull
393    private ScriptEngine getEngineOrEval(File file) throws ScriptException, IOException {
394        ScriptEngine engine = this.getEngine(FilenameUtils.getExtension(file.getName()), EXTENSION);
395
396        if (engine.getFactory().getEngineName().equals("Oracle Nashorn")) {
397            warnJavaScriptUsers();
398        }
399
400        if (JYTHON.equals(engine.getFactory().getEngineName()) && this.jython != null) {
401            try (FileInputStream fi = new FileInputStream(file)) {
402                this.jython.execfile(fi);
403            }
404            return null;
405        }
406        return engine;
407    }
408
409    /**
410     * Run a script, suppressing common errors. Note that the file needs to have
411     * a registered extension, or a NullPointerException will be thrown.
412     * <p>
413     * <strong>Note:</strong> this will eventually be deprecated in favor of using
414     * {@link #eval(File)} and having callers handle exceptions.
415     *
416     * @param file the script to run.
417     */
418    public void runScript(File file) {
419        try {
420            this.eval(file);
421        } catch (FileNotFoundException ex) {
422            log.error("File {} not found.", file);
423        } catch (IOException ex) {
424            log.error("Exception working with file {}", file);
425        } catch (ScriptException ex) {
426            log.error("Error in script {}.", file, ex);
427        }
428
429    }
430
431    /**
432     * Initialize all ScriptEngines. This can be used to prevent the on-demand
433     * initialization of a ScriptEngine from causing a pause in JMRI.
434     */
435    public void initializeAllEngines() {
436        this.factories.keySet().stream().forEach(this::getEngine);
437    }
438
439    /**
440     * This is a temporary method to warn users that the JavaScript/ECMAscript
441     * support may be going away soon.
442     */
443    private void warnJavaScriptUsers() {
444        if (! dontWarnJavaScript) {
445            log.warn("*** Scripting with JavaScript/ECMAscript is being deprecated ***");
446            log.warn("*** and may soon be removed.  If you are using this, please  ***");
447            log.warn("*** contact us on the jmriusers group for assistance.        ***");
448            
449            if (! java.awt.GraphicsEnvironment.isHeadless()) {
450                jmri.util.swing.JmriJOptionPane.showMessageDialog(null, 
451                    "<html>"+
452                    "Scripting with JavaScript/ECMAscript is being deprecated <br/>"+
453                    "and may soon be removed.  If you are using this, please<br/>"+
454                    "contact us on the jmriusers group for assistance.<br/>"+
455                    "</html>"
456                );
457            }
458        }
459        dontWarnJavaScript = true;
460    }
461    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "MS_PKGPROTECT",
462            justification = "Public accessibility for script to override warning")
463    static public boolean dontWarnJavaScript = false;
464            // The jython/DontWarnJavaScript.py script will disable the warning
465    
466    /**
467     * Get the default {@link javax.script.ScriptContext} for all
468     * {@link javax.script.ScriptEngine}s.
469     *
470     * @return the default ScriptContext;
471     */
472    @Nonnull
473    public ScriptContext getDefaultContext() {
474        return this.context;
475    }
476
477    /**
478     * Given a file extension, get the ScriptEngineFactory registered to handle
479     * that extension.
480     *
481     * @param extension a file extension
482     * @return a ScriptEngineFactory or null
483     * @throws ScriptException if unable to get a matching ScriptEngineFactory
484     */
485    @Nonnull
486    public ScriptEngineFactory getFactoryByExtension(String extension) throws ScriptException {
487        return getFactory(extension, EXTENSION);
488    }
489
490    /**
491     * Given a mime type, get the ScriptEngineFactory registered to handle that
492     * mime type.
493     *
494     * @param mimeType the script mimeType
495     * @return a ScriptEngineFactory or null
496     * @throws ScriptException if unable to get a matching ScriptEngineFactory
497     */
498    @Nonnull
499    public ScriptEngineFactory getFactoryByMimeType(String mimeType) throws ScriptException {
500        return getFactory(mimeType, "mime type");
501    }
502
503    /**
504     * Given a short name, get the ScriptEngineFactory registered by that name.
505     *
506     * @param shortName the short name for the factory
507     * @return a ScriptEngineFactory or null
508     * @throws ScriptException if unable to get a matching ScriptEngineFactory
509     */
510    @Nonnull
511    public ScriptEngineFactory getFactoryByName(String shortName) throws ScriptException {
512        return getFactory(shortName, "name");
513    }
514
515    @Nonnull
516    private ScriptEngineFactory getFactory(@CheckForNull String factoryName, @Nonnull String type)
517            throws ScriptException {
518        String name = this.names.get(factoryName);
519        ScriptEngineFactory factory = getFactory(name);
520        if (name == null || factory == null) {
521            throw scriptEngineNotFound(factoryName, type, true);
522        }
523        return factory;
524    }
525
526    /**
527     * Get a ScriptEngineFactory by its name(s), mime types, or supported
528     * extensions.
529     *
530     * @param name the complete name, mime type, or extension for a factory
531     * @return a ScriptEngineFactory or null
532     */
533    @CheckForNull
534    public ScriptEngineFactory getFactory(@CheckForNull String name) {
535        if (!factories.containsKey(name)) {
536            name = names.get(name);
537        }
538        return this.factories.get(name);
539    }
540
541    /**
542     * The Python ScriptEngine can be configured using a custom
543     * python.properties file and will run jmri_defaults.py if found in the
544     * user's configuration profile or settings directory. See python.properties
545     * in the JMRI installation directory for details of how to configure the
546     * Python ScriptEngine.
547     */
548    public void initializePython() {
549        if (!this.engines.containsKey(JYTHON)) {
550            initializePythonInterpreter(initializePythonState());
551        }
552    }
553
554    /**
555     * Create a new PythonInterpreter with the default bindings.
556     *
557     * @return a new interpreter
558     */
559    public PythonInterpreter newPythonInterpreter() {
560        initializePython();
561        PythonInterpreter pi = new PythonInterpreter();
562        context.getBindings(ScriptContext.GLOBAL_SCOPE).forEach(pi::set);
563        return pi;
564    }
565
566    /**
567     * Initialize the Python ScriptEngine state including Python global state.
568     *
569     * @return true if the Python interpreter will be used outside a
570     *         ScriptEngine; false otherwise
571     */
572    private boolean initializePythonState() {
573        // Get properties for interpreter
574        // Search in user files, the profile directory, the settings directory,
575        // and in the program path in that order
576        InputStream is = FileUtil.findInputStream("python.properties",
577                FileUtil.getUserFilesPath(),
578                FileUtil.getProfilePath(),
579                FileUtil.getPreferencesPath(),
580                FileUtil.getProgramPath());
581        Properties properties;
582        properties = new Properties(System.getProperties());
583        properties.setProperty("python.console.encoding", StandardCharsets.UTF_8.name()); // NOI18N
584        properties.setProperty("python.cachedir", FileUtil
585                .getAbsoluteFilename(properties.getProperty("python.cachedir", "settings:jython/cache"))); // NOI18N
586        boolean execJython = false;
587        if (is != null) {
588            String pythonPath = "python.path";
589            try {
590                properties.load(is);
591                String path = properties.getProperty(pythonPath, "");
592                if (path.length() != 0) {
593                    path = path.concat(File.pathSeparator);
594                }
595                properties.setProperty(pythonPath, path.concat(FileUtil.getScriptsPath()
596                        .concat(File.pathSeparator).concat(FileUtil.getAbsoluteFilename("program:jython"))));
597                execJython = Boolean.valueOf(properties.getProperty("jython.exec", Boolean.toString(execJython)));
598            } catch (IOException ex) {
599                log.error("Found, but unable to read python.properties: {}", ex.getMessage());
600            }
601            log.debug("Jython path is {}", PySystemState.getBaseProperties().getProperty(pythonPath));
602        }
603        PySystemState.initialize(null, properties);
604        return execJython;
605    }
606
607    /**
608     * Initialize the Python ScriptEngine and interpreter, including running any
609     * code in {@value #JYTHON_DEFAULTS}, if present.
610     *
611     * @param execJython true if also initializing an independent interpreter;
612     *                   false otherwise
613     */
614    private void initializePythonInterpreter(boolean execJython) {
615        // Create the interpreter
616        try {
617            log.debug("create interpreter");
618            ScriptEngine python = this.manager.getEngineByName(JYTHON);
619            python.setContext(this.context);
620            engines.put(JYTHON, python);
621            InputStream is = FileUtil.findInputStream(JYTHON_DEFAULTS,
622                    FileUtil.getUserFilesPath(),
623                    FileUtil.getProfilePath(),
624                    FileUtil.getPreferencesPath());
625            if (execJython) {
626                jython = newPythonInterpreter();
627            }
628            if (is != null) {
629                python.eval(new InputStreamReader(is));
630                if (this.jython != null) {
631                    this.jython.execfile(is);
632                }
633            }
634        } catch (ScriptException e) {
635            log.error("Exception creating jython system objects", e);
636        }
637    }
638
639    // package private for unit testing
640    @CheckForNull
641    PythonInterpreter getPythonInterpreter() {
642        return jython;
643    }
644
645    /**
646     * Helper to handle logging and exceptions.
647     *
648     * @param key       the item for which a ScriptEngine or ScriptEngineFactory
649     *                  was not found
650     * @param type      the type of key (name, mime type, extension)
651     * @param isFactory true for a not found ScriptEngineFactory, false for a
652     *                  not found ScriptEngine
653     */
654    private ScriptException scriptEngineNotFound(@CheckForNull String key, @Nonnull String type, boolean isFactory) {
655        String expected = String.join(",", names.keySet());
656        String factory = isFactory ? " factory" : "";
657        log.error("Could not find script engine{} for {} \"{}\", expected one of {}", factory, type, key, expected);
658        return new ScriptException(String.format("Could not find script engine%s for %s \"%s\" expected one of %s",
659                factory, type, key, expected));
660    }
661
662    /**
663     * Service routine to make engine-type strings to a human-readable prompt
664     * @param engineName Self-provided name of the engine
665     * @param languageName Names of language supported by the engine
666     * @return Human readable string, i.e. Jython Files
667     */
668    @Nonnull
669    public static String fileForLanguage(@Nonnull String engineName, @Nonnull String languageName) {
670        String language = engineName+"_"+languageName;
671        language = language.replaceAll("\\W+", "_"); // drop white space to _
672
673        try {
674            return Bundle.getMessage(language);
675        } catch (MissingResourceException ex) {
676            log.warn("Translation not found for language \"{}\"", language);
677            if (!language.endsWith(Bundle.getMessage("files"))) { // NOI18N
678                return language + " " + Bundle.getMessage("files");
679            }
680            return language;
681        }
682    }
683
684
685    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JmriScriptEngineManager.class);
686}