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