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}