001package jmri.managers; 002 003import java.beans.*; 004import java.text.DecimalFormat; 005import java.util.*; 006import java.util.concurrent.atomic.AtomicInteger; 007import java.util.concurrent.atomic.AtomicReference; 008 009import javax.annotation.CheckReturnValue; 010import javax.annotation.CheckForNull; 011import javax.annotation.Nonnull; 012import javax.annotation.OverridingMethodsMustInvokeSuper; 013 014import jmri.*; 015import jmri.beans.VetoableChangeSupport; 016import jmri.NamedBean.DuplicateSystemNameException; 017 018/** 019 * Abstract partial implementation for all Manager-type classes. 020 * <p> 021 * Note that this does not enforce any particular system naming convention at 022 * the present time. They're just names... 023 * <p> 024 * It does include, with AbstractNamedBean, the implementation of the normalized 025 * user name. 026 * <p> 027 * See source file for extensive implementation notes. 028 * 029 * @param <E> the class this manager supports 030 * @see jmri.NamedBean#normalizeUserName 031 * 032 * @author Bob Jacobsen Copyright (C) 2003 033 */ 034public abstract class AbstractManager<E extends NamedBean> extends VetoableChangeSupport implements Manager<E>, PropertyChangeListener, VetoableChangeListener { 035 036 // The data model consists of several components: 037 // * The primary reference is _beans, a SortedSet of NamedBeans, sorted automatically on system name. 038 // Currently that's implemented as a TreeSet; further performance work might change that 039 // Live access is available as an unmodifiableSortedSet via getNamedBeanSet() 040 // * The manager also maintains synchronized maps from SystemName -> NamedBean (_tsys) and UserName -> NamedBean (_tuser) 041 // These are not made available: get access through the manager calls 042 // These use regular HashMaps instead of some sorted form for efficiency 043 // * Caches for the List<String> getSystemNameList() and List<E> getNamedBeanList() calls 044 045 protected final SystemConnectionMemo memo; 046 protected final TreeSet<E> _beans; 047 protected final Hashtable<String, E> _tsys = new Hashtable<>(); // stores known E (NamedBean, i.e. Turnout) instances by system name 048 protected final Hashtable<String, E> _tuser = new Hashtable<>(); // stores known E (NamedBean, i.e. Turnout) instances by user name 049 protected final Map<String, Boolean> silencedProperties = new HashMap<>(); 050 protected final Set<String> silenceableProperties = new HashSet<>(); 051 052 // Auto names. The atomic integer is always created even if not used, to 053 // simplify concurrency. 054 AtomicInteger lastAutoNamedBeanRef = new AtomicInteger(0); 055 DecimalFormat paddedNumber = new DecimalFormat("0000"); 056 057 public AbstractManager(SystemConnectionMemo memo) { 058 this.memo = memo; 059 this._beans = new TreeSet<>(memo.getNamedBeanComparator(getNamedBeanClass())); 060 silenceableProperties.add("beans"); 061 setRegisterSelf(); 062 } 063 064 final void setRegisterSelf(){ 065 registerSelf(); 066 } 067 068 public AbstractManager() { 069 // create and use a reference to an internal connection 070 this(InstanceManager.getDefault(jmri.jmrix.internal.InternalSystemConnectionMemo.class)); 071 } 072 073 /** 074 * By default, register this manager to store as configuration information. 075 * Override to change that. 076 */ 077 @OverridingMethodsMustInvokeSuper 078 protected void registerSelf() { 079 log.debug("registerSelf for config of type {}", getClass()); 080 InstanceManager.getOptionalDefault(ConfigureManager.class).ifPresent(cm -> { 081 cm.registerConfig(this, getXMLOrder()); 082 log.debug("registering for config of type {}", getClass()); 083 }); 084 } 085 086 /** {@inheritDoc} */ 087 @Override 088 @Nonnull 089 public SystemConnectionMemo getMemo() { 090 return memo; 091 } 092 093 /** {@inheritDoc} */ 094 @Override 095 @Nonnull 096 public String makeSystemName(@Nonnull String s, boolean logErrors, Locale locale) { 097 try { 098 return Manager.super.makeSystemName(s, logErrors, locale); 099 } catch (IllegalArgumentException ex) { 100 if (logErrors || log.isTraceEnabled()) { 101 log.error("Invalid system name for {}: {}", getBeanTypeHandled(), ex.getMessage()); 102 } 103 throw ex; 104 } 105 } 106 107 /** {@inheritDoc} */ 108 @Override 109 @OverridingMethodsMustInvokeSuper 110 public void dispose() { 111 InstanceManager.getOptionalDefault(ConfigureManager.class).ifPresent(cm -> cm.deregister(this)); 112 _beans.clear(); 113 _tsys.clear(); 114 _tuser.clear(); 115 } 116 117 /** {@inheritDoc} */ 118 @CheckForNull 119 @Override 120 public E getBySystemName(@Nonnull String systemName) { 121 return _tsys.get(systemName); 122 } 123 124 /** 125 * Protected method used by subclasses to over-ride the default behavior of 126 * getBySystemName when a simple string lookup is not sufficient. 127 * 128 * @param systemName the system name to check 129 * @param comparator a Comparator encapsulating the system specific comparison behavior 130 * @return a named bean of the appropriate type, or null if not found 131 */ 132 @CheckForNull 133 protected E getBySystemName(String systemName, Comparator<String> comparator){ 134 for (Map.Entry<String,E> e : _tsys.entrySet()) { 135 if (0 == comparator.compare(e.getKey(), systemName)) { 136 return e.getValue(); 137 } 138 } 139 return null; 140 } 141 142 /** {@inheritDoc} */ 143 @Override 144 @CheckForNull 145 public E getByUserName(@Nonnull String userName) { 146 String normalizedUserName = NamedBean.normalizeUserName(userName); 147 return normalizedUserName != null ? _tuser.get(normalizedUserName) : null; 148 } 149 150 /** {@inheritDoc} */ 151 @CheckForNull 152 @Override 153 public E getNamedBean(@Nonnull String name) { 154 String normalizedUserName = NamedBean.normalizeUserName(name); 155 if (normalizedUserName != null) { 156 E b = getByUserName(normalizedUserName); 157 if (b != null) { 158 return b; 159 } 160 } 161 return getBySystemName(name); 162 } 163 164 /** {@inheritDoc} */ 165 @Override 166 @OverridingMethodsMustInvokeSuper 167 public void deleteBean(@Nonnull E bean, @Nonnull String property) throws PropertyVetoException { 168 // throws PropertyVetoException if vetoed 169 fireVetoableChange(property, bean, null); 170 if (property.equals("DoDelete")) { // NOI18N 171 deregister(bean); 172 bean.dispose(); 173 } 174 } 175 176 /** {@inheritDoc} */ 177 @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST", 178 justification="String already built for use in exception text") 179 @Override 180 @OverridingMethodsMustInvokeSuper 181 public void register(@Nonnull E s) { 182 String systemName = s.getSystemName(); 183 184 E existingBean = getBySystemName(systemName); 185 if (existingBean != null) { 186 if (s == existingBean) { 187 log.debug("the named bean is registered twice: {}", systemName); 188 } else { 189 log.error("systemName is already registered: {}", systemName); 190 throw new DuplicateSystemNameException("systemName is already registered: " + systemName); 191 } 192 } else { 193 // Check if the manager already has a bean with a system name that is 194 // not equal to the system name of the new bean, but there the two 195 // system names are treated as the same. For example LT1 and LT01. 196 if (_beans.contains(s)) { 197 final AtomicReference<String> oldSysName = new AtomicReference<>(); 198 Comparator<E> c = memo.getNamedBeanComparator(getNamedBeanClass()); 199 _beans.forEach(t -> { 200 if (c.compare(s, t) == 0) { 201 oldSysName.set(t.getSystemName()); 202 } 203 }); 204 if (!systemName.equals(oldSysName.get())) { 205 String msg = String.format("systemName is already registered. Current system name: %s. New system name: %s", 206 oldSysName, systemName); 207 log.error(msg); 208 throw new DuplicateSystemNameException(msg); 209 } 210 } 211 } 212 213 // save this bean 214 _beans.add(s); 215 _tsys.put(systemName, s); 216 registerUserName(s); 217 218 // notifications 219 int position = getPosition(s); 220 fireDataListenersAdded(position, position, s); 221 if (!silencedProperties.getOrDefault("beans", false)) { 222 fireIndexedPropertyChange("beans", position, null, s); 223 } 224 firePropertyChange("length", null, _beans.size()); 225 // listen for name and state changes to forward 226 s.addPropertyChangeListener(this); 227 } 228 229 // not efficient, but does job for now 230 private int getPosition(E s) { 231 if (_beans.contains(s)) { 232 return _beans.headSet(s, false).size(); 233 } else { 234 return -1; 235 } 236 } 237 238 /** 239 * Invoked by {@link #register(NamedBean)} to register the user name of the 240 * bean. 241 * 242 * @param s the bean to register 243 */ 244 protected void registerUserName(E s) { 245 String userName = s.getUserName(); 246 if (userName == null) { 247 return; 248 } 249 250 handleUserNameUniqueness(s); 251 // since we've handled uniqueness, 252 // store the new bean under the name 253 _tuser.put(userName, s); 254 } 255 256 /** 257 * Invoked by {@link #registerUserName(NamedBean)} to ensure uniqueness of 258 * the NamedBean during registration. 259 * 260 * @param s the bean to register 261 */ 262 protected void handleUserNameUniqueness(E s) { 263 String userName = s.getUserName(); 264 // enforce uniqueness of user names 265 // by setting username to null in any existing bean with the same name 266 // Note that this is not a "move" operation for the user name 267 if (userName != null && _tuser.get(userName) != null && _tuser.get(userName) != s) { 268 _tuser.get(userName).setUserName(null); 269 } 270 } 271 272 /** {@inheritDoc} */ 273 @Override 274 @OverridingMethodsMustInvokeSuper 275 public void deregister(@Nonnull E s) { 276 int position = getPosition(s); 277 278 // stop listening for user name changes 279 s.removePropertyChangeListener(this); 280 281 // remove bean from local storage 282 String systemName = s.getSystemName(); 283 _beans.remove(s); 284 _tsys.remove(systemName); 285 String userName = s.getUserName(); 286 if (userName != null) { 287 _tuser.remove(userName); 288 } 289 290 // notifications 291 fireDataListenersRemoved(position, position, s); 292 if (!silencedProperties.getOrDefault("beans", false)) { 293 fireIndexedPropertyChange("beans", position, s, null); 294 } 295 firePropertyChange("length", null, _beans.size()); 296 } 297 298 /** 299 * By default there are no custom properties. 300 * 301 * @return empty list 302 */ 303 @Override 304 @Nonnull 305 public List<NamedBeanPropertyDescriptor<?>> getKnownBeanProperties() { 306 return new LinkedList<>(); 307 } 308 309 /** 310 * Get the outer bean of an encapsulated bean. 311 * Some managers encapsulates the beans and those managers needs to 312 * override this method. 313 * @param bean the bean 314 * @return the most outer bean or the bean itself if there is no 315 * outer bean 316 */ 317 protected E getOuterBean(E bean) { 318 return bean; 319 } 320 321 /** 322 * The PropertyChangeListener interface in this class is intended to keep 323 * track of user name changes to individual NamedBeans. It is not completely 324 * implemented yet. In particular, listeners are not added to newly 325 * registered objects. 326 * 327 * @param e the event 328 */ 329 @Override 330 @SuppressWarnings("unchecked") // The cast of getSource() to E can't be checked due to type erasure, but we catch errors 331 @OverridingMethodsMustInvokeSuper 332 public void propertyChange(PropertyChangeEvent e) { 333 if (e.getPropertyName().equals("UserName")) { 334 String old = (String) e.getOldValue(); // previous user name 335 String now = (String) e.getNewValue(); // current user name 336 try { // really should always succeed 337 E t = getOuterBean((E) e.getSource()); 338 if (old != null) { 339 _tuser.remove(old); // remove old name for this bean 340 } 341 if (now != null) { 342 // was there previously a bean with the new name? 343 if (_tuser.get(now) != null && _tuser.get(now) != t) { 344 // If so, clear. Note that this is not a "move" operation 345 _tuser.get(now).setUserName(null); 346 } 347 _tuser.put(now, t); // put new name for this bean 348 } 349 } catch (ClassCastException ex) { 350 log.error("Received event of wrong type {}", e.getSource().getClass().getName(), ex); 351 } 352 353 // called DisplayListName, as DisplayName might get used at some point by a NamedBean 354 firePropertyChange("DisplayListName", old, now); // NOI18N 355 } 356 } 357 358 /** {@inheritDoc} */ 359 @Override 360 @CheckReturnValue 361 public int getObjectCount() { return _beans.size();} 362 363 /** {@inheritDoc} */ 364 @Override 365 @Nonnull 366 public SortedSet<E> getNamedBeanSet() { 367 return Collections.unmodifiableSortedSet(_beans); 368 } 369 370 /** 371 * Inform all registered listeners of a vetoable change. If the 372 * propertyName is "CanDelete" ALL listeners with an interest in the bean 373 * will throw an exception, which is recorded returned back to the invoking 374 * method, so that it can be presented back to the user. However if a 375 * listener decides that the bean can not be deleted then it should throw an 376 * exception with a property name of "DoNotDelete", this is thrown back up 377 * to the user and the delete process should be aborted. 378 * 379 * @param p The programmatic name of the property that is to be changed. 380 * "CanDelete" will inquire with all listeners if the item can 381 * be deleted. "DoDelete" tells the listener to delete the item. 382 * @param old The old value of the property. 383 * @param n The new value of the property. 384 * @throws PropertyVetoException if the recipients wishes the delete to be 385 * aborted. 386 */ 387 @OverridingMethodsMustInvokeSuper 388 @Override 389 public void fireVetoableChange(String p, Object old, Object n) throws PropertyVetoException { 390 PropertyChangeEvent evt = new PropertyChangeEvent(this, p, old, n); 391 if (p.equals("CanDelete")) { // NOI18N 392 StringBuilder message = new StringBuilder(); 393 for (VetoableChangeListener vc : vetoableChangeSupport.getVetoableChangeListeners()) { 394 try { 395 vc.vetoableChange(evt); 396 } catch (PropertyVetoException e) { 397 if (e.getPropertyChangeEvent().getPropertyName().equals("DoNotDelete")) { // NOI18N 398 log.info("Do Not Delete : {}", e.getMessage()); 399 throw e; 400 } 401 message.append(e.getMessage()).append("<hr>"); // NOI18N 402 } 403 } 404 throw new PropertyVetoException(message.toString(), evt); 405 } else { 406 try { 407 vetoableChangeSupport.fireVetoableChange(evt); 408 } catch (PropertyVetoException e) { 409 log.error("Change vetoed.", e); 410 } 411 } 412 } 413 414 /** {@inheritDoc} */ 415 @Override 416 @OverridingMethodsMustInvokeSuper 417 public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { 418 419 if ("CanDelete".equals(evt.getPropertyName())) { // NOI18N 420 StringBuilder message = new StringBuilder(); 421 message.append(Bundle.getMessage("VetoFoundIn", getBeanTypeHandled())) 422 .append("<ul>"); 423 boolean found = false; 424 for (NamedBean nb : _beans) { 425 try { 426 nb.vetoableChange(evt); 427 } catch (PropertyVetoException e) { 428 if (e.getPropertyChangeEvent().getPropertyName().equals("DoNotDelete")) { // NOI18N 429 throw e; 430 } 431 found = true; 432 message.append("<li>") 433 .append(e.getMessage()) 434 .append("</li>"); 435 } 436 } 437 message.append("</ul>") 438 .append(Bundle.getMessage("VetoWillBeRemovedFrom", getBeanTypeHandled())); 439 if (found) { 440 throw new PropertyVetoException(message.toString(), evt); 441 } 442 } else { 443 for (NamedBean nb : _beans) { 444 // throws PropertyVetoException if vetoed 445 nb.vetoableChange(evt); 446 } 447 } 448 } 449 450 /** 451 * {@inheritDoc} 452 * 453 * @return {@link jmri.Manager.NameValidity#INVALID} if system name does not 454 * start with 455 * {@link #getSystemNamePrefix()}; {@link jmri.Manager.NameValidity#VALID_AS_PREFIX_ONLY} 456 * if system name equals {@link #getSystemNamePrefix()}; otherwise 457 * {@link jmri.Manager.NameValidity#VALID} to allow Managers that do 458 * not perform more specific validation to be considered valid. 459 */ 460 @Override 461 public NameValidity validSystemNameFormat(@Nonnull String systemName) { 462 if (getSystemNamePrefix().equals(systemName)) { 463 return NameValidity.VALID_AS_PREFIX_ONLY; 464 } 465 return systemName.startsWith(getSystemNamePrefix()) ? NameValidity.VALID : NameValidity.INVALID; 466 } 467 468 /** 469 * {@inheritDoc} 470 * 471 * The implementation in {@link AbstractManager} should be final, but is not 472 * for four managers that have arbitrary prefixes. 473 */ 474 @Override 475 @Nonnull 476 public final String getSystemPrefix() { 477 return memo.getSystemPrefix(); 478 } 479 480 /** 481 * {@inheritDoc} 482 */ 483 @Override 484 @OverridingMethodsMustInvokeSuper 485 public void setPropertyChangesSilenced(@Nonnull String propertyName, boolean silenced) { 486 if (!silenceableProperties.contains(propertyName)) { 487 throw new IllegalArgumentException("Property " + propertyName + " cannot be silenced."); 488 } 489 silencedProperties.put(propertyName, silenced); 490 if (propertyName.equals("beans") && !silenced) { 491 fireIndexedPropertyChange("beans", _beans.size(), null, null); 492 } 493 } 494 495 /** {@inheritDoc} */ 496 @Override 497 public void addDataListener(ManagerDataListener<E> e) { 498 if (e != null) listeners.add(e); 499 } 500 501 /** {@inheritDoc} */ 502 @Override 503 public void removeDataListener(ManagerDataListener<E> e) { 504 if (e != null) listeners.remove(e); 505 } 506 507 private final List<ManagerDataListener<E>> listeners = new ArrayList<>(); 508 509 private boolean muted = false; 510 511 /** {@inheritDoc} */ 512 @Override 513 public void setDataListenerMute(boolean m) { 514 if (muted && !m) { 515 // send a total update, as we haven't kept track of specifics 516 ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.CONTENTS_CHANGED, 0, getObjectCount()-1, null); 517 listeners.forEach(listener -> listener.contentsChanged(e)); 518 } 519 this.muted = m; 520 } 521 522 protected void fireDataListenersAdded(int start, int end, E changedBean) { 523 if (muted) return; 524 ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.INTERVAL_ADDED, start, end, changedBean); 525 listeners.forEach(m -> m.intervalAdded(e)); 526 } 527 528 protected void fireDataListenersRemoved(int start, int end, E changedBean) { 529 if (muted) return; 530 ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.INTERVAL_REMOVED, start, end, changedBean); 531 listeners.forEach(m -> m.intervalRemoved(e)); 532 } 533 534 public void updateAutoNumber(String systemName) { 535 /* The following keeps track of the last created auto system name. 536 currently we do not reuse numbers, although there is nothing to stop the 537 user from manually recreating them */ 538 String autoPrefix = getSubSystemNamePrefix() + ":AUTO:"; 539 if (systemName.startsWith(autoPrefix)) { 540 try { 541 int autoNumber = Integer.parseInt(systemName.substring(autoPrefix.length())); 542 lastAutoNamedBeanRef.accumulateAndGet(autoNumber, Math::max); 543 } catch (NumberFormatException e) { 544 log.warn("Auto generated SystemName {} is not in the correct format", systemName); 545 } 546 } 547 } 548 549 public String getAutoSystemName() { 550 int nextAutoBlockRef = lastAutoNamedBeanRef.incrementAndGet(); 551 StringBuilder b = new StringBuilder(getSubSystemNamePrefix() + ":AUTO:"); 552 String nextNumber = paddedNumber.format(nextAutoBlockRef); 553 b.append(nextNumber); 554 return b.toString(); 555 } 556 557 /** 558 * Create a System Name from hardware address and system letter prefix. 559 * AbstractManager performs no validation. 560 * @param curAddress hardware address, no system prefix or type letter. 561 * @param prefix - just system prefix, not including Type Letter. 562 * @return full system name with system prefix, type letter and hardware address. 563 * @throws JmriException if unable to create a system name. 564 */ 565 public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException { 566 return prefix + typeLetter() + curAddress; 567 } 568 569 /** 570 * checks for numeric-only system names. 571 * @param curAddress the System name ( excluding both prefix and type letter) to check. 572 * @return unchanged if is numeric string. 573 * @throws JmriException if not numeric. 574 */ 575 protected String checkNumeric(@Nonnull String curAddress) throws JmriException { 576 try { 577 Integer.parseInt(curAddress); 578 } catch (java.lang.NumberFormatException ex) { 579 throw new JmriException("Hardware Address passed "+curAddress+" should be a number"); 580 } 581 return curAddress; 582 } 583 584 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractManager.class); 585 586}