001package jmri.jmrit.logixng.expressions; 002 003import java.beans.*; 004import java.util.*; 005 006import javax.annotation.Nonnull; 007 008import jmri.*; 009import jmri.jmrit.dispatcher.*; 010import jmri.jmrit.logixng.*; 011import jmri.jmrit.logixng.util.*; 012import jmri.jmrit.logixng.util.parser.*; 013import jmri.jmrit.logixng.util.parser.ExpressionNode; 014import jmri.jmrit.logixng.util.parser.RecursiveDescentParser; 015import jmri.util.TypeConversionUtil; 016 017/** 018 * This expression checks the status or mode of an active train. 019 * <p> 020 * A Dispatcher ActiveTrain is a transient object. The DispatcherActiveTrainManager is a special 021 * LogiNG class which manages the relationships with expressions using the Dispatcher TrainInfo file 022 * as the key. This makes it possible to add and remove ActiveTrain PropertyChange listeners. 023 * 024 * @author Dave Sand Copyright 2021 025 */ 026public class ExpressionDispatcher extends AbstractDigitalExpression 027 implements PropertyChangeListener { 028 029 private NamedBeanAddressing _addressing = NamedBeanAddressing.Direct; 030 private String _reference = ""; 031 private String _localVariable = ""; 032 private String _formula = ""; 033 private ExpressionNode _expressionNode; 034 035 private final LogixNG_SelectEnum<DispatcherState> _selectEnum = 036 new LogixNG_SelectEnum<>(this, DispatcherState.values(), DispatcherState.Mode, this); 037 038 private String _trainInfoFileName = ""; 039 private Is_IsNot_Enum _is_IsNot = Is_IsNot_Enum.Is; 040 041 private final DispatcherActiveTrainManager _atManager; 042 private boolean _activeTrainListeners = false; 043 044 /** 045 * An active train is transient. It can be terminated manually which means the reference 046 * will no longer be valid. 047 */ 048 private ActiveTrain _activeTrain = null; 049 050 051 public ExpressionDispatcher(String sys, String user) 052 throws BadUserNameException, BadSystemNameException { 053 super(sys, user); 054 _atManager = InstanceManager.getDefault(DispatcherActiveTrainManager.class); 055 } 056 057 @Override 058 public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) throws ParserException { 059 DigitalExpressionManager manager = InstanceManager.getDefault(DigitalExpressionManager.class); 060 String sysName = systemNames.get(getSystemName()); 061 String userName = userNames.get(getSystemName()); 062 if (sysName == null) sysName = manager.getAutoSystemName(); 063 ExpressionDispatcher copy = new ExpressionDispatcher(sysName, userName); 064 copy.setComment(getComment()); 065 066 copy.setTrainInfoFileName(_trainInfoFileName); 067 068 copy.setAddressing(_addressing); 069 copy.setFormula(_formula); 070 copy.setLocalVariable(_localVariable); 071 copy.setReference(_reference); 072 073 copy.set_Is_IsNot(_is_IsNot); 074 075 _selectEnum.copy(copy._selectEnum); 076 077 return manager.registerExpression(copy); 078 } 079 080 public LogixNG_SelectEnum<DispatcherState> getSelectEnum() { 081 return _selectEnum; 082 } 083 084 public void setTrainInfoFileName(@Nonnull String fileName) { 085 _trainInfoFileName = fileName; 086 } 087 088 public String getTrainInfoFileName() { 089 return _trainInfoFileName; 090 } 091 092 093 public void setAddressing(NamedBeanAddressing addressing) throws ParserException { 094 _addressing = addressing; 095 parseFormula(); 096 } 097 098 public NamedBeanAddressing getAddressing() { 099 return _addressing; 100 } 101 102 public void setReference(@Nonnull String reference) { 103 if ((! reference.isEmpty()) && (! ReferenceUtil.isReference(reference))) { 104 throw new IllegalArgumentException("The reference \"" + reference + "\" is not a valid reference"); 105 } 106 _reference = reference; 107 } 108 109 public String getReference() { 110 return _reference; 111 } 112 113 public void setLocalVariable(@Nonnull String localVariable) { 114 _localVariable = localVariable; 115 } 116 117 public String getLocalVariable() { 118 return _localVariable; 119 } 120 121 public void setFormula(@Nonnull String formula) throws ParserException { 122 _formula = formula; 123 parseFormula(); 124 } 125 126 public String getFormula() { 127 return _formula; 128 } 129 130 private void parseFormula() throws ParserException { 131 if (_addressing == NamedBeanAddressing.Formula) { 132 Map<String, Variable> variables = new HashMap<>(); 133 134 RecursiveDescentParser parser = new RecursiveDescentParser(variables); 135 _expressionNode = parser.parseExpression(_formula); 136 } else { 137 _expressionNode = null; 138 } 139 } 140 141 142 public void set_Is_IsNot(Is_IsNot_Enum is_IsNot) { 143 _is_IsNot = is_IsNot; 144 } 145 146 public Is_IsNot_Enum get_Is_IsNot() { 147 return _is_IsNot; 148 } 149 150 151 /** {@inheritDoc} */ 152 @Override 153 public Category getCategory() { 154 return Category.ITEM; 155 } 156 157 private String getSelectedFileName() throws JmriException { 158 switch (_addressing) { 159 case Direct: 160 return getTrainInfoFileName(); 161 162 case Reference: 163 return ReferenceUtil.getReference( 164 getConditionalNG().getSymbolTable(), _reference); 165 166 case LocalVariable: 167 SymbolTable symbolTable = getConditionalNG().getSymbolTable(); 168 return TypeConversionUtil 169 .convertToString(symbolTable.getValue(_localVariable), false); 170 171 case Formula: 172 return _expressionNode != null ? 173 TypeConversionUtil.convertToString(_expressionNode.calculate( 174 getConditionalNG().getSymbolTable()), false) 175 : ""; 176 177 default: 178 throw new IllegalArgumentException("invalid _addressing state: " + _addressing.name()); 179 } 180 } 181 182 /** {@inheritDoc} */ 183 @Override 184 public boolean evaluate() throws JmriException { 185 ConditionalNG conditionalNG = getConditionalNG(); 186 187 String trainInfoFileName = getSelectedFileName(); 188 189 if (trainInfoFileName.isEmpty()) { 190 return false; 191 } 192 193 DispatcherState checkDispatcherState = _selectEnum.evaluateEnum(conditionalNG); 194 195 boolean result = false; 196 197 switch (checkDispatcherState) { 198 case Automatic: 199 if (_activeTrain != null) result = (_activeTrain.getMode() == ActiveTrain.AUTOMATIC); 200 break; 201 case Dispatched: 202 if (_activeTrain != null) result = (_activeTrain.getMode() == ActiveTrain.DISPATCHED); 203 break; 204 case Manual: 205 if (_activeTrain != null) result = (_activeTrain.getMode() == ActiveTrain.MANUAL); 206 break; 207 case Running: 208 if (_activeTrain != null) result = (_activeTrain.getStatus() == ActiveTrain.RUNNING); 209 break; 210 case Paused: 211 if (_activeTrain != null) result = (_activeTrain.getStatus() == ActiveTrain.PAUSED); 212 break; 213 case Waiting: 214 if (_activeTrain != null) result = (_activeTrain.getStatus() == ActiveTrain.WAITING); 215 break; 216 case Working: 217 if (_activeTrain != null) result = (_activeTrain.getStatus() == ActiveTrain.WORKING); 218 break; 219 case Ready: 220 if (_activeTrain != null) result = (_activeTrain.getStatus() == ActiveTrain.READY); 221 break; 222 case Stopped: 223 if (_activeTrain != null) result = (_activeTrain.getStatus() == ActiveTrain.STOPPED); 224 break; 225 case Done: 226 if (_activeTrain != null) result = (_activeTrain.getStatus() == ActiveTrain.DONE); 227 break; 228 229 default: 230 throw new UnsupportedOperationException("checkDispatcherState has unknown value: " + checkDispatcherState.name()); 231 } 232 233 if (_is_IsNot == Is_IsNot_Enum.Is) { 234 return result; 235 } else { 236 return !result; 237 } 238 } 239 240 @Override 241 public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException { 242 throw new UnsupportedOperationException("Not supported."); 243 } 244 245 @Override 246 public int getChildCount() { 247 return 0; 248 } 249 250 251 @Override 252 public String getShortDescription(Locale locale) { 253 return Bundle.getMessage(locale, "Dispatcher_Short"); 254 } 255 256 @Override 257 public String getLongDescription(Locale locale) { 258 String fileName; 259 260 switch (_addressing) { 261 case Direct: 262 fileName = Bundle.getMessage(locale, "AddressByDirect", _trainInfoFileName); 263 break; 264 265 case Reference: 266 fileName = Bundle.getMessage(locale, "AddressByReference", _reference); 267 break; 268 269 case LocalVariable: 270 fileName = Bundle.getMessage(locale, "AddressByLocalVariable", _localVariable); 271 break; 272 273 case Formula: 274 fileName = Bundle.getMessage(locale, "AddressByFormula", _formula); 275 break; 276 277 default: 278 throw new IllegalArgumentException("invalid _addressing state: " + _addressing.name()); 279 } 280 281 String state = _selectEnum.getDescription(locale); 282 283 return Bundle.getMessage(locale, "Dispatcher_Long", fileName, _is_IsNot.toString(), state); 284 } 285 286 /** {@inheritDoc} */ 287 @Override 288 public void setup() { 289 // Do nothing 290 } 291 292 /** {@inheritDoc} */ 293 @Override 294 public void registerListenersForThisClass() { 295 if (! _listenersAreRegistered) { 296 _atManager.addPropertyChangeListener(this); 297 _selectEnum.registerListeners(); 298 _listenersAreRegistered = true; 299 } 300 } 301 302 /** {@inheritDoc} */ 303 @Override 304 public void unregisterListenersForThisClass() { 305 if (_listenersAreRegistered) { 306 _atManager.removePropertyChangeListener(this); 307 _selectEnum.unregisterListeners(); 308 _listenersAreRegistered = false; 309 } 310 } 311 312 /** {@inheritDoc} 313 * ActiveTrain is created by DispatcherActiveTrainManager 314 * status and mode are created by Dispatcher ActiveTrain 315 */ 316 @Override 317 public void propertyChange(PropertyChangeEvent evt) { 318 switch (evt.getPropertyName()) { 319 case "ActiveTrain": 320 manageActiveTrain(evt); 321 break; 322 323 case "mode": 324 if ((int) evt.getNewValue() == ActiveTrain.TERMINATED) { 325 // The Dispatcher active train was terminated by an external process. 326 // Force the manager to update the LogixNG active train map. 327 _atManager.getActiveTrain(_trainInfoFileName); 328 return; 329 } 330 331 getConditionalNG().execute(); 332 break; 333 334 case "status": 335 getConditionalNG().execute(); 336 break; 337 338 default: 339 log.debug("Other property changes are ignored: name = {}", evt.getPropertyName()); 340 } 341 } 342 343 /** 344 * The DispatcherActiveTrainManager keeps track of the ActiveTrains created using LogixNG. 345 * When an ActiveTrain is added, ActiveTrain property change listeners are added so that the 346 * expression can be notified of status and mode changes. _activeTrain is updated with the 347 * with current active train. 348 * <p> 349 * When an ActiveTrain is removed, the listeners are removed and _activeTrain is set to null. 350 * @param event The DispatcherActiveTrainManager property change event. 351 */ 352 private void manageActiveTrain(PropertyChangeEvent event) { 353 String selectedFileName; 354 try { 355 selectedFileName = getSelectedFileName(); 356 } catch (JmriException ex) { 357 log.warn("Unexpected exception, using Direct file name"); 358 selectedFileName = _trainInfoFileName; 359 } 360 361 String eventFileName = (String) event.getOldValue(); 362 if (eventFileName.isEmpty()) { 363 eventFileName = (String) event.getNewValue(); 364 } 365 366 if (! selectedFileName.equals(eventFileName)) return; 367 368 ActiveTrain checkTrain = _atManager.getActiveTrain(selectedFileName); 369 370 if (checkTrain == null) { 371 if (_activeTrain != null) { 372 if (_activeTrainListeners) { 373 _activeTrain.removePropertyChangeListener(this); 374 _activeTrainListeners = false; 375 } 376 _activeTrain = null; 377 } 378 return; 379 } 380 381 if (checkTrain == _activeTrain) return; 382 383 if (_activeTrain == null) { 384 _activeTrain = checkTrain; 385 _activeTrainListeners = false; 386 } 387 388 if (! _activeTrainListeners) { 389 _activeTrain.addPropertyChangeListener(this); 390 _activeTrainListeners = true; 391 } 392 } 393 394 395 /** {@inheritDoc} */ 396 @Override 397 public void disposeMe() { 398 } 399 400 401 public enum DispatcherState { 402 Mode(Bundle.getMessage("DispatcherSeparatorMode"), "Separator"), 403 Automatic(Bundle.getMessage("DispatcherMode_Automatic"), "Mode"), 404 Dispatched(Bundle.getMessage("DispatcherMode_Dispatched"), "Mode"), 405 Manual(Bundle.getMessage("DispatcherMode_Manual"), "Mode"), 406 Status(Bundle.getMessage("DispatcherSeparatorStatus"), "Separator"), 407 Running(Bundle.getMessage("DispatcherStatus_Running"), "Status"), 408 Paused(Bundle.getMessage("DispatcherStatus_Paused"), "Status"), 409 Waiting(Bundle.getMessage("DispatcherStatus_Waiting"), "Status"), 410 Working(Bundle.getMessage("DispatcherStatus_Working"), "Status"), 411 Ready(Bundle.getMessage("DispatcherStatus_Ready"), "Status"), 412 Stopped(Bundle.getMessage("DispatcherStatus_Stopped"), "Status"), 413 Done(Bundle.getMessage("DispatcherStatus_Done"), "Status"); 414 415 private final String _text; 416 private final String _type; 417 418 private DispatcherState(String text, String type) { 419 this._text = text; 420 this._type = type; 421 } 422 423 public String getType() { 424 return _type; 425 } 426 427 @Override 428 public String toString() { 429 return _text; 430 } 431 432 } 433 434 435 /** {@inheritDoc} */ 436 @Override 437 public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl) { 438 log.debug("getUsageReport :: ExpressionDispatcher: bean = {}, report = {}", cdl, report); 439 } 440 441 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ExpressionDispatcher.class); 442 443}