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