001package jmri.jmrit.logixng.expressions; 002 003import java.beans.*; 004import java.io.*; 005import java.nio.charset.StandardCharsets; 006import java.util.*; 007 008import javax.annotation.Nonnull; 009import javax.script.Bindings; 010import javax.script.ScriptException; 011import javax.script.SimpleBindings; 012 013import jmri.InstanceManager; 014import jmri.JmriException; 015import jmri.jmrit.logixng.*; 016import jmri.jmrit.logixng.util.ReferenceUtil; 017import jmri.jmrit.logixng.util.parser.*; 018import jmri.script.ScriptEngineSelector; 019import jmri.util.ThreadingUtil; 020import jmri.util.TypeConversionUtil; 021 022import org.apache.commons.lang3.mutable.MutableBoolean; 023 024/** 025 * Executes a script. 026 * The method evaluate() creates a MutableBoolean with the value "false" and 027 * then sends that value as the variable "result" to the script. The script 028 * then sets the value by the code: "result.setValue(value)" 029 * 030 * @author Daniel Bergqvist Copyright 2021 031 */ 032public class ExpressionScript extends AbstractDigitalExpression 033 implements PropertyChangeListener { 034 035 private NamedBeanAddressing _operationAddressing = NamedBeanAddressing.Direct; 036 private OperationType _operationType = OperationType.SingleLineCommand; 037 private String _operationReference = ""; 038 private String _operationLocalVariable = ""; 039 private String _operationFormula = ""; 040 private ExpressionNode _operationExpressionNode; 041 042 private NamedBeanAddressing _scriptAddressing = NamedBeanAddressing.Direct; 043 private String _script = ""; 044 private String _scriptReference = ""; 045 private String _scriptLocalVariable = ""; 046 private String _scriptFormula = ""; 047 private ExpressionNode _scriptExpressionNode; 048 049 private String _registerScript = ""; 050 private String _unregisterScript = ""; 051 052 private final ScriptEngineSelector _scriptEngineSelector = new ScriptEngineSelector(); 053 054 055 public ExpressionScript(String sys, String user) 056 throws BadUserNameException, BadSystemNameException { 057 super(sys, user); 058 } 059 060 @Override 061 public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException { 062 DigitalExpressionManager manager = InstanceManager.getDefault(DigitalExpressionManager.class); 063 String sysName = systemNames.get(getSystemName()); 064 String userName = userNames.get(getSystemName()); 065 if (sysName == null) sysName = manager.getAutoSystemName(); 066 ExpressionScript copy = new ExpressionScript(sysName, userName); 067 copy.setComment(getComment()); 068 copy.setScript(_script); 069 copy.setOperationAddressing(_operationAddressing); 070 copy.setOperationType(_operationType); 071 copy.setOperationFormula(_operationFormula); 072 copy.setOperationLocalVariable(_operationLocalVariable); 073 copy.setOperationReference(_operationReference); 074 copy.setScriptAddressing(_scriptAddressing); 075 copy.setScriptFormula(_scriptFormula); 076 copy.setScriptLocalVariable(_scriptLocalVariable); 077 copy.setScriptReference(_scriptReference); 078 copy.setRegisterListenerScript(_registerScript); 079 copy.setUnregisterListenerScript(_unregisterScript); 080 return manager.registerExpression(copy); 081 } 082 083 public ScriptEngineSelector getScriptEngineSelector() { 084 return _scriptEngineSelector; 085 } 086 087 public void setOperationAddressing(NamedBeanAddressing addressing) throws ParserException { 088 _operationAddressing = addressing; 089 parseOperationFormula(); 090 } 091 092 public NamedBeanAddressing getOperationAddressing() { 093 return _operationAddressing; 094 } 095 096 public void setOperationType(OperationType operationType) { 097 _operationType = operationType; 098 } 099 100 public OperationType getOperationType() { 101 return _operationType; 102 } 103 104 public void setOperationReference(@Nonnull String reference) { 105 if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) { 106 throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference"); 107 } 108 _operationReference = reference; 109 } 110 111 public String getOperationReference() { 112 return _operationReference; 113 } 114 115 public void setOperationLocalVariable(@Nonnull String localVariable) { 116 _operationLocalVariable = localVariable; 117 } 118 119 public String getOperationLocalVariable() { 120 return _operationLocalVariable; 121 } 122 123 public void setOperationFormula(@Nonnull String formula) throws ParserException { 124 _operationFormula = formula; 125 parseOperationFormula(); 126 } 127 128 public String getOperationFormula() { 129 return _operationFormula; 130 } 131 132 private void parseOperationFormula() throws ParserException { 133 if (_operationAddressing == NamedBeanAddressing.Formula) { 134 Map<String, Variable> variables = new HashMap<>(); 135 136 RecursiveDescentParser parser = new RecursiveDescentParser(variables); 137 _operationExpressionNode = parser.parseExpression(_operationFormula); 138 } else { 139 _operationExpressionNode = null; 140 } 141 } 142 143 public void setScriptAddressing(NamedBeanAddressing addressing) throws ParserException { 144 _scriptAddressing = addressing; 145 parseScriptFormula(); 146 } 147 148 public NamedBeanAddressing getScriptAddressing() { 149 return _scriptAddressing; 150 } 151 152 public void setScript(String script) { 153 if (script == null) _script = ""; 154 else _script = script; 155 } 156 157 public String getScript() { 158 return _script; 159 } 160 161 public void setScriptReference(@Nonnull String reference) { 162 if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) { 163 throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference"); 164 } 165 _scriptReference = reference; 166 } 167 168 public String getScriptReference() { 169 return _scriptReference; 170 } 171 172 public void setScriptLocalVariable(@Nonnull String localVariable) { 173 _scriptLocalVariable = localVariable; 174 } 175 176 public String getScriptLocalVariable() { 177 return _scriptLocalVariable; 178 } 179 180 public void setScriptFormula(@Nonnull String formula) throws ParserException { 181 _scriptFormula = formula; 182 parseScriptFormula(); 183 } 184 185 public String getScriptFormula() { 186 return _scriptFormula; 187 } 188 189 private void parseScriptFormula() throws ParserException { 190 if (_scriptAddressing == NamedBeanAddressing.Formula) { 191 Map<String, Variable> variables = new HashMap<>(); 192 193 RecursiveDescentParser parser = new RecursiveDescentParser(variables); 194 _scriptExpressionNode = parser.parseExpression(_scriptFormula); 195 } else { 196 _scriptExpressionNode = null; 197 } 198 } 199 200 public void setRegisterListenerScript(String script) { 201 if (script == null) _registerScript = ""; 202 else _registerScript = script; 203 } 204 205 public String getRegisterListenerScript() { 206 return _registerScript; 207 } 208 209 public void setUnregisterListenerScript(String script) { 210 if (script == null) _unregisterScript = ""; 211 else _unregisterScript = script; 212 } 213 214 public String getUnregisterListenerScript() { 215 return _unregisterScript; 216 } 217 218 /** {@inheritDoc} */ 219 @Override 220 public Category getCategory() { 221 return Category.ITEM; 222 } 223 224 private String getTheScript() throws JmriException { 225 226 switch (_scriptAddressing) { 227 case Direct: 228 return _script; 229 230 case Reference: 231 return ReferenceUtil.getReference(getConditionalNG().getSymbolTable(), _scriptReference); 232 233 case LocalVariable: 234 SymbolTable symbolTable = getConditionalNG().getSymbolTable(); 235 return TypeConversionUtil 236 .convertToString(symbolTable.getValue(_scriptLocalVariable), false); 237 238 case Formula: 239 return _scriptExpressionNode != null 240 ? TypeConversionUtil.convertToString( 241 _scriptExpressionNode.calculate( 242 getConditionalNG().getSymbolTable()), false) 243 : ""; 244 245 default: 246 throw new IllegalArgumentException("invalid _scriptAddressing state: " + _scriptAddressing.name()); 247 } 248 } 249 250 private OperationType getOperation() throws JmriException { 251 252 String oper = ""; 253 try { 254 switch (_operationAddressing) { 255 case Direct: 256 return _operationType; 257 258 case Reference: 259 oper = ReferenceUtil.getReference( 260 getConditionalNG().getSymbolTable(), _operationReference); 261 return OperationType.valueOf(oper); 262 263 case LocalVariable: 264 SymbolTable symbolTable = getConditionalNG().getSymbolTable(); 265 oper = TypeConversionUtil 266 .convertToString(symbolTable.getValue(_operationLocalVariable), false); 267 return OperationType.valueOf(oper); 268 269 case Formula: 270 if (_scriptExpressionNode != null) { 271 oper = TypeConversionUtil.convertToString( 272 _operationExpressionNode.calculate( 273 getConditionalNG().getSymbolTable()), false); 274 return OperationType.valueOf(oper); 275 } else { 276 return null; 277 } 278 default: 279 throw new IllegalArgumentException("invalid _addressing state: " + _operationAddressing.name()); 280 } 281 } catch (IllegalArgumentException e) { 282 throw new JmriException("Unknown operation: "+oper, e); 283 } 284 } 285 286 /** {@inheritDoc} */ 287 @Override 288 public boolean evaluate() throws JmriException { 289 290 OperationType operation = getOperation(); 291 String script = getTheScript(); 292 293 Bindings bindings = new SimpleBindings(); 294 MutableBoolean result = new MutableBoolean(false); 295 296 LogixNG_ScriptBindings.addScriptBindings(bindings); 297 298 SymbolTable symbolTable = getConditionalNG().getSymbolTable(); 299 bindings.put("symbolTable", symbolTable); // Give the script access to the local variables in the symbol table 300 301 bindings.put("result", result); // Give the script access to the local variable 'result' 302 303 ThreadingUtil.runOnLayoutWithJmriException(() -> { 304 ScriptEngineSelector.Engine engine = 305 _scriptEngineSelector.getSelectedEngine(); 306 307 if (engine == null) throw new JmriException("Script engine is null"); 308 309 switch (operation) { 310 case RunScript: 311 try (InputStreamReader reader = new InputStreamReader( 312 new FileInputStream(jmri.util.FileUtil.getExternalFilename(script)), 313 StandardCharsets.UTF_8)) { 314 engine.getScriptEngine().eval(reader, bindings); 315 } catch (IOException | ScriptException e) { 316 log.warn("cannot execute script", e); 317 } 318 break; 319 320 case SingleLineCommand: 321 try { 322 String theScript; 323 if (engine.isJython()) { 324 theScript = String.format("import jmri%n") + script; 325 } else { 326 theScript = script; 327 } 328 engine.getScriptEngine().eval(theScript, bindings); 329 } catch (ScriptException e) { 330 log.warn("cannot execute script", e); 331 } 332 break; 333 334 default: 335 throw new IllegalArgumentException("invalid _stateAddressing state: " + _scriptAddressing.name()); 336 } 337 }); 338 339 return result.booleanValue(); 340 } 341 342 @Override 343 public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException { 344 throw new UnsupportedOperationException("Not supported."); 345 } 346 347 @Override 348 public int getChildCount() { 349 return 0; 350 } 351 352 @Override 353 public String getShortDescription(Locale locale) { 354 return Bundle.getMessage(locale, "ExpressionScript_Short"); 355 } 356 357 @Override 358 public String getLongDescription(Locale locale) { 359 String operation; 360 String script; 361 362 switch (_operationAddressing) { 363 case Direct: 364 operation = Bundle.getMessage(locale, "AddressByDirect", _operationType._text); 365 break; 366 367 case Reference: 368 operation = Bundle.getMessage(locale, "AddressByReference", _operationReference); 369 break; 370 371 case LocalVariable: 372 operation = Bundle.getMessage(locale, "AddressByLocalVariable", _operationLocalVariable); 373 break; 374 375 case Formula: 376 operation = Bundle.getMessage(locale, "AddressByFormula", _operationFormula); 377 break; 378 379 default: 380 throw new IllegalArgumentException("invalid _operationAddressing state: " + _operationAddressing.name()); 381 } 382 383 switch (_scriptAddressing) { 384 case Direct: 385 script = Bundle.getMessage(locale, "AddressByDirect", _script); 386 break; 387 388 case Reference: 389 script = Bundle.getMessage(locale, "AddressByReference", _scriptReference); 390 break; 391 392 case LocalVariable: 393 script = Bundle.getMessage(locale, "AddressByLocalVariable", _scriptLocalVariable); 394 break; 395 396 case Formula: 397 script = Bundle.getMessage(locale, "AddressByFormula", _scriptFormula); 398 break; 399 400 default: 401 throw new IllegalArgumentException("invalid _stateAddressing state: " + _scriptAddressing.name()); 402 } 403 404 if (_operationAddressing == NamedBeanAddressing.Direct) { 405 return Bundle.getMessage(locale, "ExpressionScript_Long", operation, script); 406 } else { 407 return Bundle.getMessage(locale, "ExpressionScript_LongUnknownOper", operation, script); 408 } 409 } 410 411 /** {@inheritDoc} */ 412 @Override 413 public void setup() { 414 // Do nothing 415 } 416 417 /** {@inheritDoc} */ 418 @Override 419 public void registerListenersForThisClass() { 420 if (!_listenersAreRegistered) { 421 _listenersAreRegistered = true; 422 423 if (!_registerScript.trim().isEmpty()) { 424 Bindings bindings = new SimpleBindings(); 425 MutableBoolean result = new MutableBoolean(false); 426 427 LogixNG_ScriptBindings.addScriptBindings(bindings); 428 429 bindings.put("result", result); // Give the script access to the local variable 'result' 430 431 bindings.put("self", this); // Give the script access to myself with the local variable 'self' 432 433 ThreadingUtil.runOnLayout(() -> { 434 ScriptEngineSelector.Engine engine = 435 _scriptEngineSelector.getSelectedEngine(); 436 if (engine == null) { 437 log.error("Script engine is null", new JmriException()); 438 return; 439 } 440 441 try { 442 String theScript; 443 if (engine.isJython()) { 444 theScript = String.format("import jmri%n") + _registerScript; 445 } else { 446 theScript = _registerScript; 447 } 448 engine.getScriptEngine().eval(theScript, bindings); 449 } catch (RuntimeException | ScriptException e) { 450 log.warn("cannot execute script during registerListeners", e); 451 } 452 }); 453 } 454 } 455 } 456 457 /** {@inheritDoc} */ 458 @Override 459 public void unregisterListenersForThisClass() { 460 if (_listenersAreRegistered) { 461 _listenersAreRegistered = false; 462 463 if (!_unregisterScript.trim().isEmpty()) { 464 Bindings bindings = new SimpleBindings(); 465 MutableBoolean result = new MutableBoolean(false); 466 467 LogixNG_ScriptBindings.addScriptBindings(bindings); 468 469 bindings.put("result", result); // Give the script access to the local variable 'result' 470 471 bindings.put("self", this); // Give the script access to myself with the local variable 'self' 472 473 ThreadingUtil.runOnLayout(() -> { 474 ScriptEngineSelector.Engine engine = 475 _scriptEngineSelector.getSelectedEngine(); 476 if (engine == null) { 477 log.error("Script engine is null", new JmriException()); 478 return; 479 } 480 481 try { 482 String theScript; 483 if (engine.isJython()) { 484 theScript = String.format("import jmri%n") + _unregisterScript; 485 } else { 486 theScript = _unregisterScript; 487 } 488 engine.getScriptEngine().eval(theScript, bindings); 489 } catch (RuntimeException | ScriptException e) { 490 log.warn("cannot execute script during unregisterListeners", e); 491 } 492 }); 493 } 494 } 495 } 496 497 /** {@inheritDoc} */ 498 @Override 499 public void propertyChange(PropertyChangeEvent evt) { 500 getConditionalNG().execute(); 501 } 502 503 /** {@inheritDoc} */ 504 @Override 505 public void disposeMe() { 506 // Do nothing 507 } 508 509 510 public enum OperationType { 511 RunScript(Bundle.getMessage("ExpressionScript_RunScript")), 512 SingleLineCommand(Bundle.getMessage("ExpressionScript_SingleLineCommand")); 513 514 private final String _text; 515 516 private OperationType(String text) { 517 this._text = text; 518 } 519 520 @Override 521 public String toString() { 522 return _text; 523 } 524 525 } 526 527 528 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ExpressionScript.class); 529 530}