001package jmri.jmrit.logixng.expressions;
002
003import java.beans.*;
004import java.util.*;
005
006import javax.annotation.Nonnull;
007
008import jmri.*;
009import jmri.jmrit.dispatcher.*;
010import jmri.jmrit.logixng.*;
011import jmri.jmrit.logixng.util.DispatcherActiveTrainManager;
012import jmri.jmrit.logixng.util.ReferenceUtil;
013import jmri.jmrit.logixng.util.parser.*;
014import jmri.jmrit.logixng.util.parser.ExpressionNode;
015import jmri.jmrit.logixng.util.parser.RecursiveDescentParser;
016import jmri.util.TypeConversionUtil;
017
018/**
019 * This expression checks the status or mode of an active train.
020 * <p>
021 * A Dispatcher ActiveTrain is a transient object.  The DispatcherActiveTrainManager is a special
022 * LogiNG class which manages the relationships with expressions using the Dispatcher TrainInfo file
023 * as the key. This makes it possible to add and remove ActiveTrain PropertyChange listeners.
024 *
025 * @author Dave Sand Copyright 2021
026 */
027public class ExpressionDispatcher extends AbstractDigitalExpression
028        implements PropertyChangeListener {
029
030    private NamedBeanAddressing _addressing = NamedBeanAddressing.Direct;
031    private String _reference = "";
032    private String _localVariable = "";
033    private String _formula = "";
034    private ExpressionNode _expressionNode;
035
036    private NamedBeanAddressing _stateAddressing = NamedBeanAddressing.Direct;
037    private String _stateReference = "";
038    private String _stateLocalVariable = "";
039    private String _stateFormula = "";
040    private ExpressionNode _stateExpressionNode;
041
042    private String _trainInfoFileName = "";
043    private Is_IsNot_Enum _is_IsNot = Is_IsNot_Enum.Is;
044    private DispatcherState _dispatcherState = DispatcherState.Mode;
045
046    private final DispatcherActiveTrainManager _atManager;
047    private boolean _activeTrainListeners = false;
048
049    /**
050     * An active train is transient.  It can be terminated manually which means the reference
051     * will no longer be valid.
052     */
053    private ActiveTrain _activeTrain = null;
054
055
056    public ExpressionDispatcher(String sys, String user)
057            throws BadUserNameException, BadSystemNameException {
058        super(sys, user);
059        _atManager = InstanceManager.getDefault(DispatcherActiveTrainManager.class);
060    }
061
062    @Override
063    public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws ParserException {
064        DigitalExpressionManager manager = InstanceManager.getDefault(DigitalExpressionManager.class);
065        String sysName = systemNames.get(getSystemName());
066        String userName = userNames.get(getSystemName());
067        if (sysName == null) sysName = manager.getAutoSystemName();
068        ExpressionDispatcher copy = new ExpressionDispatcher(sysName, userName);
069        copy.setComment(getComment());
070
071        copy.setTrainInfoFileName(_trainInfoFileName);
072        copy.setBeanState(getBeanState());
073
074        copy.setBeanState(_dispatcherState);
075        copy.setAddressing(_addressing);
076        copy.setFormula(_formula);
077        copy.setLocalVariable(_localVariable);
078        copy.setReference(_reference);
079
080        copy.set_Is_IsNot(_is_IsNot);
081
082        copy.setStateAddressing(_stateAddressing);
083        copy.setStateFormula(_stateFormula);
084        copy.setStateLocalVariable(_stateLocalVariable);
085        copy.setStateReference(_stateReference);
086
087
088        return manager.registerExpression(copy);
089    }
090
091
092    public void setTrainInfoFileName(@Nonnull String fileName) {
093        _trainInfoFileName = fileName;
094    }
095
096    public String getTrainInfoFileName() {
097        return _trainInfoFileName;
098    }
099
100
101    public void setAddressing(NamedBeanAddressing addressing) throws ParserException {
102        _addressing = addressing;
103        parseFormula();
104    }
105
106    public NamedBeanAddressing getAddressing() {
107        return _addressing;
108    }
109
110    public void setReference(@Nonnull String reference) {
111        if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) {
112            throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference");
113        }
114        _reference = reference;
115    }
116
117    public String getReference() {
118        return _reference;
119    }
120
121    public void setLocalVariable(@Nonnull String localVariable) {
122        _localVariable = localVariable;
123    }
124
125    public String getLocalVariable() {
126        return _localVariable;
127    }
128
129    public void setFormula(@Nonnull String formula) throws ParserException {
130        _formula = formula;
131        parseFormula();
132    }
133
134    public String getFormula() {
135        return _formula;
136    }
137
138    private void parseFormula() throws ParserException {
139        if (_addressing == NamedBeanAddressing.Formula) {
140            Map<String, Variable> variables = new HashMap<>();
141
142            RecursiveDescentParser parser = new RecursiveDescentParser(variables);
143            _expressionNode = parser.parseExpression(_formula);
144        } else {
145            _expressionNode = null;
146        }
147    }
148
149
150    public void set_Is_IsNot(Is_IsNot_Enum is_IsNot) {
151        _is_IsNot = is_IsNot;
152    }
153
154    public Is_IsNot_Enum get_Is_IsNot() {
155        return _is_IsNot;
156    }
157
158
159    public void setStateAddressing(NamedBeanAddressing addressing) throws ParserException {
160        _stateAddressing = addressing;
161        parseStateFormula();
162    }
163
164    public NamedBeanAddressing getStateAddressing() {
165        return _stateAddressing;
166    }
167
168    public void setStateReference(@Nonnull String reference) {
169        if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) {
170            throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference");
171        }
172        _stateReference = reference;
173    }
174
175    public String getStateReference() {
176        return _stateReference;
177    }
178
179    public void setStateLocalVariable(@Nonnull String localVariable) {
180        _stateLocalVariable = localVariable;
181    }
182
183    public String getStateLocalVariable() {
184        return _stateLocalVariable;
185    }
186
187    public void setStateFormula(@Nonnull String formula) throws ParserException {
188        _stateFormula = formula;
189        parseStateFormula();
190    }
191
192    public String getStateFormula() {
193        return _stateFormula;
194    }
195
196    private void parseStateFormula() throws ParserException {
197        if (_stateAddressing == NamedBeanAddressing.Formula) {
198            Map<String, Variable> variables = new HashMap<>();
199
200            RecursiveDescentParser parser = new RecursiveDescentParser(variables);
201            _stateExpressionNode = parser.parseExpression(_stateFormula);
202        } else {
203            _stateExpressionNode = null;
204        }
205    }
206
207
208    public void setBeanState(DispatcherState state) {
209        _dispatcherState = state;
210    }
211
212    public DispatcherState getBeanState() {
213        return _dispatcherState;
214    }
215
216
217    /** {@inheritDoc} */
218    @Override
219    public Category getCategory() {
220        return Category.ITEM;
221    }
222
223    private String getSelectedFileName() throws JmriException {
224        switch (_addressing) {
225            case Direct:
226                return getTrainInfoFileName();
227
228            case Reference:
229                return ReferenceUtil.getReference(
230                        getConditionalNG().getSymbolTable(), _reference);
231
232            case LocalVariable:
233                SymbolTable symbolTable = getConditionalNG().getSymbolTable();
234                return TypeConversionUtil
235                        .convertToString(symbolTable.getValue(_localVariable), false);
236
237            case Formula:
238                return _expressionNode != null ?
239                        TypeConversionUtil.convertToString(_expressionNode.calculate(
240                                getConditionalNG().getSymbolTable()), false)
241                        : "";
242
243            default:
244                throw new IllegalArgumentException("invalid _addressing state: " + _addressing.name());
245        }
246    }
247
248    private String getNewState() throws JmriException {
249
250        switch (_stateAddressing) {
251            case Reference:
252                return ReferenceUtil.getReference(
253                        getConditionalNG().getSymbolTable(), _stateReference);
254
255            case LocalVariable:
256                SymbolTable symbolTable = getConditionalNG().getSymbolTable();
257                return TypeConversionUtil
258                        .convertToString(symbolTable.getValue(_stateLocalVariable), false);
259
260            case Formula:
261                return _stateExpressionNode != null
262                        ? TypeConversionUtil.convertToString(
263                                _stateExpressionNode.calculate(
264                                        getConditionalNG().getSymbolTable()), false)
265                        : null;
266
267            default:
268                throw new IllegalArgumentException("invalid _addressing state: " + _stateAddressing.name());
269        }
270    }
271
272    /** {@inheritDoc} */
273    @Override
274    public boolean evaluate() throws JmriException {
275        String trainInfoFileName = getSelectedFileName();
276
277        if (trainInfoFileName.isEmpty()) {
278            return false;
279        }
280
281        DispatcherState checkDispatcherState;
282        if ((_stateAddressing == NamedBeanAddressing.Direct)) {
283            checkDispatcherState = _dispatcherState;
284        } else {
285            checkDispatcherState = DispatcherState.valueOf(getNewState());
286        }
287
288        boolean result = false;
289
290        switch (checkDispatcherState) {
291            case Automatic:
292                if (_activeTrain != null) result = (_activeTrain.getMode() == ActiveTrain.AUTOMATIC);
293                break;
294            case Dispatched:
295                if (_activeTrain != null) result = (_activeTrain.getMode() == ActiveTrain.DISPATCHED);
296                break;
297            case Manual:
298                if (_activeTrain != null) result = (_activeTrain.getMode() == ActiveTrain.MANUAL);
299                break;
300            case Running:
301                if (_activeTrain != null) result = (_activeTrain.getStatus() == ActiveTrain.RUNNING);
302                break;
303            case Paused:
304                if (_activeTrain != null) result = (_activeTrain.getStatus() == ActiveTrain.PAUSED);
305                break;
306            case Waiting:
307                if (_activeTrain != null) result = (_activeTrain.getStatus() == ActiveTrain.WAITING);
308                break;
309            case Working:
310                if (_activeTrain != null) result = (_activeTrain.getStatus() == ActiveTrain.WORKING);
311                break;
312            case Ready:
313                if (_activeTrain != null) result = (_activeTrain.getStatus() == ActiveTrain.READY);
314                break;
315            case Stopped:
316                if (_activeTrain != null) result = (_activeTrain.getStatus() == ActiveTrain.STOPPED);
317                break;
318            case Done:
319                if (_activeTrain != null) result = (_activeTrain.getStatus() == ActiveTrain.DONE);
320                break;
321
322            default:
323                throw new UnsupportedOperationException("checkDispatcherState has unknown value: " + checkDispatcherState.name());
324        }
325
326        if (_is_IsNot == Is_IsNot_Enum.Is) {
327            return result;
328        } else {
329            return !result;
330        }
331    }
332
333    @Override
334    public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException {
335        throw new UnsupportedOperationException("Not supported.");
336    }
337
338    @Override
339    public int getChildCount() {
340        return 0;
341    }
342
343
344    @Override
345    public String getShortDescription(Locale locale) {
346        return Bundle.getMessage(locale, "Dispatcher_Short");
347    }
348
349    @Override
350    public String getLongDescription(Locale locale) {
351        String fileName;
352        String state;
353
354        switch (_addressing) {
355            case Direct:
356                fileName = Bundle.getMessage(locale, "AddressByDirect", _trainInfoFileName);
357                break;
358
359            case Reference:
360                fileName = Bundle.getMessage(locale, "AddressByReference", _reference);
361                break;
362
363            case LocalVariable:
364                fileName = Bundle.getMessage(locale, "AddressByLocalVariable", _localVariable);
365                break;
366
367            case Formula:
368                fileName = Bundle.getMessage(locale, "AddressByFormula", _formula);
369                break;
370
371            default:
372                throw new IllegalArgumentException("invalid _addressing state: " + _addressing.name());
373        }
374
375        switch (_stateAddressing) {
376            case Direct:
377                state = Bundle.getMessage(locale, "AddressByDirect", _dispatcherState._text);
378                break;
379
380            case Reference:
381                state = Bundle.getMessage(locale, "AddressByReference", _stateReference);
382                break;
383
384            case LocalVariable:
385                state = Bundle.getMessage(locale, "AddressByLocalVariable", _stateLocalVariable);
386                break;
387
388            case Formula:
389                state = Bundle.getMessage(locale, "AddressByFormula", _stateFormula);
390                break;
391
392            default:
393                throw new IllegalArgumentException("invalid _stateAddressing state: " + _stateAddressing.name());
394        }
395
396        return Bundle.getMessage(locale, "Dispatcher_Long", fileName, _is_IsNot.toString(), state);
397    }
398
399    /** {@inheritDoc} */
400    @Override
401    public void setup() {
402        // Do nothing
403    }
404
405    /** {@inheritDoc} */
406    @Override
407    public void registerListenersForThisClass() {
408        if (! _listenersAreRegistered) {
409            _atManager.addPropertyChangeListener(this);
410            _listenersAreRegistered = true;
411        }
412    }
413
414    /** {@inheritDoc} */
415    @Override
416    public void unregisterListenersForThisClass() {
417        if (_listenersAreRegistered) {
418            _atManager.removePropertyChangeListener(this);
419            _listenersAreRegistered = false;
420        }
421    }
422
423    /** {@inheritDoc}
424     * ActiveTrain is created by DispatcherActiveTrainManager
425     * status and mode are created by Dispatcher ActiveTrain
426     */
427    @Override
428    public void propertyChange(PropertyChangeEvent evt) {
429        switch (evt.getPropertyName()) {
430            case "ActiveTrain":
431                manageActiveTrain(evt);
432                break;
433
434            case "mode":
435                if ((int) evt.getNewValue() == ActiveTrain.TERMINATED) {
436                    // The Dispatcher active train was terminated by an external process.
437                    // Force the manager to update the LogixNG active train map.
438                    _atManager.getActiveTrain(_trainInfoFileName);
439                    return;
440                }
441
442                getConditionalNG().execute();
443                break;
444
445            case "status":
446                getConditionalNG().execute();
447                break;
448
449            default:
450                log.debug("Other property changes are ignored: name = {}", evt.getPropertyName());
451        }
452    }
453
454    /**
455     * The DispatcherActiveTrainManager keeps track of the ActiveTrains created using LogixNG.
456     * When an ActiveTrain is added, ActiveTrain property change listeners are added so that the
457     * expression can be notified of status and mode changes.  _activeTrain is updated with the
458     * with current active train.
459     * <p>
460     * When an ActiveTrain is removed, the listeners are removed and _activeTrain is set to null.
461     * @param event The DispatcherActiveTrainManager property change event.
462     */
463    private void manageActiveTrain(PropertyChangeEvent event) {
464        String selectedFileName;
465        try {
466            selectedFileName = getSelectedFileName();
467        } catch (JmriException ex) {
468            log.warn("Unexpected exception, using Direct file name");
469            selectedFileName = _trainInfoFileName;
470        }
471
472        String eventFileName = (String) event.getOldValue();
473        if (eventFileName.isEmpty()) {
474            eventFileName = (String) event.getNewValue();
475        }
476
477        if (! selectedFileName.equals(eventFileName)) return;
478
479        ActiveTrain checkTrain = _atManager.getActiveTrain(selectedFileName);
480
481        if (checkTrain == null) {
482            if (_activeTrain != null) {
483                if (_activeTrainListeners) {
484                    _activeTrain.removePropertyChangeListener(this);
485                    _activeTrainListeners = false;
486                }
487                _activeTrain = null;
488            }
489            return;
490        }
491
492        if (checkTrain == _activeTrain) return;
493
494        if (_activeTrain == null) {
495            _activeTrain = checkTrain;
496            _activeTrainListeners = false;
497        }
498
499        if (! _activeTrainListeners) {
500            _activeTrain.addPropertyChangeListener(this);
501            _activeTrainListeners = true;
502        }
503    }
504
505
506    /** {@inheritDoc} */
507    @Override
508    public void disposeMe() {
509    }
510
511
512    public enum DispatcherState {
513        Mode(Bundle.getMessage("DispatcherSeparatorMode"), "Separator"),
514        Automatic(Bundle.getMessage("DispatcherMode_Automatic"), "Mode"),
515        Dispatched(Bundle.getMessage("DispatcherMode_Dispatched"), "Mode"),
516        Manual(Bundle.getMessage("DispatcherMode_Manual"), "Mode"),
517        Status(Bundle.getMessage("DispatcherSeparatorStatus"), "Separator"),
518        Running(Bundle.getMessage("DispatcherStatus_Running"), "Status"),
519        Paused(Bundle.getMessage("DispatcherStatus_Paused"), "Status"),
520        Waiting(Bundle.getMessage("DispatcherStatus_Waiting"), "Status"),
521        Working(Bundle.getMessage("DispatcherStatus_Working"), "Status"),
522        Ready(Bundle.getMessage("DispatcherStatus_Ready"), "Status"),
523        Stopped(Bundle.getMessage("DispatcherStatus_Stopped"), "Status"),
524        Done(Bundle.getMessage("DispatcherStatus_Done"), "Status");
525
526        private final String _text;
527        private final String _type;
528
529        private DispatcherState(String text, String type) {
530            this._text = text;
531            this._type = type;
532        }
533
534        public String getType() {
535            return _type;
536        }
537
538        @Override
539        public String toString() {
540            return _text;
541        }
542
543    }
544
545
546    /** {@inheritDoc} */
547    @Override
548    public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl) {
549        log.debug("getUsageReport :: ExpressionDispatcher: bean = {}, report = {}", cdl, report);
550    }
551
552    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ExpressionDispatcher.class);
553
554}