001package jmri.implementation;
002
003import java.awt.GraphicsEnvironment;
004import java.beans.PropertyChangeEvent;
005import java.text.MessageFormat;
006import java.util.ArrayList;
007import java.util.BitSet;
008import java.util.List;
009
010import javax.annotation.CheckForNull;
011import javax.annotation.Nonnull;
012import javax.swing.*;
013
014import jmri.*;
015import jmri.jmrit.logix.OBlock;
016import jmri.jmrit.logix.Warrant;
017import jmri.util.ThreadingUtil;
018
019/**
020 * Class providing the basic logic of the Conditional interface.
021 *
022 * @author Dave Duchamp Copyright (C) 2007
023 * @author Pete Cressman Copyright (C) 2009, 2010, 2011
024 * @author Matthew Harris copyright (C) 2009
025 * @author Egbert Broerse i18n 2016
026 */
027public class DefaultConditional extends AbstractNamedBean
028        implements Conditional {
029
030    static final java.util.ResourceBundle conditionalRbx = java.util.ResourceBundle
031        .getBundle("jmri.jmrit.conditional.ConditionalBundle");
032
033    private final DefaultConditionalExecute conditionalExecute;
034
035    public DefaultConditional(String systemName, String userName) {
036        super(systemName, userName);
037        conditionalExecute = new DefaultConditionalExecute(this);
038    }
039
040    public DefaultConditional(String systemName) {
041        super(systemName);
042        conditionalExecute = new DefaultConditionalExecute(this);
043    }
044
045    @Override
046    public String getBeanType() {
047        return Bundle.getMessage("BeanNameConditional");  // NOI18N
048    }
049
050    // boolean expression of state variables
051    private String _antecedent = "";
052    private Conditional.AntecedentOperator _logicType =
053            Conditional.AntecedentOperator.ALL_AND;
054    // variables (antecedent) parameters
055    private List<ConditionalVariable> _variableList = new ArrayList<>();
056    // actions (consequent) parameters
057    protected List<ConditionalAction> _actionList = new ArrayList<>();
058
059    private int _currentState = NamedBean.UNKNOWN;
060    private boolean _triggerActionsOnChange = true;
061
062    public static int getIndexInTable(int[] table, int entry) {
063        for (int i = 0; i < table.length; i++) {
064            if (entry == table[i]) {
065                return i;
066            }
067        }
068        return -1;
069    }
070
071    /**
072     * Get antecedent (boolean string expression) of Conditional.
073     */
074    @Override
075    public String getAntecedentExpression() {
076        return _antecedent;
077    }
078
079    /**
080     * Get type of operators in the antecedent statement.
081     */
082    @Override
083    public Conditional.AntecedentOperator getLogicType() {
084        return _logicType;
085    }
086
087    /**
088     * Set the logic type (all AND's all OR's or mixed AND's and OR's set the
089     * antecedent expression - should be a well formed boolean statement with
090     * parenthesis indicating the order of evaluation)
091     *
092     * @param type index of the logic type
093     * @param antecedent non-localized (US-english) string description of antecedent
094     */
095    @Override
096    public void setLogicType(Conditional.AntecedentOperator type, String antecedent) {
097        _logicType = type;
098        _antecedent = antecedent; // non-localised (universal) string description
099        setState(NamedBean.UNKNOWN);
100    }
101
102    @Override
103    public boolean getTriggerOnChange() {
104        return _triggerActionsOnChange;
105    }
106
107    @Override
108    public void setTriggerOnChange(boolean trigger) {
109        _triggerActionsOnChange = trigger;
110    }
111
112    /**
113     * Set State Variables for this Conditional. Each state variable will
114     * evaluate either True or False when this Conditional is calculated.
115     * <p>
116     * This method assumes that all information has been validated.
117     */
118    @Override
119    public void setStateVariables(@Nonnull List<ConditionalVariable> arrayList) {
120        log.debug("Conditional \"{}\" ({}) updated ConditionalVariable list.",
121                getUserName(), getSystemName());  // NOI18N
122        _variableList = arrayList;
123    }
124
125    /**
126     * Make deep clone of variables.
127     */
128    @Override
129    @Nonnull
130    public List<ConditionalVariable> getCopyOfStateVariables() {
131        ArrayList<ConditionalVariable> variableList = new ArrayList<>();
132        for (int i = 0; i < _variableList.size(); i++) {
133            ConditionalVariable variable = _variableList.get(i);
134            ConditionalVariable clone = new ConditionalVariable();
135            clone.setNegation(variable.isNegated());
136            clone.setOpern(variable.getOpern());
137            clone.setType(variable.getType());
138            clone.setName(variable.getName());
139            clone.setDataString(variable.getDataString());
140            clone.setNum1(variable.getNum1());
141            clone.setNum2(variable.getNum2());
142            clone.setTriggerActions(variable.doTriggerActions());
143            clone.setState(variable.getState());
144            clone.setGuiName(variable.getGuiName());
145            variableList.add(clone);
146        }
147        return variableList;
148    }
149
150    /**
151     * Get the list of state variables for this Conditional.
152     *
153     * @return the list of state variables
154     */
155    public List<ConditionalVariable> getStateVariableList() {
156        return _variableList;
157    }
158
159    /**
160     * Set list of actions.
161     */
162    @Override
163    public void setAction(@Nonnull List<ConditionalAction> arrayList) {
164        _actionList = arrayList;
165    }
166
167    /**
168     * Make deep clone of actions.
169     */
170    @Override
171    @Nonnull
172    public List<ConditionalAction> getCopyOfActions() {
173        ArrayList<ConditionalAction> actionList = new ArrayList<>();
174        for (int i = 0; i < _actionList.size(); i++) {
175            ConditionalAction action = _actionList.get(i);
176            ConditionalAction clone = new DefaultConditionalAction();
177            clone.setType(action.getType());
178            clone.setOption(action.getOption());
179            clone.setDeviceName(action.getDeviceName());
180            clone.setActionData(action.getActionData());
181            clone.setActionString(action.getActionString());
182            actionList.add(clone);
183        }
184        return actionList;
185    }
186
187    /**
188     * Get the list of actions for this conditional.
189     *
190     * @return the list of actions
191     */
192    @Nonnull
193    public List<ConditionalAction> getActionList() {
194        return _actionList;
195    }
196
197    /**
198     * Calculate this Conditional.
199     * When _enabled is false, Conditional.calculate will compute the state of
200     * the conditional, but will not trigger its actions.
201     * When _enabled is true, the state is computed and if the state
202     * has changed, will trigger all its actions.
203     * @param enable true to enable, else false.
204     */
205    @Override
206    public int calculate(final boolean enable, PropertyChangeEvent evt) {
207        log.trace("calculate starts for {}", getSystemName());  // NOI18N
208
209        // check if  there are no state variables
210        if (_variableList.isEmpty()) {
211            // if there are no state variables, no state can be calculated
212            setState(NamedBean.UNKNOWN);
213            return _currentState;
214        }
215        boolean result = true;
216        switch (_logicType) {
217            case ALL_AND:
218                for (int i = 0; (i < _variableList.size()) && result; i++) {
219                    result = _variableList.get(i).evaluate();
220                }
221                break;
222            case ALL_OR:
223                result = false;
224                for (int k = 0; (k < _variableList.size()) && !result; k++) {
225                    result = _variableList.get(k).evaluate();
226                }
227                break;
228            case MIXED:
229                char[] ch = _antecedent.toCharArray();
230                int n = 0;
231                for (int j = 0; j < ch.length; j++) {
232                    if (ch[j] != ' ') {
233                        if (ch[j] == '{' || ch[j] == '[') {
234                            ch[j] = '(';
235                        } else if (ch[j] == '}' || ch[j] == ']') {
236                            ch[j] = ')';
237                        }
238                        ch[n++] = ch[j];
239                    }
240                }
241                try {
242                    DataPair dp = parseCalculate(new String(ch, 0, n), _variableList);
243                    result = dp.result;
244                } catch (NumberFormatException | IndexOutOfBoundsException | JmriException e) {
245                    result = false;
246                    log.error("{} parseCalculation error antecedent= {}, ex= {}",
247                        getDisplayName(), _antecedent, e.toString(), e);
248                }
249                break;
250            default:
251                log.warn("Conditional {} fell through switch in calculate", getSystemName());  // NOI18N
252                break;
253        }
254        int newState = FALSE;
255        log.debug("Conditional \"{}\" ({}) has calculated its state to be {}. current state is {}. enabled= {}",
256                getUserName(), getSystemName(), result, _currentState, enable);  // NOI18N
257        if (result) {
258            newState = TRUE;
259        }
260
261        boolean enabled = enable;
262        log.trace("   enabled starts at {}", enabled);
263
264        if (enabled && evt != null) {
265            // check if the current listener wants to (NOT) trigger actions
266            enabled = wantsToTrigger(evt);
267            log.trace("   wantsToTrigger sets enabled to {}", enabled);
268        }
269        if (_triggerActionsOnChange
270                // pre 1/15/2011 on change only behavior
271                && newState == _currentState) {
272            enabled = false;
273            log.trace("   _triggerActionsOnChange sets enabled to false");
274        }
275        setState(newState);
276        if (enabled) {
277            takeActionIfNeeded();
278        }
279        return _currentState;
280    }
281
282    /**
283     * Check if an event will trigger actions.
284     *
285     * @param evt the event that possibly triggers actions
286     * @return true if event will trigger actions; false otherwise
287     */
288    boolean wantsToTrigger(PropertyChangeEvent evt) {
289        try {
290            String sysName = ((NamedBean) evt.getSource()).getSystemName();
291            String userName = ((NamedBean) evt.getSource()).getUserName();
292            for (int i = 0; i < _variableList.size(); i++) {
293                if (sysName.equals(_variableList.get(i).getName())) {
294                    return _variableList.get(i).doTriggerActions();
295                }
296            }
297            if (userName != null) {
298                for (int i = 0; i < _variableList.size(); i++) {
299                    if (userName.equals(_variableList.get(i).getName())) {
300                        return _variableList.get(i).doTriggerActions();
301                    }
302                }
303            }
304        } catch (ClassCastException e) {
305            log.error("{} PropertyChangeEvent source of unexpected type: {}", getDisplayName(), evt);  // NOI18N
306        }
307        return true;
308    }
309
310    private static class DataPair {
311        boolean result = false;
312        int indexCount = 0;         // index reached when parsing completed
313        BitSet argsUsed = null;     // error detection for missing arguments
314    }
315
316    /**
317     * Check that an antecedent is well formed.
318     *
319     * @param ant the antecedent string description
320     * @param variableList arraylist of existing Conditional variables
321     * @return error message string if not well formed
322     */
323    @Override
324    @CheckForNull
325    public String validateAntecedent(@Nonnull String ant, List<ConditionalVariable> variableList) {
326        char[] ch = ant.toCharArray();
327        int n = 0;
328        for (int j = 0; j < ch.length; j++) {
329            if (ch[j] != ' ') {
330                if (ch[j] == '{' || ch[j] == '[') {
331                    ch[j] = '(';
332                } else if (ch[j] == '}' || ch[j] == ']') {
333                    ch[j] = ')';
334                }
335                ch[n++] = ch[j];
336            }
337        }
338        int count = 0;
339        for (int j = 0; j < n; j++) {
340            if (ch[j] == '(') {
341                count++;
342            }
343            if (ch[j] == ')') {
344                count--;
345            }
346        }
347        if (count > 0) {
348            return MessageFormat.format(conditionalRbx.getString("ParseError7"), ')');
349        }
350        if (count < 0) {
351            return MessageFormat.format(conditionalRbx.getString("ParseError7"), '(');
352        }
353        try {
354            DataPair dp = parseCalculate(new String(ch, 0, n), variableList);
355            if (n != dp.indexCount) {
356                return MessageFormat.format(conditionalRbx.getString("ParseError4"), ch[dp.indexCount - 1]);
357            }
358            int index = dp.argsUsed.nextClearBit(0);
359            if (index >= 0 && index < variableList.size()) {
360                return MessageFormat.format(conditionalRbx.getString("ParseError5"),
361                    variableList.size(), index + 1);
362            }
363        } catch (NumberFormatException | IndexOutOfBoundsException | JmriException nfe) {
364            return conditionalRbx.getString("ParseError6") + nfe.getMessage();
365        }
366        return null;
367    }
368
369    /**
370     * Parses and computes one parenthesis level of a boolean statement.
371     * <p>
372     * Recursively calls inner parentheses levels. Note that all logic operators
373     * are detected by the parsing, therefore the internal negation of a
374     * variable is washed.
375     *
376     * @param expression   The expression to be parsed
377     * @param variableList ConditionalVariables for R1, R2, etc
378     * @return a data pair consisting of the truth value of the level a count of
379     *         the indices consumed to parse the level and a bitmap of the
380     *         variable indices used.
381     * @throws jmri.JmriException if unable to compute the logic
382     */
383    DataPair parseCalculate(String expression, List<ConditionalVariable> variableList)
384            throws JmriException {
385
386        // for simplicity, we force the string to upper case before scanning
387        String s = expression.toUpperCase();
388
389        BitSet argsUsed = new BitSet(_variableList.size());
390        DataPair dp;
391        boolean leftArg = false;
392        boolean rightArg = false;
393        int oper;
394        int k;
395        int i = 0;      // index of String s
396        //int numArgs = 0;
397        if (s.charAt(i) == '(') {
398            dp = parseCalculate(s.substring(++i), variableList);
399            leftArg = dp.result;
400            i += dp.indexCount;
401            argsUsed.or(dp.argsUsed);
402        } else // cannot be '('.  must be either leftArg or notleftArg
403        {
404            if (s.charAt(i) == 'R') {  // NOI18N
405                try {
406                    k = Integer.parseInt(String.valueOf(s.substring(i + 1, i + 3)));
407                    i += 2;
408                } catch (NumberFormatException | IndexOutOfBoundsException nfe) {
409                    k = Integer.parseInt(String.valueOf(s.charAt(++i)));
410                }
411                leftArg = variableList.get(k - 1).evaluate();
412                if (variableList.get(k - 1).isNegated()) {
413                    leftArg = !leftArg;
414                }
415                i++;
416                argsUsed.set(k - 1);
417            } else if ("NOT".equals(s.substring(i, i + 3))) {  // NOI18N
418                i += 3;
419
420                // not leftArg
421                if (s.charAt(i) == '(') {
422                    dp = parseCalculate(s.substring(++i), variableList);
423                    leftArg = dp.result;
424                    i += dp.indexCount;
425                    argsUsed.or(dp.argsUsed);
426                } else if (s.charAt(i) == 'R') {  // NOI18N
427                    try {
428                        k = Integer.parseInt(String.valueOf(s.substring(i + 1, i + 3)));
429                        i += 2;
430                    } catch (NumberFormatException | IndexOutOfBoundsException nfe) {
431                        k = Integer.parseInt(String.valueOf(s.charAt(++i)));
432                    }
433                    leftArg = variableList.get(k - 1).evaluate();
434                    if (variableList.get(k - 1).isNegated()) {
435                        leftArg = !leftArg;
436                    }
437                    i++;
438                    argsUsed.set(k - 1);
439                } else {
440                    throw new JmriException(MessageFormat.format(
441                        conditionalRbx.getString("ParseError1"), s.substring(i)));
442                }
443                leftArg = !leftArg;
444            } else {
445                throw new JmriException(MessageFormat.format(conditionalRbx.getString("ParseError9"), s));
446            }
447        }
448        // crank away to the right until a matching parent is reached
449        while (i < s.length()) {
450            if (s.charAt(i) != ')') {
451                // must be either AND or OR
452                if ("AND".equals(s.substring(i, i + 3))) {  // NOI18N
453                    i += 3;
454                    oper = OPERATOR_AND;
455                } else if ("OR".equals(s.substring(i, i + 2))) {  // NOI18N
456                    i += 2;
457                    oper = OPERATOR_OR;
458                } else {
459                    throw new JmriException(MessageFormat.format(
460                        conditionalRbx.getString("ParseError2"), s.substring(i)));
461                }
462                if (s.charAt(i) == '(') {
463                    dp = parseCalculate(s.substring(++i), variableList);
464                    rightArg = dp.result;
465                    i += dp.indexCount;
466                    argsUsed.or(dp.argsUsed);
467                } else // cannot be '('.  must be either rightArg or notRightArg
468                {
469                    if (s.charAt(i) == 'R') {  // NOI18N
470                        try {
471                            k = Integer.parseInt(String.valueOf(s.substring(i + 1, i + 3)));
472                            i += 2;
473                        } catch (NumberFormatException | IndexOutOfBoundsException nfe) {
474                            k = Integer.parseInt(String.valueOf(s.charAt(++i)));
475                        }
476                        rightArg = variableList.get(k - 1).evaluate();
477                        if (variableList.get(k - 1).isNegated()) {
478                            rightArg = !rightArg;
479                        }
480                        i++;
481                        argsUsed.set(k - 1);
482                    } else if ("NOT".equals(s.substring(i, i + 3))) {  // NOI18N
483                        i += 3;
484                        // not rightArg
485                        if (s.charAt(i) == '(') {
486                            dp = parseCalculate(s.substring(++i), variableList);
487                            rightArg = dp.result;
488                            i += dp.indexCount;
489                            argsUsed.or(dp.argsUsed);
490                        } else if (s.charAt(i) == 'R') {  // NOI18N
491                            try {
492                                k = Integer.parseInt(String.valueOf(s.substring(i + 1, i + 3)));
493                                i += 2;
494                            } catch (NumberFormatException | IndexOutOfBoundsException nfe) {
495                                k = Integer.parseInt(String.valueOf(s.charAt(++i)));
496                            }
497                            rightArg = variableList.get(k - 1).evaluate();
498                            if (variableList.get(k - 1).isNegated()) {
499                                rightArg = !rightArg;
500                            }
501                            i++;
502                            argsUsed.set(k - 1);
503                        } else {
504                            throw new JmriException(MessageFormat.format(
505                                conditionalRbx.getString("ParseError3"), s.substring(i)));
506                        }
507                        rightArg = !rightArg;
508                    } else {
509                        throw new JmriException(MessageFormat.format(
510                            conditionalRbx.getString("ParseError9"), s.substring(i)));
511                    }
512                }
513                if (oper == OPERATOR_AND) {
514                    leftArg = (leftArg && rightArg);
515                } else if (oper == OPERATOR_OR) {
516                    leftArg = (leftArg || rightArg);
517                }
518            } else {  // This level done, pop recursion
519                i++;
520                break;
521            }
522        }
523        dp = new DataPair();
524        dp.result = leftArg;
525        dp.indexCount = i;
526        dp.argsUsed = argsUsed;
527        return dp;
528    }
529
530    /**
531     * Compares action options, and takes action if appropriate
532     * <p>
533     * Only get here if a change in state has occurred when calculating this
534     * Conditional
535     */
536    private void takeActionIfNeeded() {
537        log.trace("takeActionIfNeeded starts for {}", getSystemName());
538        Reference<Integer> actionCount = new Reference<>(0);
539        int actionNeeded = 0;
540        List<String> errorList = new ArrayList<>();
541        // Use a local copy of state to guarantee the entire list of actions will be fired off
542        // before a state change occurs that may block their completion.
543        int currentState = _currentState;
544        for (int i = 0; i < _actionList.size(); i++) {
545            ConditionalAction action = _actionList.get(i);
546            int neededAction = actionNeeded;
547            int option = action.getOption();
548            log.trace(" takeActionIfNeeded considers action {} with currentState: {} and option: {}",
549                i, currentState, option);
550
551            if (((currentState == TRUE) && (option == ACTION_OPTION_ON_CHANGE_TO_TRUE))
552                    || ((currentState == FALSE) && (option == ACTION_OPTION_ON_CHANGE_TO_FALSE))
553                    || (option == ACTION_OPTION_ON_CHANGE)) {
554                // need to take this action
555                actionNeeded++;
556                NamedBean nb = null;
557                var anb = action.getNamedBean();
558                if ( anb != null) {
559                    nb = anb.getBean();
560                }
561                Conditional.Action type = action.getType();
562                String devName = getDeviceName(action);
563                if (devName == null) {
564                    errorList.add("invalid memory name in action - " + action);  // NOI18N
565                    continue;
566                }
567                log.debug("getDeviceName()={} devName= {}", action.getDeviceName(), devName);
568
569                switch (type) {
570                    case NONE:
571                        break;
572                    case SET_TURNOUT:
573                        conditionalExecute.setTurnout(action, (Turnout) nb, actionCount, errorList);
574                        break;
575                    case RESET_DELAYED_TURNOUT:
576                        conditionalExecute.delayedTurnout(action, actionCount, new TimeTurnout(i), true, devName);
577                        break;
578                    case DELAYED_TURNOUT:
579                        conditionalExecute.delayedTurnout(action, actionCount, new TimeTurnout(i), false, devName);
580                        break;
581                    case CANCEL_TURNOUT_TIMERS:
582                        conditionalExecute.cancelTurnoutTimers(action, actionCount, errorList, devName);
583                        break;
584                    case LOCK_TURNOUT:
585                        conditionalExecute.lockTurnout(action, (Turnout) nb, actionCount, errorList);
586                        break;
587                    case SET_SIGNAL_APPEARANCE:
588                        conditionalExecute.setSignalAppearance(action, (SignalHead) nb, actionCount, errorList);
589                        break;
590                    case SET_SIGNAL_HELD:
591                        conditionalExecute.setSignalHeld(action, (SignalHead) nb, actionCount, errorList);
592                        break;
593                    case CLEAR_SIGNAL_HELD:
594                        conditionalExecute.clearSignalHeld(action, (SignalHead) nb, actionCount, errorList);
595                        break;
596                    case SET_SIGNAL_DARK:
597                        conditionalExecute.setSignalDark(action, (SignalHead) nb, actionCount, errorList);
598                        break;
599                    case SET_SIGNAL_LIT:
600                        conditionalExecute.setSignalLit(action, (SignalHead) nb, actionCount, errorList);
601                        break;
602                    case TRIGGER_ROUTE:
603                        conditionalExecute.triggerRoute(action, (Route) nb, actionCount, errorList);
604                        break;
605                    case SET_SENSOR:
606                        conditionalExecute.setSensor(action, (Sensor) nb, actionCount, errorList, devName);
607                        break;
608                    case RESET_DELAYED_SENSOR:
609                        conditionalExecute.delayedSensor(action, actionCount,
610                            new TimeSensor(i), getMillisecondValue(action), true, devName);
611                        break;
612                    case DELAYED_SENSOR:
613                        conditionalExecute.delayedSensor(action, actionCount,
614                            new TimeSensor(i), getMillisecondValue(action), false, devName);
615                        break;
616                    case CANCEL_SENSOR_TIMERS:
617                        conditionalExecute.cancelSensorTimers(action, actionCount, errorList, devName);
618                        break;
619                    case SET_LIGHT:
620                        conditionalExecute.setLight(action, (Light) nb, actionCount, errorList);
621                        break;
622                    case SET_LIGHT_INTENSITY:
623                        conditionalExecute.setLightIntensity(action, (Light) nb,
624                            getIntegerValue(action), actionCount, errorList);
625                        break;
626                    case SET_LIGHT_TRANSITION_TIME:
627                        conditionalExecute.setLightTransitionTime(action, (Light) nb,
628                            getIntegerValue(action), actionCount, errorList);
629                        break;
630                    case SET_MEMORY:
631                        conditionalExecute.setMemory(action, (Memory) nb, actionCount, errorList);
632                        break;
633                    case COPY_MEMORY:
634                        conditionalExecute.copyMemory(action, (Memory) nb, getMemory(action.getActionString()),
635                            getActionString(action), actionCount, errorList);
636                        break;
637                    case ENABLE_LOGIX:
638                        conditionalExecute.enableLogix(action, actionCount, errorList, devName);
639                        break;
640                    case DISABLE_LOGIX:
641                        conditionalExecute.disableLogix(action, actionCount, errorList, devName);
642                        break;
643                    case PLAY_SOUND:
644                        conditionalExecute.playSound(action, getActionString(action), actionCount, errorList);
645                        break;
646                    case RUN_SCRIPT:
647                        conditionalExecute.runScript(action, getActionString(action), actionCount);
648                        break;
649                    case SET_FAST_CLOCK_TIME:
650                        conditionalExecute.setFastClockTime(action, actionCount);
651                        break;
652                    case START_FAST_CLOCK:
653                        conditionalExecute.startFastClock(actionCount);
654                        break;
655                    case STOP_FAST_CLOCK:
656                        conditionalExecute.stopFastClock(actionCount);
657                        break;
658                    case CONTROL_AUDIO:
659                        conditionalExecute.controlAudio(action, devName);
660                        break;
661                    case JYTHON_COMMAND:
662                        conditionalExecute.jythonCommand(action, getActionString(action), actionCount);
663                        break;
664                    case ALLOCATE_WARRANT_ROUTE:
665                        conditionalExecute.allocateWarrantRoute(action, (Warrant) nb, actionCount, errorList);
666                        break;
667                    case DEALLOCATE_WARRANT_ROUTE:
668                        conditionalExecute.deallocateWarrantRoute(action, (Warrant) nb, actionCount, errorList);
669                        break;
670                    case SET_ROUTE_TURNOUTS:
671                        conditionalExecute.setRouteTurnouts(action, (Warrant) nb, actionCount, errorList);
672                        break;
673                    case GET_TRAIN_LOCATION:
674                        conditionalExecute.getTrainLocation(action, (Warrant) nb, getMemory(action.getActionString()),
675                            getActionString(action), actionCount, errorList);
676                        break;
677                    case SET_TRAIN_ID:
678                        conditionalExecute.setTrainId(action, (Warrant) nb,
679                            getActionString(action), actionCount, errorList);
680                        break;
681                    case SET_TRAIN_NAME:
682                        conditionalExecute.setTrainName(action, (Warrant) nb,
683                            getActionString(action), actionCount, errorList);
684                        break;
685                    case AUTO_RUN_WARRANT:
686                        conditionalExecute.autoRunWarrant(action, (Warrant) nb, actionCount, errorList);
687                        break;
688                    case MANUAL_RUN_WARRANT:
689                        conditionalExecute.manualRunWarrant(action, (Warrant) nb, actionCount, errorList);
690                        break;
691                    case CONTROL_TRAIN:
692                        conditionalExecute.controlTrain(action, (Warrant) nb, actionCount, errorList, devName);
693                        break;
694                    case SET_SIGNALMAST_ASPECT:
695                        conditionalExecute.setSignalMastAspect(action, (SignalMast) nb,
696                            getActionString(action), actionCount, errorList);
697                        break;
698                    case SET_SIGNALMAST_HELD:
699                        conditionalExecute.setSignalMastHeld(action, (SignalMast) nb, actionCount, errorList);
700                        break;
701                    case CLEAR_SIGNALMAST_HELD:
702                        conditionalExecute.clearSignalMastHeld(action, (SignalMast) nb, actionCount, errorList);
703                        break;
704                    case SET_SIGNALMAST_DARK:
705                        conditionalExecute.setSignalMastDark(action, (SignalMast) nb, actionCount, errorList);
706                        break;
707                    case SET_SIGNALMAST_LIT:
708                        conditionalExecute.setSignalMastLit(action, (SignalMast) nb, actionCount, errorList);
709                        break;
710                    case SET_BLOCK_VALUE:
711                        conditionalExecute.setBlockValue(action, (OBlock) nb,
712                            getActionString(action), actionCount, errorList);
713                        break;
714                    case SET_BLOCK_ERROR:
715                        conditionalExecute.setBlockError(action, (OBlock) nb, actionCount, errorList);
716                        break;
717                    case CLEAR_BLOCK_ERROR:
718                        conditionalExecute.clearBlockError(action, (OBlock) nb, errorList);
719                        break;
720                    case DEALLOCATE_BLOCK:
721                        conditionalExecute.deallocateBlock(action, (OBlock) nb, actionCount, errorList);
722                        break;
723                    case SET_BLOCK_OUT_OF_SERVICE:
724                        conditionalExecute.setBlockOutOfService(action, (OBlock) nb, actionCount, errorList);
725                        break;
726                    case SET_BLOCK_IN_SERVICE:
727                        conditionalExecute.setBlockInService(action, (OBlock) nb, actionCount, errorList);
728                        break;
729                    case GET_BLOCK_TRAIN_NAME:
730                        conditionalExecute.getBlockTrainName(action, (OBlock) nb, getMemory(action.getActionString()),
731                            getActionString(action), actionCount, errorList);
732                        break;
733                    case GET_BLOCK_WARRANT:
734                        conditionalExecute.getBlockWarrant(action, (OBlock) nb, getMemory(action.getActionString()),
735                            getActionString(action), actionCount, errorList);
736                        break;
737                    case SET_NXPAIR_ENABLED:
738                        conditionalExecute.setNXPairEnabled(action, actionCount, errorList, devName);
739                        break;
740                    case SET_NXPAIR_DISABLED:
741                        conditionalExecute.setNXPairDisabled(action, actionCount, errorList, devName);
742                        break;
743                    case SET_NXPAIR_SEGMENT:
744                        conditionalExecute.setNXPairSegment(action, actionCount, errorList, devName);
745                        break;
746                    default:
747                        log.warn("takeActionIfNeeded drops through switch statement for action {} of {}",
748                            i, getSystemName());
749                        break;
750                }
751            }
752            if (log.isDebugEnabled()) {
753                log.debug("Global state= {} Local state= {} - Action {} taken for action = {} {} for device {}",
754                    _currentState, currentState, actionNeeded > neededAction ? "WAS" : "NOT",
755                    action.getTypeString(), action.getActionString(), action.getDeviceName());
756            }
757        }
758        if (!errorList.isEmpty()) {
759            for (int i = 0; i < errorList.size(); i++) {
760                log.error(" error: {} - {}", getDisplayName(), errorList.get(i));
761            }
762            if (!GraphicsEnvironment.isHeadless()) {
763                java.awt.Toolkit.getDefaultToolkit().beep();
764                if (!skipErrorDialog) {
765                    ThreadingUtil.runOnGUI( () ->
766                        new ErrorDialog(errorList, this).setVisible(true));
767                }
768            }
769        }
770        if (log.isDebugEnabled()) {
771            log.debug("Conditional \"{}\" ({} has {} actions and has executed {} actions of {} "
772                    + "actions needed on state change to {}",
773                getUserName(), getSystemName(), _actionList.size(), actionCount, actionNeeded, currentState);  // NOI18N
774        }
775    }   // takeActionIfNeeded
776
777    private static volatile boolean skipErrorDialog = false;
778
779    private static synchronized void setSkipErrorDialog( boolean skip ) {
780        skipErrorDialog = skip;
781    }
782
783    private class ErrorDialog extends JDialog {
784
785        JCheckBox rememberSession;
786
787        ErrorDialog(List<String> list, Conditional cond) {
788            super();
789            setTitle("Logix Runtime Errors");  // NOI18N
790            JPanel contentPanel = new JPanel();
791            contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
792            JPanel panel = new JPanel();
793            panel.add(new JLabel("Errors occurred executing Actions in Conditional:"));  // NOI18N
794            contentPanel.add(panel);
795
796            panel = new JPanel();
797            panel.add(new JLabel(getDisplayName(DisplayOptions.USERNAME_SYSTEMNAME)));
798            contentPanel.add(panel);
799
800            panel = new JPanel();
801            panel.add(new JList<>(list.toArray(new String[0])));
802            contentPanel.add(panel);
803
804            panel = new JPanel();
805            rememberSession = new JCheckBox("Skip error dialog for this session only?");  // NOI18N
806            panel.add(rememberSession);
807            contentPanel.add(panel);
808
809            panel = new JPanel();
810            JButton closeButton = new JButton("Close");  // NOI18N
811            closeButton.addActionListener( e -> {
812                DefaultConditional.setSkipErrorDialog(rememberSession.isSelected());
813                this.dispose();
814            });
815            panel.add(closeButton);
816            contentPanel.add(panel);
817            setContentPane(contentPanel);
818            setLocation(250, 150);
819            pack();
820        }
821    }
822
823    private String getDeviceName(@Nonnull ConditionalAction action) {
824        String devName = action.getDeviceName();
825        if (devName != null && devName.length() > 0 && devName.charAt(0) == '@') {
826            String memName = devName.substring(1);
827            Memory m = getMemory(memName);
828            if (m == null) {
829                log.error("{} invalid memory name in action - {}", getDisplayName(), devName);  // NOI18N
830                return null;
831            }
832            devName = (String) m.getValue();
833        }
834        return devName;
835    }
836
837    private String getActionString(@Nonnull ConditionalAction action) {
838        String devAction = action.getActionString();
839        if ( !devAction.isEmpty() && devAction.charAt(0) == '@') {
840            String memName = devAction.substring(1);
841            Memory m = getMemory(memName);
842            if (m == null) {
843                log.error("{} action \"{}\" has invalid memory name in actionString - {}",
844                    getDisplayName(), action.getDeviceName(), action.getActionString());
845                return "";
846            }
847            devAction = (String) m.getValue();
848        }
849        return devAction;
850    }
851
852    /**
853     * for backward compatibility with config files having system names in lower
854     * case
855     */
856    @CheckForNull
857    private static Memory getMemory(String name) {
858        return InstanceManager.memoryManagerInstance().getMemory(name);
859    }
860
861    /**
862     * Get an integer from either a String literal or named memory reference.
863     *
864     * @param action an action containing either an integer or name of a Memory
865     * @return the integral value of the action or -1 if the action references a
866     *         Memory that does not contain an integral value
867     */
868    int getIntegerValue( @Nonnull ConditionalAction action) {
869        String sNumber = action.getActionString();
870        int time;
871        try {
872            time = Integer.parseInt(sNumber);
873        } catch (NumberFormatException e) {
874            if (sNumber.charAt(0) == '@') {
875                sNumber = sNumber.substring(1);
876            }
877            Memory mem = getMemory(sNumber);
878            if (mem == null) {
879                log.error("invalid memory name for action time variable - {}, for Action \"{}\", "
880                    + "in Conditional \"{}\" ({})",
881                    sNumber, action.getTypeString(), getUserName(), getSystemName());
882                return -1;
883            }
884            try {
885                time = Integer.parseInt((String) mem.getValue());
886            } catch (NumberFormatException ex) {
887                log.error("invalid action number variable from memory, \"{}\" ({}), "
888                    + "value = {}, for Action \"{}\", in Conditional \"{}\" ({})",
889                    getUserName(), mem.getSystemName(), mem.getValue(),
890                    action.getTypeString(), getUserName(), getSystemName());
891                return -1;
892            }
893        }
894        return time;
895    }
896
897    /**
898     * Get the number of milliseconds from either a String literal or named
899     * memory reference containing a value representing a number of seconds.
900     * <p>
901     * The String is not I18N and should use a . decimal seperater.
902     * @param action an action containing either a number of seconds or name of
903     *               a Memory
904     * @return the number of milliseconds represented by action of -1 if action
905     *         references a Memory without a numeric value
906     */
907    int getMillisecondValue(@Nonnull ConditionalAction action) {
908        String sNumber = action.getActionString();
909        float time;
910        try {
911            time = Float.parseFloat(sNumber);
912        } catch (NumberFormatException e) {
913            if (sNumber.charAt(0) == '@') {
914                sNumber = sNumber.substring(1);
915            }
916            Memory mem = getMemory(sNumber);
917            if (mem == null) {
918                log.error("invalid memory name for action time variable - {}, "
919                    + "for Action \"{}\", in Conditional \"{}\" ({})",
920                    sNumber, action.getTypeString(), getUserName(), getSystemName());
921                return -1;
922            }
923            try {
924                time = Float.parseFloat((String) mem.getValue());
925            } catch (NumberFormatException ex) {
926                time = -1;
927            }
928            if (time <= 0) {
929                log.error("invalid Millisecond value from memory, \"{}\" ({}), "
930                    + "value = {}, for Action \"{}\", in Conditional \"{}\" ({})",
931                    getUserName(), mem.getSystemName(), mem.getValue(),
932                    action.getTypeString(), getUserName(), getSystemName());
933            }
934        }
935        return (int) (time * 1000);
936    }
937
938    /**
939     * Stop a sensor timer if one is actively delaying setting of the specified
940     * sensor
941     */
942    @Override
943    public void cancelSensorTimer(String sname) {
944        for (int i = 0; i < _actionList.size(); i++) {
945            ConditionalAction action = _actionList.get(i);
946            if ((action.getType() == Conditional.Action.DELAYED_SENSOR)
947                    || (action.getType() == Conditional.Action.RESET_DELAYED_SENSOR)) {
948                if (action.isTimerActive()) {
949                    String devName = getDeviceName(action);
950                    if ( devName == null ) {
951                        log.error("When cancelling S Timer Could not locate Device Name for Sensor {}", sname);
952                        return;
953                    }
954                    // have active set sensor timer - is it for our sensor?
955                    if (devName.equals(sname)) {
956                        // yes, names match, cancel timer
957                        action.stopTimer();
958                    } else {
959                        // check if same sensor by a different name
960                        Sensor sn = InstanceManager.sensorManagerInstance().getSensor(devName);
961                        if (sn == null) {
962                            log.error("{} Unknown sensor *{} in cancelSensorTimer.",
963                                getDisplayName(), action.getDeviceName());
964                        } else if (sname.equals(sn.getSystemName())
965                                || sname.equals(sn.getUserName())) {
966                            // same sensor, cancel timer
967                            action.stopTimer();
968                        }
969                    }
970                }
971            }
972        }
973    }
974
975    /**
976     * Stop a turnout timer if one is actively delaying setting of the specified
977     * turnout
978     */
979    @Override
980    public void cancelTurnoutTimer(String sname) {
981        for (int i = 0; i < _actionList.size(); i++) {
982            ConditionalAction action = _actionList.get(i);
983            if ((action.getType() == Conditional.Action.DELAYED_TURNOUT)
984                    || (action.getType() == Conditional.Action.RESET_DELAYED_TURNOUT)) {
985                if (action.isTimerActive()) {
986                    // have active set turnout timer - is it for our turnout?
987                    String devName = getDeviceName(action);
988                    if ( devName == null ) {
989                        log.error("When cancelling T Timer Could not locate Device Name for Turnout {}", sname);
990                        return;
991                    }
992                    if (devName.equals(sname)) {
993                        // yes, names match, cancel timer
994                        action.stopTimer();
995                    } else {
996                        // check if same turnout by a different name
997                        Turnout tn = InstanceManager.turnoutManagerInstance().getTurnout(devName);
998                        if (tn == null) {
999                            log.error("{} Unknown turnout *{} in cancelTurnoutTimer.",
1000                                getDisplayName(), action.getDeviceName());
1001                        } else if (sname.equals(tn.getSystemName())
1002                                || sname.equals(tn.getUserName())) {
1003                            // same turnout, cancel timer
1004                            action.stopTimer();
1005                        }
1006                    }
1007                }
1008            }
1009        }
1010    }
1011
1012    /**
1013     * State of the Conditional is returned.
1014     *
1015     * @return state value
1016     */
1017    @Override
1018    public int getState() {
1019        return _currentState;
1020    }
1021
1022    /**
1023     * State of Conditional is set. Not really public for Conditionals. The
1024     * state of a Conditional is only changed by its calculate method, so the
1025     * state is really a read-only bound property.
1026     *
1027     * @param state the new state
1028     */
1029    @Override
1030    public void setState(int state) {
1031        if (_currentState != state) {
1032            int oldState = _currentState;
1033            _currentState = state;
1034            firePropertyChange(NamedBean.PROPERTY_KNOWN_STATE, oldState, _currentState);
1035        }
1036    }
1037
1038    /**
1039     * Dispose this DefaultConditional.
1040     */
1041    @Override
1042    public void dispose() {
1043        super.dispose();
1044        for (int i = 0; i < _actionList.size(); i++) {
1045            _actionList.get(i).dispose();
1046        }
1047    }
1048
1049    /**
1050     * Class for defining ActionListener for ACTION_DELAYED_SENSOR
1051     */
1052    class TimeSensor implements java.awt.event.ActionListener {
1053
1054        TimeSensor(int index) {
1055            mIndex = index;
1056        }
1057
1058        private int mIndex = 0;
1059
1060        @Override
1061        public void actionPerformed(java.awt.event.ActionEvent event) {
1062            // set sensor state
1063            ConditionalAction action = _actionList.get(mIndex);
1064            if (action.getNamedBean() == null) {
1065                log.error("{} Invalid delayed sensor name - {}", getDisplayName(), action.getDeviceName());
1066            } else {
1067                // set the sensor
1068
1069                var anb = action.getNamedBean();
1070                if ( anb == null ) {
1071                    log.error("No NamedBean for Acrion {}", action.getActionString());
1072                    return;
1073                }
1074                Sensor s = (Sensor) anb.getBean();
1075                try {
1076                    int act = action.getActionData();
1077                    if (act == Route.TOGGLE) {
1078                        // toggle from current state
1079                        int state = s.getKnownState();
1080                        if (state == Sensor.ACTIVE) {
1081                            act = Sensor.INACTIVE;
1082                        } else {
1083                            act = Sensor.ACTIVE;
1084                        }
1085                    }
1086                    s.setKnownState(act);
1087                } catch (JmriException e) {
1088                    log.warn("Exception setting delayed sensor {} in action", action.getDeviceName());  // NOI18N
1089                }
1090            }
1091            // Turn Timer OFF
1092            action.stopTimer();
1093        }
1094    }
1095
1096    /**
1097     * Class for defining ActionListener for ACTION_DELAYED_TURNOUT
1098     */
1099    class TimeTurnout implements java.awt.event.ActionListener {
1100
1101        TimeTurnout(int index) {
1102            mIndex = index;
1103        }
1104
1105        private int mIndex = 0;
1106
1107        @Override
1108        public void actionPerformed(java.awt.event.ActionEvent event) {
1109            // set turnout state
1110            ConditionalAction action = _actionList.get(mIndex);
1111            var beanHandle = action.getNamedBean();
1112            if (beanHandle == null) {
1113                log.error("{} Invalid delayed turnout name - {}", getDisplayName(), action.getDeviceName());
1114            } else {
1115                Turnout t = (Turnout) beanHandle.getBean();
1116                int act = action.getActionData();
1117                if (act == Route.TOGGLE) {
1118                    // toggle from current state
1119                    int state = t.getKnownState();
1120                    if (state == Turnout.CLOSED) {
1121                        act = Turnout.THROWN;
1122                    } else {
1123                        act = Turnout.CLOSED;
1124                    }
1125                }
1126                // set the turnout
1127                t.setCommandedState(act);
1128            }
1129            // Turn Timer OFF
1130            action.stopTimer();
1131        }
1132    }
1133
1134    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultConditional.class);
1135}