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 * The PropertyChangeListener interface in this class is intended to keep 311 * track of user name changes to individual NamedBeans. It is not completely 312 * implemented yet. In particular, listeners are not added to newly 313 * registered objects. 314 * 315 * @param e the event 316 */ 317 @Override 318 @SuppressWarnings("unchecked") // The cast of getSource() to E can't be checked due to type erasure, but we catch errors 319 @OverridingMethodsMustInvokeSuper 320 public void propertyChange(PropertyChangeEvent e) { 321 if (e.getPropertyName().equals("UserName")) { 322 String old = (String) e.getOldValue(); // previous user name 323 String now = (String) e.getNewValue(); // current user name 324 try { // really should always succeed 325 E t = (E) e.getSource(); 326 if (old != null) { 327 _tuser.remove(old); // remove old name for this bean 328 } 329 if (now != null) { 330 // was there previously a bean with the new name? 331 if (_tuser.get(now) != null && _tuser.get(now) != t) { 332 // If so, clear. Note that this is not a "move" operation 333 _tuser.get(now).setUserName(null); 334 } 335 _tuser.put(now, t); // put new name for this bean 336 } 337 } catch (ClassCastException ex) { 338 log.error("Received event of wrong type {}", e.getSource().getClass().getName(), ex); 339 } 340 341 // called DisplayListName, as DisplayName might get used at some point by a NamedBean 342 firePropertyChange("DisplayListName", old, now); // NOI18N 343 } 344 } 345 346 /** {@inheritDoc} */ 347 @Override 348 @CheckReturnValue 349 public int getObjectCount() { return _beans.size();} 350 351 /** {@inheritDoc} */ 352 @Override 353 @Nonnull 354 public SortedSet<E> getNamedBeanSet() { 355 return Collections.unmodifiableSortedSet(_beans); 356 } 357 358 /** 359 * Inform all registered listeners of a vetoable change. If the 360 * propertyName is "CanDelete" ALL listeners with an interest in the bean 361 * will throw an exception, which is recorded returned back to the invoking 362 * method, so that it can be presented back to the user. However if a 363 * listener decides that the bean can not be deleted then it should throw an 364 * exception with a property name of "DoNotDelete", this is thrown back up 365 * to the user and the delete process should be aborted. 366 * 367 * @param p The programmatic name of the property that is to be changed. 368 * "CanDelete" will inquire with all listeners if the item can 369 * be deleted. "DoDelete" tells the listener to delete the item. 370 * @param old The old value of the property. 371 * @param n The new value of the property. 372 * @throws PropertyVetoException if the recipients wishes the delete to be 373 * aborted. 374 */ 375 @OverridingMethodsMustInvokeSuper 376 @Override 377 public void fireVetoableChange(String p, Object old, Object n) throws PropertyVetoException { 378 PropertyChangeEvent evt = new PropertyChangeEvent(this, p, old, n); 379 if (p.equals("CanDelete")) { // NOI18N 380 StringBuilder message = new StringBuilder(); 381 for (VetoableChangeListener vc : vetoableChangeSupport.getVetoableChangeListeners()) { 382 try { 383 vc.vetoableChange(evt); 384 } catch (PropertyVetoException e) { 385 if (e.getPropertyChangeEvent().getPropertyName().equals("DoNotDelete")) { // NOI18N 386 log.info("Do Not Delete : {}", e.getMessage()); 387 throw e; 388 } 389 message.append(e.getMessage()).append("<hr>"); // NOI18N 390 } 391 } 392 throw new PropertyVetoException(message.toString(), evt); 393 } else { 394 try { 395 vetoableChangeSupport.fireVetoableChange(evt); 396 } catch (PropertyVetoException e) { 397 log.error("Change vetoed.", e); 398 } 399 } 400 } 401 402 /** {@inheritDoc} */ 403 @Override 404 @OverridingMethodsMustInvokeSuper 405 public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { 406 407 if ("CanDelete".equals(evt.getPropertyName())) { // NOI18N 408 StringBuilder message = new StringBuilder(); 409 message.append(Bundle.getMessage("VetoFoundIn", getBeanTypeHandled())) 410 .append("<ul>"); 411 boolean found = false; 412 for (NamedBean nb : _beans) { 413 try { 414 nb.vetoableChange(evt); 415 } catch (PropertyVetoException e) { 416 if (e.getPropertyChangeEvent().getPropertyName().equals("DoNotDelete")) { // NOI18N 417 throw e; 418 } 419 found = true; 420 message.append("<li>") 421 .append(e.getMessage()) 422 .append("</li>"); 423 } 424 } 425 message.append("</ul>") 426 .append(Bundle.getMessage("VetoWillBeRemovedFrom", getBeanTypeHandled())); 427 if (found) { 428 throw new PropertyVetoException(message.toString(), evt); 429 } 430 } else { 431 for (NamedBean nb : _beans) { 432 // throws PropertyVetoException if vetoed 433 nb.vetoableChange(evt); 434 } 435 } 436 } 437 438 /** 439 * {@inheritDoc} 440 * 441 * @return {@link jmri.Manager.NameValidity#INVALID} if system name does not 442 * start with 443 * {@link #getSystemNamePrefix()}; {@link jmri.Manager.NameValidity#VALID_AS_PREFIX_ONLY} 444 * if system name equals {@link #getSystemNamePrefix()}; otherwise 445 * {@link jmri.Manager.NameValidity#VALID} to allow Managers that do 446 * not perform more specific validation to be considered valid. 447 */ 448 @Override 449 public NameValidity validSystemNameFormat(@Nonnull String systemName) { 450 if (getSystemNamePrefix().equals(systemName)) { 451 return NameValidity.VALID_AS_PREFIX_ONLY; 452 } 453 return systemName.startsWith(getSystemNamePrefix()) ? NameValidity.VALID : NameValidity.INVALID; 454 } 455 456 /** 457 * {@inheritDoc} 458 * 459 * The implementation in {@link AbstractManager} should be final, but is not 460 * for four managers that have arbitrary prefixes. 461 */ 462 @Override 463 @Nonnull 464 public final String getSystemPrefix() { 465 return memo.getSystemPrefix(); 466 } 467 468 /** 469 * {@inheritDoc} 470 */ 471 @Override 472 @OverridingMethodsMustInvokeSuper 473 public void setPropertyChangesSilenced(@Nonnull String propertyName, boolean silenced) { 474 if (!silenceableProperties.contains(propertyName)) { 475 throw new IllegalArgumentException("Property " + propertyName + " cannot be silenced."); 476 } 477 silencedProperties.put(propertyName, silenced); 478 if (propertyName.equals("beans") && !silenced) { 479 fireIndexedPropertyChange("beans", _beans.size(), null, null); 480 } 481 } 482 483 /** {@inheritDoc} */ 484 @Override 485 public void addDataListener(ManagerDataListener<E> e) { 486 if (e != null) listeners.add(e); 487 } 488 489 /** {@inheritDoc} */ 490 @Override 491 public void removeDataListener(ManagerDataListener<E> e) { 492 if (e != null) listeners.remove(e); 493 } 494 495 private final List<ManagerDataListener<E>> listeners = new ArrayList<>(); 496 497 private boolean muted = false; 498 499 /** {@inheritDoc} */ 500 @Override 501 public void setDataListenerMute(boolean m) { 502 if (muted && !m) { 503 // send a total update, as we haven't kept track of specifics 504 ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.CONTENTS_CHANGED, 0, getObjectCount()-1, null); 505 listeners.forEach(listener -> listener.contentsChanged(e)); 506 } 507 this.muted = m; 508 } 509 510 protected void fireDataListenersAdded(int start, int end, E changedBean) { 511 if (muted) return; 512 ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.INTERVAL_ADDED, start, end, changedBean); 513 listeners.forEach(m -> m.intervalAdded(e)); 514 } 515 516 protected void fireDataListenersRemoved(int start, int end, E changedBean) { 517 if (muted) return; 518 ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.INTERVAL_REMOVED, start, end, changedBean); 519 listeners.forEach(m -> m.intervalRemoved(e)); 520 } 521 522 public void updateAutoNumber(String systemName) { 523 /* The following keeps track of the last created auto system name. 524 currently we do not reuse numbers, although there is nothing to stop the 525 user from manually recreating them */ 526 String autoPrefix = getSubSystemNamePrefix() + ":AUTO:"; 527 if (systemName.startsWith(autoPrefix)) { 528 try { 529 int autoNumber = Integer.parseInt(systemName.substring(autoPrefix.length())); 530 lastAutoNamedBeanRef.accumulateAndGet(autoNumber, Math::max); 531 } catch (NumberFormatException e) { 532 log.warn("Auto generated SystemName {} is not in the correct format", systemName); 533 } 534 } 535 } 536 537 public String getAutoSystemName() { 538 int nextAutoBlockRef = lastAutoNamedBeanRef.incrementAndGet(); 539 StringBuilder b = new StringBuilder(getSubSystemNamePrefix() + ":AUTO:"); 540 String nextNumber = paddedNumber.format(nextAutoBlockRef); 541 b.append(nextNumber); 542 return b.toString(); 543 } 544 545 /** 546 * Create a System Name from hardware address and system letter prefix. 547 * AbstractManager performs no validation. 548 * @param curAddress hardware address, no system prefix or type letter. 549 * @param prefix - just system prefix, not including Type Letter. 550 * @return full system name with system prefix, type letter and hardware address. 551 * @throws JmriException if unable to create a system name. 552 */ 553 public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException { 554 return prefix + typeLetter() + curAddress; 555 } 556 557 /** 558 * checks for numeric-only system names. 559 * @param curAddress the System name ( excluding both prefix and type letter) to check. 560 * @return unchanged if is numeric string. 561 * @throws JmriException if not numeric. 562 */ 563 protected String checkNumeric(@Nonnull String curAddress) throws JmriException { 564 try { 565 Integer.parseInt(curAddress); 566 } catch (java.lang.NumberFormatException ex) { 567 throw new JmriException("Hardware Address passed "+curAddress+" should be a number"); 568 } 569 return curAddress; 570 } 571 572 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractManager.class); 573 574}