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