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