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}