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