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