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 if (JYTHON.equals(engine.getFactory().getEngineName()) && this.jython != null) { 286 this.jython.exec(script); 287 return null; 288 } 289 return engine.eval(script); 290 } 291 292 /** 293 * Evaluate a script contained in a file. Uses the extension of the file to 294 * determine which ScriptEngine to use. 295 * 296 * @param file the script file to evaluate. 297 * @return the results of the evaluation. 298 * @throws javax.script.ScriptException if there is an error evaluating the 299 * script. 300 * @throws java.io.FileNotFoundException if the script file cannot be found. 301 * @throws java.io.IOException if the script file cannot be read. 302 */ 303 public Object eval(File file) throws ScriptException, IOException { 304 return eval(file, null, null); 305 } 306 307 /** 308 * Evaluate a script contained in a file given a set of 309 * {@link javax.script.Bindings} to add to the script's context. Uses the 310 * extension of the file to determine which ScriptEngine to use. 311 * 312 * @param file the script file to evaluate. 313 * @param bindings script bindings to evaluate against. 314 * @return the results of the evaluation. 315 * @throws javax.script.ScriptException if there is an error evaluating the 316 * script. 317 * @throws java.io.FileNotFoundException if the script file cannot be found. 318 * @throws java.io.IOException if the script file cannot be read. 319 */ 320 public Object eval(File file, Bindings bindings) throws ScriptException, IOException { 321 return eval(file, null, bindings); 322 } 323 324 /** 325 * Evaluate a script contained in a file given a special context for the 326 * script. Uses the extension of the file to determine which ScriptEngine to 327 * use. 328 * 329 * @param file the script file to evaluate. 330 * @param context script context to evaluate within. 331 * @return the results of the evaluation. 332 * @throws javax.script.ScriptException if there is an error evaluating the 333 * script. 334 * @throws java.io.FileNotFoundException if the script file cannot be found. 335 * @throws java.io.IOException if the script file cannot be read. 336 */ 337 public Object eval(File file, ScriptContext context) throws ScriptException, IOException { 338 return eval(file, context, null); 339 } 340 341 /** 342 * Evaluate a script contained in a file given a set of 343 * {@link javax.script.Bindings} to add to the script's context. Uses the 344 * extension of the file to determine which ScriptEngine to use. 345 * 346 * @param file the script file to evaluate. 347 * @param context script context to evaluate within. 348 * @param bindings script bindings to evaluate against. 349 * @return the results of the evaluation. 350 * @throws javax.script.ScriptException if there is an error evaluating the 351 * script. 352 * @throws java.io.FileNotFoundException if the script file cannot be found. 353 * @throws java.io.IOException if the script file cannot be read. 354 */ 355 @CheckForNull 356 private Object eval(File file, @CheckForNull ScriptContext context, @CheckForNull Bindings bindings) 357 throws ScriptException, IOException { 358 ScriptEngine engine; 359 Object result = null; 360 if ((engine = getEngineOrEval(file)) != null) { 361 try (InputStreamReader reader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) { 362 if (context != null) { 363 result = engine.eval(reader, context); 364 } else if (bindings != null) { 365 result = engine.eval(reader, bindings); 366 } else { 367 result = engine.eval(reader); 368 } 369 } 370 } 371 return result; 372 } 373 374 /** 375 * Get the ScriptEngine to evaluate the file with; if not using a 376 * ScriptEngine to evaluate Python files, evaluate the file with a 377 * {@link org.python.util.PythonInterpreter} and do not return a 378 * ScriptEngine. 379 * 380 * @param file the script file to evaluate. 381 * @return the ScriptEngine or null if evaluated with a PythonInterpreter. 382 * @throws javax.script.ScriptException if there is an error evaluating the 383 * script. 384 * @throws java.io.FileNotFoundException if the script file cannot be found. 385 * @throws java.io.IOException if the script file cannot be read. 386 */ 387 @CheckForNull 388 private ScriptEngine getEngineOrEval(File file) throws ScriptException, IOException { 389 ScriptEngine engine = this.getEngine(FilenameUtils.getExtension(file.getName()), EXTENSION); 390 if (JYTHON.equals(engine.getFactory().getEngineName()) && this.jython != null) { 391 try (FileInputStream fi = new FileInputStream(file)) { 392 this.jython.execfile(fi); 393 } 394 return null; 395 } 396 return engine; 397 } 398 399 /** 400 * Run a script, suppressing common errors. Note that the file needs to have 401 * a registered extension, or a NullPointerException will be thrown. 402 * <p> 403 * <strong>Note:</strong> this will eventually be deprecated in favor of using 404 * {@link #eval(File)} and having callers handle exceptions. 405 * 406 * @param file the script to run. 407 */ 408 public void runScript(File file) { 409 try { 410 this.eval(file); 411 } catch (FileNotFoundException ex) { 412 log.error("File {} not found.", file); 413 } catch (IOException ex) { 414 log.error("Exception working with file {}", file); 415 } catch (ScriptException ex) { 416 log.error("Error in script {}.", file, ex); 417 } 418 419 } 420 421 /** 422 * Initialize all ScriptEngines. This can be used to prevent the on-demand 423 * initialization of a ScriptEngine from causing a pause in JMRI. 424 */ 425 public void initializeAllEngines() { 426 this.factories.keySet().stream().forEach(this::getEngine); 427 } 428 429 /** 430 * Get the default {@link javax.script.ScriptContext} for all 431 * {@link javax.script.ScriptEngine}s. 432 * 433 * @return the default ScriptContext; 434 */ 435 @Nonnull 436 public ScriptContext getDefaultContext() { 437 return this.context; 438 } 439 440 /** 441 * Given a file extension, get the ScriptEngineFactory registered to handle 442 * that extension. 443 * 444 * @param extension a file extension 445 * @return a ScriptEngineFactory or null 446 * @throws ScriptException if unable to get a matching ScriptEngineFactory 447 */ 448 @Nonnull 449 public ScriptEngineFactory getFactoryByExtension(String extension) throws ScriptException { 450 return getFactory(extension, EXTENSION); 451 } 452 453 /** 454 * Given a mime type, get the ScriptEngineFactory registered to handle that 455 * mime type. 456 * 457 * @param mimeType the script mimeType 458 * @return a ScriptEngineFactory or null 459 * @throws ScriptException if unable to get a matching ScriptEngineFactory 460 */ 461 @Nonnull 462 public ScriptEngineFactory getFactoryByMimeType(String mimeType) throws ScriptException { 463 return getFactory(mimeType, "mime type"); 464 } 465 466 /** 467 * Given a short name, get the ScriptEngineFactory registered by that name. 468 * 469 * @param shortName the short name for the factory 470 * @return a ScriptEngineFactory or null 471 * @throws ScriptException if unable to get a matching ScriptEngineFactory 472 */ 473 @Nonnull 474 public ScriptEngineFactory getFactoryByName(String shortName) throws ScriptException { 475 return getFactory(shortName, "name"); 476 } 477 478 @Nonnull 479 private ScriptEngineFactory getFactory(@CheckForNull String factoryName, @Nonnull String type) 480 throws ScriptException { 481 String name = this.names.get(factoryName); 482 ScriptEngineFactory factory = getFactory(name); 483 if (name == null || factory == null) { 484 throw scriptEngineNotFound(factoryName, type, true); 485 } 486 return factory; 487 } 488 489 /** 490 * Get a ScriptEngineFactory by its name(s), mime types, or supported 491 * extensions. 492 * 493 * @param name the complete name, mime type, or extension for a factory 494 * @return a ScriptEngineFactory or null 495 */ 496 @CheckForNull 497 public ScriptEngineFactory getFactory(@CheckForNull String name) { 498 if (!factories.containsKey(name)) { 499 name = names.get(name); 500 } 501 return this.factories.get(name); 502 } 503 504 /** 505 * The Python ScriptEngine can be configured using a custom 506 * python.properties file and will run jmri_defaults.py if found in the 507 * user's configuration profile or settings directory. See python.properties 508 * in the JMRI installation directory for details of how to configure the 509 * Python ScriptEngine. 510 */ 511 public void initializePython() { 512 if (!this.engines.containsKey(JYTHON)) { 513 initializePythonInterpreter(initializePythonState()); 514 } 515 } 516 517 /** 518 * Create a new PythonInterpreter with the default bindings. 519 * 520 * @return a new interpreter 521 */ 522 public PythonInterpreter newPythonInterpreter() { 523 initializePython(); 524 PythonInterpreter pi = new PythonInterpreter(); 525 context.getBindings(ScriptContext.GLOBAL_SCOPE).forEach(pi::set); 526 return pi; 527 } 528 529 /** 530 * Initialize the Python ScriptEngine state including Python global state. 531 * 532 * @return true if the Python interpreter will be used outside a 533 * ScriptEngine; false otherwise 534 */ 535 private boolean initializePythonState() { 536 // Get properties for interpreter 537 // Search in user files, the profile directory, the settings directory, 538 // and in the program path in that order 539 InputStream is = FileUtil.findInputStream("python.properties", 540 FileUtil.getUserFilesPath(), 541 FileUtil.getProfilePath(), 542 FileUtil.getPreferencesPath(), 543 FileUtil.getProgramPath()); 544 Properties properties; 545 properties = new Properties(System.getProperties()); 546 properties.setProperty("python.console.encoding", StandardCharsets.UTF_8.name()); // NOI18N 547 properties.setProperty("python.cachedir", FileUtil 548 .getAbsoluteFilename(properties.getProperty("python.cachedir", "settings:jython/cache"))); // NOI18N 549 boolean execJython = false; 550 if (is != null) { 551 String pythonPath = "python.path"; 552 try { 553 properties.load(is); 554 String path = properties.getProperty(pythonPath, ""); 555 if (path.length() != 0) { 556 path = path.concat(File.pathSeparator); 557 } 558 properties.setProperty(pythonPath, path.concat(FileUtil.getScriptsPath() 559 .concat(File.pathSeparator).concat(FileUtil.getAbsoluteFilename("program:jython")))); 560 execJython = Boolean.valueOf(properties.getProperty("jython.exec", Boolean.toString(execJython))); 561 } catch (IOException ex) { 562 log.error("Found, but unable to read python.properties: {}", ex.getMessage()); 563 } 564 log.debug("Jython path is {}", PySystemState.getBaseProperties().getProperty(pythonPath)); 565 } 566 PySystemState.initialize(null, properties); 567 return execJython; 568 } 569 570 /** 571 * Initialize the Python ScriptEngine and interpreter, including running any 572 * code in {@value #JYTHON_DEFAULTS}, if present. 573 * 574 * @param execJython true if also initializing an independent interpreter; 575 * false otherwise 576 */ 577 private void initializePythonInterpreter(boolean execJython) { 578 // Create the interpreter 579 try { 580 log.debug("create interpreter"); 581 ScriptEngine python = this.manager.getEngineByName(JYTHON); 582 python.setContext(this.context); 583 engines.put(JYTHON, python); 584 InputStream is = FileUtil.findInputStream(JYTHON_DEFAULTS, 585 FileUtil.getUserFilesPath(), 586 FileUtil.getProfilePath(), 587 FileUtil.getPreferencesPath()); 588 if (execJython) { 589 jython = newPythonInterpreter(); 590 } 591 if (is != null) { 592 python.eval(new InputStreamReader(is)); 593 if (this.jython != null) { 594 this.jython.execfile(is); 595 } 596 } 597 } catch (ScriptException e) { 598 log.error("Exception creating jython system objects", e); 599 } 600 } 601 602 // package private for unit testing 603 @CheckForNull 604 PythonInterpreter getPythonInterpreter() { 605 return jython; 606 } 607 608 /** 609 * Helper to handle logging and exceptions. 610 * 611 * @param key the item for which a ScriptEngine or ScriptEngineFactory 612 * was not found 613 * @param type the type of key (name, mime type, extension) 614 * @param isFactory true for a not found ScriptEngineFactory, false for a 615 * not found ScriptEngine 616 */ 617 private ScriptException scriptEngineNotFound(@CheckForNull String key, @Nonnull String type, boolean isFactory) { 618 String expected = String.join(",", names.keySet()); 619 String factory = isFactory ? " factory" : ""; 620 log.error("Could not find script engine{} for {} \"{}\", expected one of {}", factory, type, key, expected); 621 return new ScriptException(String.format("Could not find script engine%s for %s \"%s\" expected one of %s", 622 factory, type, key, expected)); 623 } 624 625 /** 626 * Service routine to make engine-type strings to a human-readable prompt 627 * @param engineName Self-provided name of the engine 628 * @param languageName Names of language supported by the engine 629 * @return Human readable string, i.e. Jython Files 630 */ 631 @Nonnull 632 public static String fileForLanguage(@Nonnull String engineName, @Nonnull String languageName) { 633 String language = engineName+"_"+languageName; 634 language = language.replaceAll("\\W+", "_"); // drop white space to _ 635 636 try { 637 return Bundle.getMessage(language); 638 } catch (MissingResourceException ex) { 639 log.warn("Translation not found for language \"{}\"", language); 640 if (!language.endsWith(Bundle.getMessage("files"))) { // NOI18N 641 return language + " " + Bundle.getMessage("files"); 642 } 643 return language; 644 } 645 } 646 647 648 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JmriScriptEngineManager.class); 649}