001package jmri.jmrit.logixng.actions; 002 003import java.beans.PropertyChangeEvent; 004import java.beans.PropertyChangeListener; 005import java.util.*; 006import java.util.concurrent.atomic.AtomicReference; 007 008import jmri.*; 009import jmri.jmrit.logixng.*; 010import jmri.jmrit.logixng.implementation.DefaultSymbolTable; 011import jmri.jmrit.logixng.util.*; 012import jmri.jmrit.logixng.util.parser.*; 013import jmri.util.*; 014 015/** 016 * Executes an action when the expression is True. 017 * 018 * @author Daniel Bergqvist Copyright 2025 019 */ 020public class ForEachWithDelay extends AbstractDigitalAction 021 implements FemaleSocketListener, PropertyChangeListener { 022 023 private final LogixNG_SelectString _selectVariable = 024 new LogixNG_SelectString(this, this); 025 026 private final LogixNG_SelectNamedBean<Memory> _selectMemoryNamedBean = 027 new LogixNG_SelectNamedBean<>( 028 this, Memory.class, InstanceManager.getDefault(MemoryManager.class), this); 029 030 private boolean _useCommonSource = true; 031 private CommonManager _commonManager = CommonManager.Sensors; 032 private UserSpecifiedSource _userSpecifiedSource = UserSpecifiedSource.Variable; 033 private String _formula = ""; 034 private ExpressionNode _expressionNode; 035 private int _delay; 036 private TimerUnit _unit = TimerUnit.MilliSeconds; 037 private String _variableName = ""; 038 private boolean _resetIfAlreadyStarted; 039 private boolean _useIndividualTimers; 040 private String _socketSystemName; 041 private final FemaleDigitalActionSocket _socket; 042 private ProtectedTimerTask _defaultTimerTask; 043 044 private final InternalFemaleSocket _defaultInternalSocket = new InternalFemaleSocket(); 045 046 047 public ForEachWithDelay(String sys, String user) { 048 super(sys, user); 049 _socket = InstanceManager.getDefault(DigitalActionManager.class) 050 .createFemaleSocket(this, this, "A"); 051 } 052 053 @Override 054 public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException { 055 DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class); 056 String sysName = systemNames.get(getSystemName()); 057 String userName = userNames.get(getSystemName()); 058 if (sysName == null) sysName = manager.getAutoSystemName(); 059 ForEachWithDelay copy = new ForEachWithDelay(sysName, userName); 060 copy.setComment(getComment()); 061 copy.setUseCommonSource(_useCommonSource); 062 copy.setCommonManager(_commonManager); 063 copy.setUserSpecifiedSource(_userSpecifiedSource); 064 copy.setDelay(_delay); 065 copy.setUnit(_unit); 066 _selectVariable.copy(copy._selectVariable); 067 _selectMemoryNamedBean.copy(copy._selectMemoryNamedBean); 068 copy.setFormula(_formula); 069 copy.setLocalVariableName(_variableName); 070 copy.setResetIfAlreadyStarted(_resetIfAlreadyStarted); 071 copy.setUseIndividualTimers(_useIndividualTimers); 072 return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames); 073 } 074 075 public LogixNG_SelectString getSelectVariable() { 076 return _selectVariable; 077 } 078 079 public LogixNG_SelectNamedBean<Memory> getSelectMemoryNamedBean() { 080 return _selectMemoryNamedBean; 081 } 082 083 public void setUseCommonSource(boolean commonSource) { 084 this._useCommonSource = commonSource; 085 } 086 087 public boolean isUseCommonSource() { 088 return _useCommonSource; 089 } 090 091 public void setCommonManager(CommonManager commonManager) throws ParserException { 092 _commonManager = commonManager; 093 parseFormula(); 094 } 095 096 public CommonManager getCommonManager() { 097 return _commonManager; 098 } 099 100 public void setUserSpecifiedSource(UserSpecifiedSource userSpecifiedSource) throws ParserException { 101 _userSpecifiedSource = userSpecifiedSource; 102 parseFormula(); 103 } 104 105 public UserSpecifiedSource getUserSpecifiedSource() { 106 return _userSpecifiedSource; 107 } 108 109 public void setFormula(String formula) throws ParserException { 110 _formula = formula; 111 parseFormula(); 112 } 113 114 public String getFormula() { 115 return _formula; 116 } 117 118 private void parseFormula() throws ParserException { 119 if (_userSpecifiedSource == UserSpecifiedSource.Formula) { 120 Map<String, Variable> variables = new HashMap<>(); 121 122 RecursiveDescentParser parser = new RecursiveDescentParser(variables); 123 _expressionNode = parser.parseExpression(_formula); 124 } else { 125 _expressionNode = null; 126 } 127 } 128 129 /** 130 * Get the delay. 131 * @return the delay 132 */ 133 public int getDelay() { 134 return _delay; 135 } 136 137 /** 138 * Set the delay. 139 * @param delay the delay 140 */ 141 public void setDelay(int delay) { 142 _delay = delay; 143 } 144 145 /** 146 * Get the unit 147 * @return the unit 148 */ 149 public TimerUnit getUnit() { 150 return _unit; 151 } 152 153 /** 154 * Set the unit 155 * @param unit the unit 156 */ 157 public void setUnit(TimerUnit unit) { 158 _unit = unit; 159 } 160 161 /** 162 * Get name of local variable 163 * @return name of local variable 164 */ 165 public String getLocalVariableName() { 166 return _variableName; 167 } 168 169 /** 170 * Set name of local variable 171 * @param localVariableName name of local variable 172 */ 173 public void setLocalVariableName(String localVariableName) { 174 _variableName = localVariableName; 175 } 176 177 /** 178 * Get reset if timer is already started. 179 * @return true if the timer should be reset if this action is executed 180 * while timer is ticking, false othervise 181 */ 182 public boolean getResetIfAlreadyStarted() { 183 return _resetIfAlreadyStarted; 184 } 185 186 /** 187 * Set reset if timer is already started. 188 * @param resetIfAlreadyStarted true if the timer should be reset if this 189 * action is executed while timer is ticking, 190 * false othervise 191 */ 192 public void setResetIfAlreadyStarted(boolean resetIfAlreadyStarted) { 193 _resetIfAlreadyStarted = resetIfAlreadyStarted; 194 } 195 196 /** 197 * Get use individual timers. 198 * @return true if the timer should use individual timers, false othervise 199 */ 200 public boolean getUseIndividualTimers() { 201 return _useIndividualTimers; 202 } 203 204 /** 205 * Set reset if timer is already started. 206 * @param useIndividualTimers true if the timer should use individual timers, 207 * false othervise 208 */ 209 public void setUseIndividualTimers(boolean useIndividualTimers) { 210 _useIndividualTimers = useIndividualTimers; 211 } 212 213 /** {@inheritDoc} */ 214 @Override 215 public LogixNG_Category getCategory() { 216 return LogixNG_Category.FLOW_CONTROL; 217 } 218 219 /** {@inheritDoc} */ 220 @Override 221 @SuppressWarnings("unchecked") 222 public void execute() throws JmriException { 223 final AtomicReference<Collection<? extends Object>> collectionRef = new AtomicReference<>(); 224 final AtomicReference<JmriException> ref = new AtomicReference<>(); 225 226 final ConditionalNG conditionalNG = getConditionalNG(); 227 final SymbolTable symbolTable = getConditionalNG().getSymbolTable(); 228 229 if (_useCommonSource) { 230 collectionRef.set(_commonManager.getManager().getNamedBeanSet()); 231 } else { 232 ThreadingUtil.runOnLayoutWithJmriException(() -> { 233 234 Object value = null; 235 236 switch (_userSpecifiedSource) { 237 case Variable: 238 String otherLocalVariable = _selectVariable.evaluateValue(getConditionalNG()); 239 Object variableValue = symbolTable.getValue(otherLocalVariable); 240 241 value = variableValue; 242 break; 243 244 case Memory: 245 Memory memory = _selectMemoryNamedBean.evaluateNamedBean(getConditionalNG()); 246 if (memory != null) { 247 value = memory.getValue(); 248 } else { 249 log.warn("ForEachWithDelay memory is null"); 250 } 251 break; 252 253 case Formula: 254 if (!_formula.isEmpty() && _expressionNode != null) { 255 value = _expressionNode.calculate(conditionalNG.getSymbolTable()); 256 } 257 break; 258 259 default: 260 // Throw exception 261 throw new IllegalArgumentException("_userSpecifiedSource has invalid value: {}" + _userSpecifiedSource.name()); 262 } 263 264 if (value instanceof Manager) { 265 collectionRef.set(((Manager<? extends NamedBean>) value).getNamedBeanSet()); 266 } else if (value != null && value.getClass().isArray()) { 267 // Note: (Object[]) is needed to tell that the parameter is an array and not a vararg argument 268 // See: https://stackoverflow.com/questions/2607289/converting-array-to-list-in-java/2607327#2607327 269 collectionRef.set(Arrays.asList((Object[])value)); 270 } else if (value instanceof Collection) { 271 collectionRef.set((Collection<? extends Object>) value); 272 } else if (value instanceof Map) { 273 collectionRef.set(((Map<?,?>) value).entrySet()); 274 } else { 275 throw new JmriException(Bundle.getMessage("ForEachWithDelay_InvalidValue", 276 value != null ? value.getClass().getName() : null)); 277 } 278 }); 279 } 280 281 if (ref.get() != null) throw ref.get(); 282 283 List<Object> list = new ArrayList<>(collectionRef.get()); 284 285 synchronized(this) { 286 if (!_useIndividualTimers && (_defaultTimerTask != null)) { 287 if (_resetIfAlreadyStarted) _defaultTimerTask.stopTimer(); 288 else return; 289 } 290 long timerDelay = _delay * _unit.getMultiply(); 291 long timerStart = System.currentTimeMillis(); 292 ConditionalNG conditonalNG = getConditionalNG(); 293 scheduleTimer(conditonalNG, conditonalNG.getSymbolTable(), timerDelay, timerStart, list, 0); 294 } 295 } 296 297 /** 298 * Get a new timer task. 299 * @param conditionalNG the ConditionalNG 300 * @param symbolTable the symbol table 301 * @param timerDelay the time the timer should wait 302 * @param timerStart the time when the timer was started 303 */ 304 private ProtectedTimerTask getNewTimerTask( 305 ConditionalNG conditionalNG, 306 SymbolTable symbolTable, 307 long timerDelay, 308 long timerStart, 309 List<? extends Object> list, 310 int nextIndex) 311 throws JmriException { 312 313 DefaultSymbolTable newSymbolTable = new DefaultSymbolTable(symbolTable); 314 315 return new ProtectedTimerTask() { 316 @Override 317 public void execute() { 318 try { 319 synchronized(ForEachWithDelay.this) { 320 if (!_useIndividualTimers) _defaultTimerTask = null; 321 long currentTime = System.currentTimeMillis(); 322 long currentTimerTime = currentTime - timerStart; 323 if (currentTimerTime < timerDelay) { 324 scheduleTimer(conditionalNG, newSymbolTable, timerDelay - currentTimerTime, currentTime, list, nextIndex); 325 } else { 326 InternalFemaleSocket internalSocket; 327 if (_useIndividualTimers) { 328 internalSocket = new InternalFemaleSocket(); 329 } else { 330 internalSocket = _defaultInternalSocket; 331 } 332 internalSocket.conditionalNG = conditionalNG; 333 internalSocket.newSymbolTable = newSymbolTable; 334 internalSocket.newSymbolTable.setValue(_variableName, list.get(nextIndex)); 335 conditionalNG.execute(internalSocket); 336 337 if (nextIndex+1 < list.size()) { 338 scheduleTimer(conditionalNG, newSymbolTable, timerDelay, currentTime, list, nextIndex+1); 339 } 340 } 341 } 342 } catch (RuntimeException | JmriException e) { 343 log.error("Exception thrown", e); 344 } 345 } 346 }; 347 } 348 349 private void scheduleTimer( 350 ConditionalNG conditionalNG, 351 SymbolTable symbolTable, 352 long timerDelay, 353 long timerStart, 354 List<? extends Object> list, 355 int nextIndex) 356 throws JmriException { 357 358 synchronized(ForEachWithDelay.this) { 359 if (!_useIndividualTimers && (_defaultTimerTask != null)) { 360 _defaultTimerTask.stopTimer(); 361 } 362 ProtectedTimerTask timerTask = 363 getNewTimerTask(conditionalNG, symbolTable, timerDelay, timerStart, list, nextIndex); 364 if (!_useIndividualTimers) { 365 _defaultTimerTask = timerTask; 366 } 367 TimerUtil.schedule(timerTask, timerDelay); 368 } 369 } 370 371 @Override 372 public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException { 373 switch (index) { 374 case 0: 375 return _socket; 376 377 default: 378 throw new IllegalArgumentException( 379 String.format("index has invalid value: %d", index)); 380 } 381 } 382 383 @Override 384 public int getChildCount() { 385 return 1; 386 } 387 388 @Override 389 public void connected(FemaleSocket socket) { 390 if (socket == _socket) { 391 _socketSystemName = socket.getConnectedSocket().getSystemName(); 392 } else { 393 throw new IllegalArgumentException("unkown socket"); 394 } 395 } 396 397 @Override 398 public void disconnected(FemaleSocket socket) { 399 if (socket == _socket) { 400 _socketSystemName = null; 401 } else { 402 throw new IllegalArgumentException("unkown socket"); 403 } 404 } 405 406 @Override 407 public String getShortDescription(Locale locale) { 408 return Bundle.getMessage(locale, "ForEachWithDelay_Short"); 409 } 410 411 @Override 412 public String getLongDescription(Locale locale) { 413 if (_useCommonSource) { 414 return Bundle.getMessage(locale, "ForEachWithDelay_Long_Common", 415 _commonManager.toString(), _variableName, _socket.getName(), _unit.getTimeWithUnit(_delay), 416 _resetIfAlreadyStarted 417 ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_ResetRepeat")) 418 : Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_IgnoreRepeat")), 419 _useIndividualTimers 420 ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_UseIndividualTimers")) 421 : ""); 422 } else { 423 switch (_userSpecifiedSource) { 424 case Variable: 425 return Bundle.getMessage(locale, "ForEachWithDelay_Long_LocalVariable", 426 _selectVariable.getDescription(locale), _variableName, _socket.getName(), _unit.getTimeWithUnit(_delay), 427 _resetIfAlreadyStarted 428 ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_ResetRepeat")) 429 : Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_IgnoreRepeat")), 430 _useIndividualTimers 431 ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_UseIndividualTimers")) 432 : ""); 433 434 case Memory: 435 return Bundle.getMessage(locale, "ForEachWithDelay_Long_Memory", 436 _selectMemoryNamedBean.getDescription(locale), _variableName, _socket.getName(), _unit.getTimeWithUnit(_delay), 437 _resetIfAlreadyStarted 438 ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_ResetRepeat")) 439 : Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_IgnoreRepeat")), 440 _useIndividualTimers 441 ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_UseIndividualTimers")) 442 : ""); 443 444 case Formula: 445 return Bundle.getMessage(locale, "ForEachWithDelay_Long_Formula", 446 _formula, _variableName, _socket.getName(), _unit.getTimeWithUnit(_delay), 447 _resetIfAlreadyStarted 448 ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_ResetRepeat")) 449 : Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_IgnoreRepeat")), 450 _useIndividualTimers 451 ? Bundle.getMessage("ForEachWithDelay_Options", Bundle.getMessage("ForEachWithDelay_UseIndividualTimers")) 452 : ""); 453 454 default: 455 throw new IllegalArgumentException("_variableOperation has invalid value: " + _userSpecifiedSource.name()); 456 } 457 } 458 } 459 460 public FemaleDigitalActionSocket getSocket() { 461 return _socket; 462 } 463 464 public String getSocketSystemName() { 465 return _socketSystemName; 466 } 467 468 public void setSocketSystemName(String systemName) { 469 _socketSystemName = systemName; 470 } 471 472 /** {@inheritDoc} */ 473 @Override 474 public void setup() { 475 try { 476 if ( !_socket.isConnected() 477 || !_socket.getConnectedSocket().getSystemName() 478 .equals(_socketSystemName)) { 479 480 String socketSystemName = _socketSystemName; 481 _socket.disconnect(); 482 if (socketSystemName != null) { 483 MaleSocket maleSocket = 484 InstanceManager.getDefault(DigitalActionManager.class) 485 .getBySystemName(socketSystemName); 486 _socket.disconnect(); 487 if (maleSocket != null) { 488 _socket.connect(maleSocket); 489 maleSocket.setup(); 490 } else { 491 log.error("cannot load digital action {}", socketSystemName); 492 } 493 } 494 } else { 495 _socket.getConnectedSocket().setup(); 496 } 497 } catch (SocketAlreadyConnectedException ex) { 498 // This shouldn't happen and is a runtime error if it does. 499 throw new RuntimeException("socket is already connected"); 500 } 501 } 502 503 /** {@inheritDoc} */ 504 @Override 505 public void registerListenersForThisClass() { 506 if (!_listenersAreRegistered) { 507 if (_userSpecifiedSource == UserSpecifiedSource.Memory) { 508 _selectMemoryNamedBean.registerListeners(); 509 } 510 _listenersAreRegistered = true; 511 } 512 } 513 514 /** {@inheritDoc} */ 515 @Override 516 public void unregisterListenersForThisClass() { 517 if (_listenersAreRegistered) { 518 if (_userSpecifiedSource == UserSpecifiedSource.Memory) { 519 _selectMemoryNamedBean.unregisterListeners(); 520 } 521 _listenersAreRegistered = false; 522 } 523 } 524 525 /** {@inheritDoc} */ 526 @Override 527 public void disposeMe() { 528 } 529 530 /** {@inheritDoc} */ 531 @Override 532 public void propertyChange(PropertyChangeEvent evt) { 533 getConditionalNG().execute(); 534 } 535 536 537 public enum UserSpecifiedSource { 538 Variable(Bundle.getMessage("ForEachWithDelay_UserSpecifiedSource_Variable")), 539 Memory(Bundle.getMessage("ForEachWithDelay_UserSpecifiedSource_Memory")), 540 Formula(Bundle.getMessage("ForEachWithDelay_UserSpecifiedSource_Formula")); 541 542 private final String _text; 543 544 private UserSpecifiedSource(String text) { 545 this._text = text; 546 } 547 548 @Override 549 public String toString() { 550 return _text; 551 } 552 553 } 554 555 556 private class InternalFemaleSocket extends jmri.jmrit.logixng.implementation.DefaultFemaleDigitalActionSocket { 557 558 private ConditionalNG conditionalNG; 559 private SymbolTable newSymbolTable; 560 561 public InternalFemaleSocket() { 562 super(null, new FemaleSocketListener(){ 563 @Override 564 public void connected(FemaleSocket socket) { 565 // Do nothing 566 } 567 568 @Override 569 public void disconnected(FemaleSocket socket) { 570 // Do nothing 571 } 572 }, "A"); 573 } 574 575 @Override 576 public void execute() throws JmriException { 577 if (conditionalNG == null) { throw new NullPointerException("conditionalNG is null"); } 578 if (_socket != null) { 579 SymbolTable oldSymbolTable = conditionalNG.getSymbolTable(); 580 conditionalNG.setSymbolTable(newSymbolTable); 581 _socket.execute(); 582 conditionalNG.setSymbolTable(oldSymbolTable); 583 } 584 } 585 586 } 587 588 589 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ForEachWithDelay.class); 590 591}