001package jmri.jmrit.logixng.actions; 002 003import java.util.*; 004 005import jmri.jmrit.logixng.util.TimerUnit; 006 007import javax.annotation.Nonnull; 008 009import jmri.InstanceManager; 010import jmri.JmriException; 011import jmri.jmrit.logixng.*; 012import jmri.jmrit.logixng.implementation.DefaultSymbolTable; 013import jmri.jmrit.logixng.util.*; 014import jmri.jmrit.logixng.util.parser.*; 015import jmri.util.TimerUtil; 016import jmri.util.TypeConversionUtil; 017 018/** 019 * Executes a digital action delayed. 020 * 021 * @author Daniel Bergqvist Copyright 2021 022 */ 023public class ExecuteDelayed 024 extends AbstractDigitalAction 025 implements FemaleSocketListener { 026 027 private String _socketSystemName; 028 private final FemaleDigitalActionSocket _socket; 029 private ProtectedTimerTask _defaultTimerTask; 030 private NamedBeanAddressing _stateAddressing = NamedBeanAddressing.Direct; 031 private int _delay; 032 private String _stateReference = ""; 033 private String _stateLocalVariable = ""; 034 private String _stateFormula = ""; 035 private ExpressionNode _stateExpressionNode; 036 private TimerUnit _unit = TimerUnit.MilliSeconds; 037 private boolean _resetIfAlreadyStarted; 038 private boolean _useIndividualTimers; 039 040 private final InternalFemaleSocket _defaultInternalSocket = new InternalFemaleSocket(); 041 042 043 public ExecuteDelayed(String sys, String user) { 044 super(sys, user); 045 _socket = InstanceManager.getDefault(DigitalActionManager.class) 046 .createFemaleSocket(this, this, "A"); 047 } 048 049 @Override 050 public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws JmriException { 051 DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class); 052 String sysName = systemNames.get(getSystemName()); 053 String userName = userNames.get(getSystemName()); 054 if (sysName == null) sysName = manager.getAutoSystemName(); 055 ExecuteDelayed copy = new ExecuteDelayed(sysName, userName); 056 copy.setComment(getComment()); 057 copy.setDelayAddressing(_stateAddressing); 058 copy.setDelay(_delay); 059 copy.setDelayFormula(_stateFormula); 060 copy.setDelayLocalVariable(_stateLocalVariable); 061 copy.setDelayReference(_stateReference); 062 copy.setUnit(_unit); 063 copy.setResetIfAlreadyStarted(_resetIfAlreadyStarted); 064 copy.setUseIndividualTimers(_useIndividualTimers); 065 return manager.registerAction(copy).deepCopyChildren(this, systemNames, userNames); 066 } 067 068 /** {@inheritDoc} */ 069 @Override 070 public Category getCategory() { 071 return Category.COMMON; 072 } 073/* 074 private String getVariables(SymbolTable symbolTable) { 075 java.io.StringWriter stringWriter = new java.io.StringWriter(); 076 java.io.PrintWriter writer = new java.io.PrintWriter(stringWriter); 077 symbolTable.printSymbolTable(writer); 078 return stringWriter.toString(); 079 } 080*/ 081 /** 082 * Get a new timer task. 083 * @param conditionalNG the ConditionalNG 084 * @param symbolTable the symbol table 085 * @param timerDelay the time the timer should wait 086 * @param timerStart the time when the timer was started 087 */ 088 private ProtectedTimerTask getNewTimerTask(ConditionalNG conditionalNG, SymbolTable symbolTable, long timerDelay, long timerStart) throws JmriException { 089 090 DefaultSymbolTable newSymbolTable = new DefaultSymbolTable(symbolTable); 091 092 return new ProtectedTimerTask() { 093 @Override 094 public void execute() { 095 try { 096 synchronized(ExecuteDelayed.this) { 097 if (!_useIndividualTimers) _defaultTimerTask = null; 098 long currentTime = System.currentTimeMillis(); 099 long currentTimerTime = currentTime - timerStart; 100 if (currentTimerTime < timerDelay) { 101 scheduleTimer(conditionalNG, symbolTable, timerDelay - currentTimerTime, currentTime); 102 } else { 103 InternalFemaleSocket internalSocket; 104 if (_useIndividualTimers) { 105 internalSocket = new InternalFemaleSocket(); 106 } else { 107 internalSocket = _defaultInternalSocket; 108 } 109 internalSocket.conditionalNG = conditionalNG; 110 internalSocket.newSymbolTable = newSymbolTable; 111 conditionalNG.execute(internalSocket); 112 } 113 } 114 } catch (RuntimeException | JmriException e) { 115 log.error("Exception thrown", e); 116 } 117 } 118 }; 119 } 120 121 private void scheduleTimer(ConditionalNG conditionalNG, SymbolTable symbolTable, long timerDelay, long timerStart) throws JmriException { 122 synchronized(ExecuteDelayed.this) { 123 if (!_useIndividualTimers && (_defaultTimerTask != null)) { 124 _defaultTimerTask.stopTimer(); 125 } 126 ProtectedTimerTask timerTask = 127 getNewTimerTask(conditionalNG, symbolTable, timerDelay, timerStart); 128 if (!_useIndividualTimers) { 129 _defaultTimerTask = timerTask; 130 } 131 TimerUtil.schedule(timerTask, timerDelay); 132 } 133 } 134 135 private long getNewDelay() throws JmriException { 136 137 switch (_stateAddressing) { 138 case Direct: 139 return _delay; 140 141 case Reference: 142 return TypeConversionUtil.convertToLong(ReferenceUtil.getReference( 143 getConditionalNG().getSymbolTable(), _stateReference)); 144 145 case LocalVariable: 146 SymbolTable symbolTable = getConditionalNG().getSymbolTable(); 147 return TypeConversionUtil 148 .convertToLong(symbolTable.getValue(_stateLocalVariable)); 149 150 case Formula: 151 return _stateExpressionNode != null 152 ? TypeConversionUtil.convertToLong( 153 _stateExpressionNode.calculate( 154 getConditionalNG().getSymbolTable())) 155 : 0; 156 157 default: 158 throw new IllegalArgumentException("invalid _addressing state: " + _stateAddressing.name()); 159 } 160 } 161 162 /** {@inheritDoc} */ 163 @Override 164 public void execute() throws JmriException { 165 synchronized(this) { 166 if (!_useIndividualTimers && (_defaultTimerTask != null)) { 167 if (_resetIfAlreadyStarted) _defaultTimerTask.stopTimer(); 168 else return; 169 } 170 long timerDelay = getNewDelay() * _unit.getMultiply(); 171 long timerStart = System.currentTimeMillis(); 172 ConditionalNG conditonalNG = getConditionalNG(); 173 scheduleTimer(conditonalNG, conditonalNG.getSymbolTable(), timerDelay, timerStart); 174 } 175 } 176 177 public void setDelayAddressing(NamedBeanAddressing addressing) throws ParserException { 178 _stateAddressing = addressing; 179 parseDelayFormula(); 180 } 181 182 public NamedBeanAddressing getDelayAddressing() { 183 return _stateAddressing; 184 } 185 186 /** 187 * Get the delay. 188 * @return the delay 189 */ 190 public int getDelay() { 191 return _delay; 192 } 193 194 /** 195 * Set the delay. 196 * @param delay the delay 197 */ 198 public void setDelay(int delay) { 199 _delay = delay; 200 } 201 202 public void setDelayReference(@Nonnull String reference) { 203 if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) { 204 throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference"); 205 } 206 _stateReference = reference; 207 } 208 209 public String getDelayReference() { 210 return _stateReference; 211 } 212 213 public void setDelayLocalVariable(@Nonnull String localVariable) { 214 _stateLocalVariable = localVariable; 215 } 216 217 public String getDelayLocalVariable() { 218 return _stateLocalVariable; 219 } 220 221 public void setDelayFormula(@Nonnull String formula) throws ParserException { 222 _stateFormula = formula; 223 parseDelayFormula(); 224 } 225 226 public String getDelayFormula() { 227 return _stateFormula; 228 } 229 230 private void parseDelayFormula() throws ParserException { 231 if (_stateAddressing == NamedBeanAddressing.Formula) { 232 Map<String, Variable> variables = new HashMap<>(); 233 234 RecursiveDescentParser parser = new RecursiveDescentParser(variables); 235 _stateExpressionNode = parser.parseExpression(_stateFormula); 236 } else { 237 _stateExpressionNode = null; 238 } 239 } 240 241 /** 242 * Get the unit 243 * @return the unit 244 */ 245 public TimerUnit getUnit() { 246 return _unit; 247 } 248 249 /** 250 * Set the unit 251 * @param unit the unit 252 */ 253 public void setUnit(TimerUnit unit) { 254 _unit = unit; 255 } 256 257 /** 258 * Get reset if timer is already started. 259 * @return true if the timer should be reset if this action is executed 260 * while timer is ticking, false othervise 261 */ 262 public boolean getResetIfAlreadyStarted() { 263 return _resetIfAlreadyStarted; 264 } 265 266 /** 267 * Set reset if timer is already started. 268 * @param resetIfAlreadyStarted true if the timer should be reset if this 269 * action is executed while timer is ticking, 270 * false othervise 271 */ 272 public void setResetIfAlreadyStarted(boolean resetIfAlreadyStarted) { 273 _resetIfAlreadyStarted = resetIfAlreadyStarted; 274 } 275 276 /** 277 * Get use individual timers. 278 * @return true if the timer should use individual timers, false othervise 279 */ 280 public boolean getUseIndividualTimers() { 281 return _useIndividualTimers; 282 } 283 284 /** 285 * Set reset if timer is already started. 286 * @param useIndividualTimers true if the timer should use individual timers, 287 * false othervise 288 */ 289 public void setUseIndividualTimers(boolean useIndividualTimers) { 290 _useIndividualTimers = useIndividualTimers; 291 } 292 293 @Override 294 public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException { 295 switch (index) { 296 case 0: 297 return _socket; 298 299 default: 300 throw new IllegalArgumentException( 301 String.format("index has invalid value: %d", index)); 302 } 303 } 304 305 @Override 306 public int getChildCount() { 307 return 1; 308 } 309 310 @Override 311 public void connected(FemaleSocket socket) { 312 if (socket == _socket) { 313 _socketSystemName = socket.getConnectedSocket().getSystemName(); 314 } else { 315 throw new IllegalArgumentException("unkown socket"); 316 } 317 } 318 319 @Override 320 public void disconnected(FemaleSocket socket) { 321 if (socket == _socket) { 322 _socketSystemName = null; 323 } else { 324 throw new IllegalArgumentException("unkown socket"); 325 } 326 } 327 328 @Override 329 public String getShortDescription(Locale locale) { 330 return Bundle.getMessage(locale, "ExecuteDelayed_Short"); 331 } 332 333 @Override 334 public String getLongDescription(Locale locale) { 335 String delay; 336 337 switch (_stateAddressing) { 338 case Direct: 339 delay = Bundle.getMessage(locale, "ExecuteDelayed_DelayByDirect", _unit.getTimeWithUnit(_delay)); 340 break; 341 342 case Reference: 343 delay = Bundle.getMessage(locale, "ExecuteDelayed_DelayByReference", _stateReference, _unit.toString()); 344 break; 345 346 case LocalVariable: 347 delay = Bundle.getMessage(locale, "ExecuteDelayed_DelayByLocalVariable", _stateLocalVariable, _unit.toString()); 348 break; 349 350 case Formula: 351 delay = Bundle.getMessage(locale, "ExecuteDelayed_DelayByFormula", _stateFormula, _unit.toString()); 352 break; 353 354 default: 355 throw new IllegalArgumentException("invalid _stateAddressing state: " + _stateAddressing.name()); 356 } 357 358 return Bundle.getMessage(locale, 359 "ExecuteDelayed_Long", 360 _socket.getName(), 361 delay, 362 _resetIfAlreadyStarted 363 ? Bundle.getMessage("ExecuteDelayed_Options", Bundle.getMessage("ExecuteDelayed_ResetRepeat")) 364 : Bundle.getMessage("ExecuteDelayed_Options", Bundle.getMessage("ExecuteDelayed_IgnoreRepeat")), 365 _useIndividualTimers 366 ? Bundle.getMessage("ExecuteDelayed_Options", Bundle.getMessage("ExecuteDelayed_UseIndividualTimers")) 367 : ""); 368 } 369 370 public FemaleDigitalActionSocket getSocket() { 371 return _socket; 372 } 373 374 public String getSocketSystemName() { 375 return _socketSystemName; 376 } 377 378 public void setSocketSystemName(String systemName) { 379 _socketSystemName = systemName; 380 } 381 382 /** {@inheritDoc} */ 383 @Override 384 public void setup() { 385 try { 386 if (!_socket.isConnected() 387 || !_socket.getConnectedSocket().getSystemName() 388 .equals(_socketSystemName)) { 389 390 String socketSystemName = _socketSystemName; 391 392 _socket.disconnect(); 393 394 if (socketSystemName != null) { 395 MaleSocket maleSocket = 396 InstanceManager.getDefault(DigitalActionManager.class) 397 .getBySystemName(socketSystemName); 398 if (maleSocket != null) { 399 _socket.connect(maleSocket); 400 maleSocket.setup(); 401 } else { 402 log.error("cannot load analog action {}", socketSystemName); 403 } 404 } 405 } else { 406 _socket.getConnectedSocket().setup(); 407 } 408 } catch (SocketAlreadyConnectedException ex) { 409 // This shouldn't happen and is a runtime error if it does. 410 throw new RuntimeException("socket is already connected"); 411 } 412 } 413 414 /** {@inheritDoc} */ 415 @Override 416 public void registerListenersForThisClass() { 417 } 418 419 /** {@inheritDoc} */ 420 @Override 421 public void unregisterListenersForThisClass() { 422 synchronized(ExecuteDelayed.this) { 423 if (!_useIndividualTimers && (_defaultTimerTask != null)) { 424 _defaultTimerTask.stopTimer(); 425 _defaultTimerTask = null; 426 } 427 } 428 } 429 430 /** {@inheritDoc} */ 431 @Override 432 public void disposeMe() { 433 } 434 435 436 437 private class InternalFemaleSocket extends jmri.jmrit.logixng.implementation.DefaultFemaleDigitalActionSocket { 438 439 private ConditionalNG conditionalNG; 440 private SymbolTable newSymbolTable; 441 442 public InternalFemaleSocket() { 443 super(null, new FemaleSocketListener(){ 444 @Override 445 public void connected(FemaleSocket socket) { 446 // Do nothing 447 } 448 449 @Override 450 public void disconnected(FemaleSocket socket) { 451 // Do nothing 452 } 453 }, "A"); 454 } 455 456 @Override 457 public void execute() throws JmriException { 458 if (conditionalNG == null) { throw new NullPointerException("conditionalNG is null"); } 459 if (_socket != null) { 460 SymbolTable oldSymbolTable = conditionalNG.getSymbolTable(); 461 conditionalNG.setSymbolTable(newSymbolTable); 462 _socket.execute(); 463 conditionalNG.setSymbolTable(oldSymbolTable); 464 } 465 } 466 467 } 468 469 470 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ExecuteDelayed.class); 471 472}