001package jmri.jmrit.logixng.actions; 002 003import jmri.jmrit.logixng.NamedBeanType; 004 005import java.beans.*; 006import java.util.*; 007import java.util.stream.Collectors; 008 009import jmri.*; 010import jmri.jmrit.logixng.*; 011import jmri.jmrit.logixng.util.DuplicateKeyMap; 012 013import net.jcip.annotations.GuardedBy; 014 015/** 016 * This action listens on some beans and runs the ConditionalNG on property change. 017 * 018 * @author Daniel Bergqvist Copyright 2019 019 */ 020public class ActionListenOnBeans extends AbstractDigitalAction 021 implements PropertyChangeListener, VetoableChangeListener { 022 023 private final Map<String, NamedBeanReference> _namedBeanReferences = new DuplicateKeyMap<>(); 024 private String _localVariableNamedBean; 025 private String _localVariableEvent; 026 private String _localVariableNewValue; 027 028 @GuardedBy("this") 029 private final Deque<PropertyChangeEvent> _eventQueue = new ArrayDeque<>(); 030 031 032 public ActionListenOnBeans(String sys, String user) 033 throws BadUserNameException, BadSystemNameException { 034 super(sys, user); 035 } 036 037 @Override 038 public Base getDeepCopy(Map<String, String> systemNames, Map<String, String> userNames) { 039 DigitalActionManager manager = InstanceManager.getDefault(DigitalActionManager.class); 040 String sysName = systemNames.get(getSystemName()); 041 String userName = userNames.get(getSystemName()); 042 if (sysName == null) sysName = manager.getAutoSystemName(); 043 ActionListenOnBeans copy = new ActionListenOnBeans(sysName, userName); 044 copy.setComment(getComment()); 045 copy.setLocalVariableNamedBean(_localVariableNamedBean); 046 copy.setLocalVariableEvent(_localVariableEvent); 047 copy.setLocalVariableNewValue(_localVariableNewValue); 048 for (NamedBeanReference reference : _namedBeanReferences.values()) { 049 copy.addReference(reference); 050 } 051 return manager.registerAction(copy); 052 } 053 054 /** 055 * Register a bean 056 * The bean must be on the form "beantype:name" where beantype is for 057 * example turnout, sensor or memory, and name is the name of the bean. 058 * The type can be upper case or lower case, it doesn't matter. 059 * @param beanAndType the bean and type 060 */ 061 public void addReference(String beanAndType) { 062 assertListenersAreNotRegistered(log, "addReference"); 063 String[] parts = beanAndType.split(":"); 064 if ((parts.length < 2) || (parts.length > 3)) { 065 throw new IllegalArgumentException( 066 "Parameter 'beanAndType' must be on the format type:name" 067 + " where type is turnout, sensor, memory, ..., or on the" 068 + " format type:name:all where all is yes or no"); 069 } 070 071 boolean listenToAll = false; 072 if (parts.length == 3) listenToAll = "yes".equals(parts[2]); // NOI18N 073 074 try { 075 NamedBeanType type = NamedBeanType.valueOf(parts[0]); 076 NamedBeanReference reference = new NamedBeanReference(parts[1], type, listenToAll); 077 addReference(reference); 078 } catch (IllegalArgumentException e) { 079 String types = Arrays.asList(NamedBeanType.values()) 080 .stream() 081 .map(Enum::toString) 082 .collect(Collectors.joining(", ")); 083 throw new IllegalArgumentException( 084 "Parameter 'beanAndType' has wrong type. Valid types are: " + types); 085 } 086 } 087 088 public void addReference(NamedBeanReference reference) { 089 assertListenersAreNotRegistered(log, "addReference"); 090 _namedBeanReferences.put(reference._name, reference); 091 reference._type.getManager().addVetoableChangeListener(this); 092 } 093 094 public void removeReference(NamedBeanReference reference) { 095 assertListenersAreNotRegistered(log, "removeReference"); 096 _namedBeanReferences.remove(reference._name, reference); 097 reference._type.getManager().removeVetoableChangeListener(this); 098 } 099 100 public Collection<NamedBeanReference> getReferences() { 101 return _namedBeanReferences.values(); 102 } 103 104 public void clearReferences() { 105 _namedBeanReferences.clear(); 106 } 107 108 public void setLocalVariableNamedBean(String localVariableNamedBean) { 109 if ((localVariableNamedBean != null) && (!localVariableNamedBean.isEmpty())) { 110 this._localVariableNamedBean = localVariableNamedBean; 111 } else { 112 this._localVariableNamedBean = null; 113 } 114 } 115 116 public String getLocalVariableNamedBean() { 117 return _localVariableNamedBean; 118 } 119 120 public void setLocalVariableEvent(String localVariableEvent) { 121 if ((localVariableEvent != null) && (!localVariableEvent.isEmpty())) { 122 this._localVariableEvent = localVariableEvent; 123 } else { 124 this._localVariableEvent = null; 125 } 126 } 127 128 public String getLocalVariableEvent() { 129 return _localVariableEvent; 130 } 131 132 public void setLocalVariableNewValue(String localVariableNewValue) { 133 if ((localVariableNewValue != null) && (!localVariableNewValue.isEmpty())) { 134 this._localVariableNewValue = localVariableNewValue; 135 } else { 136 this._localVariableNewValue = null; 137 } 138 } 139 140 public String getLocalVariableNewValue() { 141 return _localVariableNewValue; 142 } 143 144 @Override 145 public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException { 146 var tempNamedBeanReferences = new ArrayList<NamedBeanReference>(_namedBeanReferences.values()); 147 for (NamedBeanReference reference : tempNamedBeanReferences) { 148 if (reference._type.getClazz().isAssignableFrom(evt.getOldValue().getClass())) { 149 if ((reference._handle != null) && evt.getOldValue().equals(reference._handle.getBean())) { 150 if ("CanDelete".equals(evt.getPropertyName())) { // No I18N 151 PropertyChangeEvent e = new PropertyChangeEvent(this, "DoNotDelete", null, null); 152 throw new PropertyVetoException(getDisplayName(), e); 153 } else if ("DoDelete".equals(evt.getPropertyName())) { // No I18N 154 _namedBeanReferences.remove(reference._name, reference); 155 } 156 } 157 } 158 } 159 } 160 161 /** {@inheritDoc} */ 162 @Override 163 public Category getCategory() { 164 return Category.OTHER; 165 } 166 167 /** {@inheritDoc} */ 168 @Override 169 public void execute() { 170 // The main purpose of this action is only to listen on property 171 // changes of the registered beans and execute the ConditionalNG 172 // when it happens. 173 174 synchronized(this) { 175 String namedBean; 176 String event; 177 String newValue; 178 179 PropertyChangeEvent evt = _eventQueue.poll(); 180 if (evt != null) { 181 namedBean = ((NamedBean)evt.getSource()).getDisplayName(); 182 event = evt.getPropertyName(); 183 newValue = evt.getNewValue() != null ? evt.getNewValue().toString() : null; 184 } else { 185 namedBean = null; 186 event = null; 187 newValue = null; 188 } 189 190 SymbolTable symbolTable = getConditionalNG().getSymbolTable(); 191 192 if (_localVariableNamedBean != null) { 193 symbolTable.setValue(_localVariableNamedBean, namedBean); 194 } 195 if (_localVariableEvent != null) { 196 symbolTable.setValue(_localVariableEvent, event); 197 } 198 if (_localVariableNewValue != null) { 199 symbolTable.setValue(_localVariableNewValue, newValue); 200 } 201 202 if (!_eventQueue.isEmpty()) { 203 getConditionalNG().execute(); 204 } 205 } 206 } 207 208 @Override 209 public FemaleSocket getChild(int index) throws IllegalArgumentException, UnsupportedOperationException { 210 throw new UnsupportedOperationException("Not supported."); 211 } 212 213 @Override 214 public int getChildCount() { 215 return 0; 216 } 217 218 @Override 219 public String getShortDescription(Locale locale) { 220 return Bundle.getMessage(locale, "ActionListenOnBeans_Short"); 221 } 222 223 @Override 224 public String getLongDescription(Locale locale) { 225 return Bundle.getMessage(locale, "ActionListenOnBeans_Long"); 226 } 227 228 /** {@inheritDoc} */ 229 @Override 230 public void setup() { 231 for (NamedBeanReference ref : getReferences()) { 232 if (ref.getType() == NamedBeanType.EntryExit) { 233 // The EntryExit objects were not available during file loading. 234 if (ref.getName() != null && !ref.getName().isEmpty()) { 235 var nxBean = ref.getType().getManager().getNamedBean(ref.getName()); 236 if (nxBean != null) { 237 // Change the system name to user name which will also create the named bean handle 238 ref.setName(nxBean.getUserName()); 239 } else { 240 log.error("NX bean null for {} during setup", ref.getName()); 241 } 242 } else { 243 log.error("NX name is null or empty"); 244 } 245 } 246 } 247 } 248 249 /** {@inheritDoc} */ 250 @Override 251 public void registerListenersForThisClass() { 252 if (_listenersAreRegistered) return; 253 254 for (NamedBeanReference namedBeanReference : _namedBeanReferences.values()) { 255 if (namedBeanReference._handle != null) { 256 if (!namedBeanReference._listenOnAllProperties 257 && (namedBeanReference._type.getPropertyName() != null)) { 258 namedBeanReference._handle.getBean() 259 .addPropertyChangeListener(namedBeanReference._type.getPropertyName(), this); 260 } else { 261 namedBeanReference._handle.getBean() 262 .addPropertyChangeListener(this); 263 } 264 } 265 } 266 _listenersAreRegistered = true; 267 } 268 269 /** {@inheritDoc} */ 270 @Override 271 public void unregisterListenersForThisClass() { 272 if (!_listenersAreRegistered) return; 273 274 for (NamedBeanReference namedBeanReference : _namedBeanReferences.values()) { 275 if (namedBeanReference._handle != null) { 276 if (!namedBeanReference._listenOnAllProperties 277 && (namedBeanReference._type.getPropertyName() != null)) { 278 namedBeanReference._handle.getBean() 279 .removePropertyChangeListener(namedBeanReference._type.getPropertyName(), this); 280 } else { 281 namedBeanReference._handle.getBean() 282 .removePropertyChangeListener(this); 283 } 284 } 285 } 286 _listenersAreRegistered = false; 287 } 288 289 /** {@inheritDoc} */ 290 @Override 291 public void propertyChange(PropertyChangeEvent evt) { 292 boolean isQueueEmpty; 293 synchronized(this) { 294 isQueueEmpty = _eventQueue.isEmpty(); 295 _eventQueue.add(evt); 296 } 297 if (isQueueEmpty) { 298 getConditionalNG().execute(); 299 } 300 } 301 302 /** {@inheritDoc} */ 303 @Override 304 public void getUsageDetail(int level, NamedBean bean, List<NamedBeanUsageReport> report, NamedBean cdl) { 305 log.debug("getUsageReport :: ActionListenOnBeans: bean = {}, report = {}", cdl, report); 306 for (NamedBeanReference namedBeanReference : _namedBeanReferences.values()) { 307 if (namedBeanReference._handle != null) { 308 if (bean.equals(namedBeanReference._handle.getBean())) { 309 report.add(new NamedBeanUsageReport("LogixNGAction", cdl, getLongDescription())); 310 } 311 } 312 } 313 } 314 315 /** {@inheritDoc} */ 316 @Override 317 public void disposeMe() { 318 } 319 320 321 public static class NamedBeanReference { 322 323 private String _name; 324 private NamedBeanType _type; 325 private NamedBeanHandle<? extends NamedBean> _handle; 326 private boolean _listenOnAllProperties = false; 327 328 public NamedBeanReference(NamedBeanReference ref) { 329 this(ref._handle, ref._type, ref._listenOnAllProperties); 330 } 331 332 public NamedBeanReference(String name, NamedBeanType type, boolean all) { 333 _name = name; 334 _type = type; 335 _listenOnAllProperties = all; 336 337 if (_type != null) { 338 NamedBean bean = _type.getManager().getNamedBean(name); 339 if (bean != null) { 340 _handle = InstanceManager.getDefault(NamedBeanHandleManager.class).getNamedBeanHandle(_name, bean); 341 } 342 } 343 } 344 345 public NamedBeanReference(NamedBeanHandle<? extends NamedBean> handle, NamedBeanType type, boolean all) { 346 _name = handle != null ? handle.getName() : null; 347 _type = type; 348 _listenOnAllProperties = all; 349 _handle = handle; 350 } 351 352 public String getName() { 353 return _name; 354 } 355 356 public void setName(String name) { 357 _name = name; 358 updateHandle(); 359 } 360 361 public void setName(NamedBean bean) { 362 if (bean != null) { 363 _handle = InstanceManager.getDefault(NamedBeanHandleManager.class) 364 .getNamedBeanHandle(bean.getDisplayName(), bean); 365 _name = _handle.getName(); 366 } else { 367 _name = null; 368 _handle = null; 369 } 370 } 371 372 public void setName(NamedBeanHandle<? extends NamedBean> handle) { 373 if (handle != null) { 374 _handle = handle; 375 _name = _handle.getName(); 376 } else { 377 _name = null; 378 _handle = null; 379 } 380 } 381 382 public NamedBeanType getType() { 383 return _type; 384 } 385 386 public void setType(NamedBeanType type) { 387 if (type == null) { 388 log.warn("type is null"); 389 type = NamedBeanType.Turnout; 390 } 391 _type = type; 392 _handle = null; 393 } 394 395 public NamedBeanHandle<? extends NamedBean> getHandle() { 396 return _handle; 397 } 398 399 private void updateHandle() { 400 if (_type != null && _name != null && !_name.isEmpty()) { 401 NamedBean bean = _type.getManager().getNamedBean(_name); 402 if (bean != null) { 403 _handle = InstanceManager.getDefault(NamedBeanHandleManager.class).getNamedBeanHandle(_name, bean); 404 } else { 405 log.warn("Cannot find named bean {} in manager for {}", _name, _type.getManager().getBeanTypeHandled()); 406 _handle = null; 407 } 408 } else { 409 _handle = null; 410 } 411 } 412 413 public boolean getListenOnAllProperties() { 414 return _listenOnAllProperties; 415 } 416 417 public void setListenOnAllProperties(boolean listenOnAllProperties) { 418 _listenOnAllProperties = listenOnAllProperties; 419 } 420 421 // This method is used by ListenOnBeansTableModel 422 @Override 423 public String toString() { 424 if (_handle != null) return _handle.getName(); 425 else return ""; 426 } 427 } 428 429 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ActionListenOnBeans.class); 430 431}