001package jmri.implementation; 002 003import java.beans.*; 004import java.util.ArrayList; 005import java.util.HashMap; 006import java.util.Objects; 007import java.util.Set; 008 009import javax.annotation.CheckReturnValue; 010import javax.annotation.Nonnull; 011import javax.annotation.CheckForNull; 012import javax.annotation.OverridingMethodsMustInvokeSuper; 013 014import jmri.NamedBean; 015import jmri.beans.BeanUtil; 016 017/** 018 * Abstract base for the NamedBean interface. 019 * <p> 020 * Implements the parameter binding support. 021 * 022 * @author Bob Jacobsen Copyright (C) 2001 023 */ 024public abstract class AbstractNamedBean implements NamedBean { 025 026 // force changes through setUserName() to ensure rules are applied 027 // as a side effect require reads through getUserName() 028 private String mUserName; 029 // final so does not need to be private to protect against changes 030 protected final String mSystemName; 031 032 /** 033 * Create a new NamedBean instance using only a system name. 034 * 035 * @param sys the system name for this bean; must not be null and must 036 * be unique within the layout 037 */ 038 protected AbstractNamedBean(@Nonnull String sys) { 039 this(sys, null); 040 } 041 042 /** 043 * Create a new NamedBean instance using both a system name and 044 * (optionally) a user name. 045 * <p> 046 * Refuses construction if unable to use the normalized user name, to prevent 047 * subclass from overriding {@link #setUserName(java.lang.String)} during construction. 048 * 049 * @param sys the system name for this bean; must not be null 050 * @param user the user name for this bean; will be normalized if needed; can be null 051 * @throws jmri.NamedBean.BadUserNameException if the user name cannot be 052 * normalized 053 * @throws jmri.NamedBean.BadSystemNameException if the system name is null 054 */ 055 protected AbstractNamedBean(@Nonnull String sys, @CheckForNull String user) 056 throws NamedBean.BadUserNameException, NamedBean.BadSystemNameException { 057 058 if (Objects.isNull(sys)) { 059 throw new NamedBean.BadSystemNameException(); 060 } 061 mSystemName = sys; 062 // normalize the user name or refuse construction if unable to 063 // use this form, to prevent subclass from overriding {@link #setUserName()} 064 // during construction 065 AbstractNamedBean.this.setUserName(user); 066 } 067 068 /** 069 * {@inheritDoc} 070 */ 071 @Override 072 public final String getComment() { 073 return this.comment; 074 } 075 076 /** 077 * {@inheritDoc} 078 */ 079 @Override 080 public final void setComment(String comment) { 081 String old = this.comment; 082 if (comment == null || comment.trim().isEmpty()) { 083 this.comment = null; 084 } else { 085 this.comment = comment; 086 } 087 firePropertyChange(PROPERTY_COMMENT, old, comment); 088 } 089 private String comment; 090 091 /** 092 * {@inheritDoc} 093 */ 094 @Override 095 @CheckReturnValue 096 @Nonnull 097 public final String getDisplayName() { 098 return NamedBean.super.getDisplayName(); 099 } 100 101 /** 102 * {@inheritDoc} 103 */ 104 @Override 105 @CheckReturnValue 106 @Nonnull 107 public final String getDisplayName(DisplayOptions displayOptions) { 108 return NamedBean.super.getDisplayName(displayOptions); 109 } 110 111 // implementing classes will typically have a function/listener to get 112 // updates from the layout, which will then call 113 // public void firePropertyChange(String propertyName, 114 // Object oldValue, 115 // Object newValue) 116 // _once_ if anything has changed state 117 // since we can't do a "super(this)" in the ctor to inherit from PropertyChangeSupport, we'll 118 // reflect to it 119 private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); 120 protected final HashMap<PropertyChangeListener, String> register = new HashMap<>(); 121 protected final HashMap<PropertyChangeListener, String> listenerRefs = new HashMap<>(); 122 123 /** {@inheritDoc} */ 124 @Override 125 @OverridingMethodsMustInvokeSuper 126 public synchronized void addPropertyChangeListener(@Nonnull PropertyChangeListener l, 127 String beanRef, String listenerRef) { 128 pcs.addPropertyChangeListener(l); 129 if (beanRef != null) { 130 register.put(l, beanRef); 131 } 132 if (listenerRef != null) { 133 listenerRefs.put(l, listenerRef); 134 } 135 } 136 137 /** {@inheritDoc} */ 138 @Override 139 @OverridingMethodsMustInvokeSuper 140 public synchronized void addPropertyChangeListener(@Nonnull String propertyName, 141 @Nonnull PropertyChangeListener l, String beanRef, String listenerRef) { 142 143 pcs.addPropertyChangeListener(propertyName, l); 144 if (beanRef != null) { 145 register.put(l, beanRef); 146 } 147 if (listenerRef != null) { 148 listenerRefs.put(l, listenerRef); 149 } 150 } 151 152 /** {@inheritDoc} */ 153 @Override 154 @OverridingMethodsMustInvokeSuper 155 public synchronized void addPropertyChangeListener(PropertyChangeListener listener) { 156 pcs.addPropertyChangeListener(listener); 157 } 158 159 /** {@inheritDoc} */ 160 @Override 161 @OverridingMethodsMustInvokeSuper 162 public synchronized void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) { 163 pcs.addPropertyChangeListener(propertyName, listener); 164 } 165 166 /** {@inheritDoc} */ 167 @Override 168 @OverridingMethodsMustInvokeSuper 169 public synchronized void removePropertyChangeListener(PropertyChangeListener listener) { 170 pcs.removePropertyChangeListener(listener); 171 if (listener != null && !BeanUtil.contains(pcs.getPropertyChangeListeners(), listener)) { 172 register.remove(listener); 173 listenerRefs.remove(listener); 174 } 175 } 176 177 /** {@inheritDoc} */ 178 @Override 179 @OverridingMethodsMustInvokeSuper 180 public synchronized void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { 181 pcs.removePropertyChangeListener(propertyName, listener); 182 if (listener != null && !BeanUtil.contains(pcs.getPropertyChangeListeners(), listener)) { 183 register.remove(listener); 184 listenerRefs.remove(listener); 185 } 186 } 187 188 /** {@inheritDoc} */ 189 @Override 190 @Nonnull 191 public synchronized PropertyChangeListener[] getPropertyChangeListenersByReference(@Nonnull String name) { 192 ArrayList<PropertyChangeListener> list = new ArrayList<>(); 193 register.entrySet().forEach((entry) -> { 194 PropertyChangeListener l = entry.getKey(); 195 if (entry.getValue().equals(name)) { 196 list.add(l); 197 } 198 }); 199 return list.toArray(new PropertyChangeListener[list.size()]); 200 } 201 202 /** 203 * Get a meaningful list of places where the bean is in use. 204 * 205 * @return ArrayList of the listeners 206 */ 207 @Override 208 public synchronized ArrayList<String> getListenerRefs() { 209 return new ArrayList<>(listenerRefs.values()); 210 } 211 212 /** {@inheritDoc} */ 213 @Override 214 @OverridingMethodsMustInvokeSuper 215 public synchronized void updateListenerRef(PropertyChangeListener l, String newName) { 216 if (listenerRefs.containsKey(l)) { 217 listenerRefs.put(l, newName); 218 } 219 } 220 221 @Override 222 public synchronized String getListenerRef(PropertyChangeListener l) { 223 return listenerRefs.get(l); 224 } 225 226 /** 227 * Get the number of current listeners. 228 * 229 * @return -1 if the information is not available for some reason. 230 */ 231 @Override 232 public synchronized int getNumPropertyChangeListeners() { 233 return pcs.getPropertyChangeListeners().length; 234 } 235 236 /** {@inheritDoc} */ 237 @Override 238 @Nonnull 239 public synchronized PropertyChangeListener[] getPropertyChangeListeners() { 240 return pcs.getPropertyChangeListeners(); 241 } 242 243 /** {@inheritDoc} */ 244 @Override 245 @Nonnull 246 public synchronized PropertyChangeListener[] getPropertyChangeListeners(String propertyName) { 247 return pcs.getPropertyChangeListeners(propertyName); 248 } 249 250 /** {@inheritDoc} */ 251 @Override 252 @Nonnull 253 public final String getSystemName() { 254 return mSystemName; 255 } 256 257 /** {@inheritDoc} 258 */ 259 @Nonnull 260 @Override 261 public final String toString() { 262 /* 263 * Implementation note: This method is final to ensure that the 264 * contract for toString is properly implemented. See the 265 * comment in NamedBean#toString() for more info. 266 * If a subclass wants to add extra info at the end of the 267 * toString output, extend {@link #toStringSuffix}. 268 */ 269 return getSystemName()+toStringSuffix(); 270 } 271 272 /** 273 * Overload this in a sub-class to add extra info to the results of toString() 274 * @return a suffix to add at the end of #toString() result 275 */ 276 protected String toStringSuffix() { 277 return ""; 278 } 279 280 /** {@inheritDoc} */ 281 @Override 282 public final String getUserName() { 283 return mUserName; 284 } 285 286 /** {@inheritDoc} */ 287 @Override 288 @OverridingMethodsMustInvokeSuper 289 public void setUserName(String s) throws NamedBean.BadUserNameException { 290 String old = mUserName; 291 mUserName = NamedBean.normalizeUserName(s); 292 firePropertyChange(PROPERTY_USERNAME, old, mUserName); 293 } 294 295 @OverridingMethodsMustInvokeSuper 296 protected void firePropertyChange(String p, Object old, Object n) { 297 pcs.firePropertyChange(p, old, n); 298 } 299 300 @OverridingMethodsMustInvokeSuper 301 protected void firePropertyChange(PropertyChangeEvent evt) { 302 pcs.firePropertyChange(evt); 303 } 304 305 /** {@inheritDoc} */ 306 @Override 307 @OverridingMethodsMustInvokeSuper 308 public void dispose() { 309 PropertyChangeListener[] listeners = pcs.getPropertyChangeListeners(); 310 for (PropertyChangeListener l : listeners) { 311 pcs.removePropertyChangeListener(l); 312 register.remove(l); 313 listenerRefs.remove(l); 314 } 315 // the name handles of this bean are no longer needed 316 if (jmri.InstanceManager.containsDefault(jmri.NamedBeanHandleManager.class)) { 317 jmri.InstanceManager.getDefault(jmri.NamedBeanHandleManager.class).removeNamedBeanHandles(this); 318 } 319 } 320 321 /** {@inheritDoc} */ 322 @Override 323 @Nonnull 324 public String describeState(int state) { 325 switch (state) { 326 case UNKNOWN: 327 return Bundle.getMessage("BeanStateUnknown"); 328 case INCONSISTENT: 329 return Bundle.getMessage("BeanStateInconsistent"); 330 default: 331 return Bundle.getMessage("BeanStateUnexpected", state); 332 } 333 } 334 335 /** 336 * {@inheritDoc} 337 */ 338 @Override 339 @OverridingMethodsMustInvokeSuper 340 public void setProperty(@Nonnull String key, Object value) { 341 if (parameters == null) { 342 parameters = new HashMap<>(); 343 } 344 Set<String> keySet = getPropertyKeys(); 345 if (keySet.contains(key)) { 346 // key already in the map, replace the value. 347 Object oldValue = getProperty(key); 348 if (!Objects.equals(oldValue, value)) { 349 removeProperty(key); // make sure the old value is removed. 350 parameters.put(key, value); 351 firePropertyChange(key, oldValue, value); 352 } 353 } else { 354 parameters.put(key, value); 355 firePropertyChange(key, null, value); 356 } 357 } 358 359 /** {@inheritDoc} */ 360 @Override 361 @OverridingMethodsMustInvokeSuper 362 public Object getProperty(@Nonnull String key) { 363 if (parameters == null) { 364 parameters = new HashMap<>(); 365 } 366 return parameters.get(key); 367 } 368 369 /** {@inheritDoc} */ 370 @Override 371 @OverridingMethodsMustInvokeSuper 372 @Nonnull 373 public java.util.Set<String> getPropertyKeys() { 374 if (parameters == null) { 375 parameters = new HashMap<>(); 376 } 377 return parameters.keySet(); 378 } 379 380 /** {@inheritDoc} */ 381 @Override 382 @OverridingMethodsMustInvokeSuper 383 public void removeProperty(String key) { 384 if (parameters == null || Objects.isNull(key)) { 385 return; 386 } 387 parameters.remove(key); 388 } 389 390 private HashMap<String, Object> parameters = null; 391 392 /** {@inheritDoc} */ 393 @Override 394 public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException { 395 } 396 397 /** 398 * {@inheritDoc} 399 * <p> 400 * This implementation tests that 401 * {@link jmri.NamedBean#getSystemName()} 402 * is equal for this and obj. 403 * 404 * @param obj the reference object with which to compare. 405 * @return {@code true} if this object is the same as the obj argument; 406 * {@code false} otherwise. 407 */ 408 @Override 409 public boolean equals(Object obj) { 410 if (obj == this) return true; // for efficiency 411 if (obj == null) return false; // by contract 412 413 if (obj instanceof AbstractNamedBean) { // NamedBeans are not equal to things of other types 414 AbstractNamedBean b = (AbstractNamedBean) obj; 415 return this.getSystemName().equals(b.getSystemName()); 416 } 417 return false; 418 } 419 420 /** 421 * {@inheritDoc} 422 * 423 * @return hash code value is based on the system name. 424 */ 425 @Override 426 public int hashCode() { 427 return getSystemName().hashCode(); // as the 428 } 429 430 /** 431 * {@inheritDoc} 432 * 433 * By default, does an alphanumeric-by-chunks comparison. 434 */ 435 @CheckReturnValue 436 @Override 437 public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull NamedBean n) { 438 jmri.util.AlphanumComparator ac = new jmri.util.AlphanumComparator(); 439 return ac.compare(suffix1, suffix2); 440 } 441 442}