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}