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(PROPERTY_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_DO_DELETE.equals(property) ) { 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 = isPositionNeeded() ? getPosition(s) : -1; 220 fireDataListenersAdded(position, position, s); 221 if (!silencedProperties.getOrDefault(PROPERTY_BEANS, false)) { 222 fireIndexedPropertyChange(PROPERTY_BEANS, position, null, s); 223 } 224 firePropertyChange(PROPERTY_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 // beans are often added in order, check the ends before counting 233 if (_beans.last() == s) { 234 return _beans.size() - 1; 235 } 236 if (_beans.first() == s) { 237 return 0; 238 } 239 return _beans.headSet(s, false).size(); 240 } else { 241 return -1; 242 } 243 } 244 245 // the position is only sent to data listeners and "beans" listeners 246 private boolean isPositionNeeded() { 247 return (!muted && !listeners.isEmpty()) 248 || (!silencedProperties.getOrDefault(PROPERTY_BEANS, false) 249 && propertyChangeSupport.hasListeners(PROPERTY_BEANS)); 250 } 251 252 /** 253 * Invoked by {@link #register(NamedBean)} to register the user name of the 254 * bean. 255 * 256 * @param s the bean to register 257 */ 258 protected void registerUserName(E s) { 259 String userName = s.getUserName(); 260 if (userName == null) { 261 return; 262 } 263 264 handleUserNameUniqueness(s); 265 // since we've handled uniqueness, 266 // store the new bean under the name 267 _tuser.put(userName, s); 268 } 269 270 /** 271 * Invoked by {@link #registerUserName(NamedBean)} to ensure uniqueness of 272 * the NamedBean during registration. 273 * 274 * @param s the bean to register 275 */ 276 protected void handleUserNameUniqueness(E s) { 277 String userName = s.getUserName(); 278 // enforce uniqueness of user names 279 // by setting username to null in any existing bean with the same name 280 // Note that this is not a "move" operation for the user name 281 if (userName != null && _tuser.get(userName) != null && _tuser.get(userName) != s) { 282 _tuser.get(userName).setUserName(null); 283 } 284 } 285 286 /** {@inheritDoc} */ 287 @Override 288 @OverridingMethodsMustInvokeSuper 289 public void deregister(@Nonnull E s) { 290 int position = isPositionNeeded() ? getPosition(s) : -1; 291 292 // stop listening for user name changes 293 s.removePropertyChangeListener(this); 294 295 // remove bean from local storage 296 String systemName = s.getSystemName(); 297 _beans.remove(s); 298 _tsys.remove(systemName); 299 String userName = s.getUserName(); 300 if (userName != null) { 301 _tuser.remove(userName); 302 } 303 304 // notifications 305 fireDataListenersRemoved(position, position, s); 306 if (!silencedProperties.getOrDefault(PROPERTY_BEANS, false)) { 307 fireIndexedPropertyChange(PROPERTY_BEANS, position, s, null); 308 } 309 firePropertyChange(PROPERTY_LENGTH, null, _beans.size()); 310 } 311 312 /** 313 * By default there are no custom properties. 314 * 315 * @return empty list 316 */ 317 @Override 318 @Nonnull 319 public List<NamedBeanPropertyDescriptor<?>> getKnownBeanProperties() { 320 return new LinkedList<>(); 321 } 322 323 /** 324 * Get the outer bean of an encapsulated bean. 325 * Some managers encapsulates the beans and those managers needs to 326 * override this method. 327 * @param bean the bean 328 * @return the most outer bean or the bean itself if there is no 329 * outer bean 330 */ 331 protected E getOuterBean(E bean) { 332 return bean; 333 } 334 335 /** 336 * The PropertyChangeListener interface in this class is intended to keep 337 * track of user name changes to individual NamedBeans. It is not completely 338 * implemented yet. In particular, listeners are not added to newly 339 * registered objects. 340 * 341 * @param e the event 342 */ 343 @Override 344 @SuppressWarnings("unchecked") // The cast of getSource() to E can't be checked due to type erasure, but we catch errors 345 @OverridingMethodsMustInvokeSuper 346 public void propertyChange(PropertyChangeEvent e) { 347 if ( NamedBean.PROPERTY_USERNAME.equals( e.getPropertyName())) { 348 String old = (String) e.getOldValue(); // previous user name 349 String now = (String) e.getNewValue(); // current user name 350 try { // really should always succeed 351 E t = getOuterBean((E) e.getSource()); 352 if (old != null) { 353 _tuser.remove(old); // remove old name for this bean 354 } 355 if (now != null) { 356 // was there previously a bean with the new name? 357 if (_tuser.get(now) != null && _tuser.get(now) != t) { 358 // If so, clear. Note that this is not a "move" operation 359 _tuser.get(now).setUserName(null); 360 } 361 _tuser.put(now, t); // put new name for this bean 362 } 363 } catch (ClassCastException ex) { 364 log.error("Received event of wrong type {}", e.getSource().getClass().getName(), ex); 365 } 366 367 // called DisplayListName, as DisplayName might get used at some point by a NamedBean 368 firePropertyChange(PROPERTY_DISPLAY_LIST_NAME, old, now); // NOI18N 369 } 370 } 371 372 /** {@inheritDoc} */ 373 @Override 374 @CheckReturnValue 375 public int getObjectCount() { return _beans.size();} 376 377 /** {@inheritDoc} */ 378 @Override 379 @Nonnull 380 public SortedSet<E> getNamedBeanSet() { 381 return Collections.unmodifiableSortedSet(_beans); 382 } 383 384 /** 385 * Inform all registered listeners of a vetoable change. If the 386 * propertyName is "CanDelete" ALL listeners with an interest in the bean 387 * will throw an exception, which is recorded returned back to the invoking 388 * method, so that it can be presented back to the user. However if a 389 * listener decides that the bean can not be deleted then it should throw an 390 * exception with a property name of "DoNotDelete", this is thrown back up 391 * to the user and the delete process should be aborted. 392 * 393 * @param p The programmatic name of the property that is to be changed. 394 * "CanDelete" will inquire with all listeners if the item can 395 * be deleted. "DoDelete" tells the listener to delete the item. 396 * @param old The old value of the property. 397 * @param n The new value of the property. 398 * @throws PropertyVetoException if the recipients wishes the delete to be 399 * aborted. 400 */ 401 @OverridingMethodsMustInvokeSuper 402 @Override 403 public void fireVetoableChange(String p, Object old, Object n) throws PropertyVetoException { 404 PropertyChangeEvent evt = new PropertyChangeEvent(this, p, old, n); 405 if ( PROPERTY_CAN_DELETE.equals(p) ) { 406 StringBuilder message = new StringBuilder(); 407 for (VetoableChangeListener vc : vetoableChangeSupport.getVetoableChangeListeners()) { 408 try { 409 vc.vetoableChange(evt); 410 } catch (PropertyVetoException e) { 411 if (PROPERTY_DO_NOT_DELETE.equals(e.getPropertyChangeEvent().getPropertyName())) { 412 log.info("Do Not Delete : {}", e.getMessage()); 413 throw e; 414 } 415 message.append(e.getMessage()).append("<hr>"); // NOI18N 416 } 417 } 418 throw new PropertyVetoException(message.toString(), evt); 419 } else { 420 try { 421 vetoableChangeSupport.fireVetoableChange(evt); 422 } catch (PropertyVetoException e) { 423 log.error("Change vetoed.", e); 424 } 425 } 426 } 427 428 /** {@inheritDoc} */ 429 @Override 430 @OverridingMethodsMustInvokeSuper 431 public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { 432 433 if (PROPERTY_CAN_DELETE.equals(evt.getPropertyName())) { 434 StringBuilder message = new StringBuilder(); 435 message.append(Bundle.getMessage("VetoFoundIn", getBeanTypeHandled())) 436 .append("<ul>"); 437 boolean found = false; 438 for (NamedBean nb : _beans) { 439 try { 440 nb.vetoableChange(evt); 441 } catch (PropertyVetoException e) { 442 if (PROPERTY_DO_NOT_DELETE.equals(e.getPropertyChangeEvent().getPropertyName())) { 443 throw e; 444 } 445 found = true; 446 message.append("<li>") 447 .append(e.getMessage()) 448 .append("</li>"); 449 } 450 } 451 message.append("</ul>") 452 .append(Bundle.getMessage("VetoWillBeRemovedFrom", getBeanTypeHandled())); 453 if (found) { 454 throw new PropertyVetoException(message.toString(), evt); 455 } 456 } else { 457 for (NamedBean nb : _beans) { 458 // throws PropertyVetoException if vetoed 459 nb.vetoableChange(evt); 460 } 461 } 462 } 463 464 /** 465 * {@inheritDoc} 466 * 467 * @return {@link jmri.Manager.NameValidity#INVALID} if system name does not 468 * start with 469 * {@link #getSystemNamePrefix()}; {@link jmri.Manager.NameValidity#VALID_AS_PREFIX_ONLY} 470 * if system name equals {@link #getSystemNamePrefix()}; otherwise 471 * {@link jmri.Manager.NameValidity#VALID} to allow Managers that do 472 * not perform more specific validation to be considered valid. 473 */ 474 @Override 475 public NameValidity validSystemNameFormat(@Nonnull String systemName) { 476 if (getSystemNamePrefix().equals(systemName)) { 477 return NameValidity.VALID_AS_PREFIX_ONLY; 478 } 479 return systemName.startsWith(getSystemNamePrefix()) ? NameValidity.VALID : NameValidity.INVALID; 480 } 481 482 /** 483 * {@inheritDoc} 484 * 485 * The implementation in {@link AbstractManager} should be final, but is not 486 * for four managers that have arbitrary prefixes. 487 */ 488 @Override 489 @Nonnull 490 public final String getSystemPrefix() { 491 return memo.getSystemPrefix(); 492 } 493 494 /** 495 * {@inheritDoc} 496 */ 497 @Override 498 @OverridingMethodsMustInvokeSuper 499 public void setPropertyChangesSilenced(@Nonnull String propertyName, boolean silenced) { 500 if (!silenceableProperties.contains(propertyName)) { 501 throw new IllegalArgumentException("Property " + propertyName + " cannot be silenced."); 502 } 503 silencedProperties.put(propertyName, silenced); 504 if (PROPERTY_BEANS.equals(propertyName) && !silenced) { 505 fireIndexedPropertyChange(PROPERTY_BEANS, _beans.size(), null, null); 506 } 507 } 508 509 /** {@inheritDoc} */ 510 @Override 511 public void addDataListener(ManagerDataListener<E> e) { 512 if (e != null) listeners.add(e); 513 } 514 515 /** {@inheritDoc} */ 516 @Override 517 public void removeDataListener(ManagerDataListener<E> e) { 518 if (e != null) listeners.remove(e); 519 } 520 521 private final List<ManagerDataListener<E>> listeners = new ArrayList<>(); 522 523 private boolean muted = false; 524 525 /** {@inheritDoc} */ 526 @Override 527 public void setDataListenerMute(boolean m) { 528 if (muted && !m) { 529 // send a total update, as we haven't kept track of specifics 530 ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.CONTENTS_CHANGED, 0, getObjectCount()-1, null); 531 listeners.forEach(listener -> listener.contentsChanged(e)); 532 } 533 this.muted = m; 534 } 535 536 protected void fireDataListenersAdded(int start, int end, E changedBean) { 537 if (muted) return; 538 ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.INTERVAL_ADDED, start, end, changedBean); 539 listeners.forEach(m -> m.intervalAdded(e)); 540 } 541 542 protected void fireDataListenersRemoved(int start, int end, E changedBean) { 543 if (muted) return; 544 ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.INTERVAL_REMOVED, start, end, changedBean); 545 listeners.forEach(m -> m.intervalRemoved(e)); 546 } 547 548 public void updateAutoNumber(String systemName) { 549 /* The following keeps track of the last created auto system name. 550 currently we do not reuse numbers, although there is nothing to stop the 551 user from manually recreating them */ 552 String autoPrefix = getSubSystemNamePrefix() + ":AUTO:"; 553 if (systemName.startsWith(autoPrefix)) { 554 try { 555 int autoNumber = Integer.parseInt(systemName.substring(autoPrefix.length())); 556 lastAutoNamedBeanRef.accumulateAndGet(autoNumber, Math::max); 557 } catch (NumberFormatException e) { 558 log.warn("Auto generated SystemName {} is not in the correct format", systemName); 559 } 560 } 561 } 562 563 public String getAutoSystemName() { 564 int nextAutoBlockRef = lastAutoNamedBeanRef.incrementAndGet(); 565 StringBuilder b = new StringBuilder(getSubSystemNamePrefix() + ":AUTO:"); 566 String nextNumber = paddedNumber.format(nextAutoBlockRef); 567 b.append(nextNumber); 568 return b.toString(); 569 } 570 571 /** 572 * Create a System Name from hardware address and system letter prefix. 573 * AbstractManager performs no validation. 574 * @param curAddress hardware address, no system prefix or type letter. 575 * @param prefix - just system prefix, not including Type Letter. 576 * @return full system name with system prefix, type letter and hardware address. 577 * @throws JmriException if unable to create a system name. 578 */ 579 public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException { 580 return prefix + typeLetter() + curAddress; 581 } 582 583 /** 584 * checks for numeric-only system names. 585 * @param curAddress the System name ( excluding both prefix and type letter) to check. 586 * @return unchanged if is numeric string. 587 * @throws JmriException if not numeric. 588 */ 589 protected String checkNumeric(@Nonnull String curAddress) throws JmriException { 590 try { 591 Integer.parseInt(curAddress); 592 } catch (java.lang.NumberFormatException ex) { 593 throw new JmriException("Hardware Address passed "+curAddress+" should be a number"); 594 } 595 return curAddress; 596 } 597 598 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractManager.class); 599 600}