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