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