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 } 316 317 /** {@inheritDoc} */ 318 @Override 319 @Nonnull 320 public String describeState(int state) { 321 switch (state) { 322 case UNKNOWN: 323 return Bundle.getMessage("BeanStateUnknown"); 324 case INCONSISTENT: 325 return Bundle.getMessage("BeanStateInconsistent"); 326 default: 327 return Bundle.getMessage("BeanStateUnexpected", state); 328 } 329 } 330 331 /** 332 * {@inheritDoc} 333 */ 334 @Override 335 @OverridingMethodsMustInvokeSuper 336 public void setProperty(@Nonnull String key, Object value) { 337 if (parameters == null) { 338 parameters = new HashMap<>(); 339 } 340 Set<String> keySet = getPropertyKeys(); 341 if (keySet.contains(key)) { 342 // key already in the map, replace the value. 343 Object oldValue = getProperty(key); 344 if (!Objects.equals(oldValue, value)) { 345 removeProperty(key); // make sure the old value is removed. 346 parameters.put(key, value); 347 firePropertyChange(key, oldValue, value); 348 } 349 } else { 350 parameters.put(key, value); 351 firePropertyChange(key, null, value); 352 } 353 } 354 355 /** {@inheritDoc} */ 356 @Override 357 @OverridingMethodsMustInvokeSuper 358 public Object getProperty(@Nonnull String key) { 359 if (parameters == null) { 360 parameters = new HashMap<>(); 361 } 362 return parameters.get(key); 363 } 364 365 /** {@inheritDoc} */ 366 @Override 367 @OverridingMethodsMustInvokeSuper 368 @Nonnull 369 public java.util.Set<String> getPropertyKeys() { 370 if (parameters == null) { 371 parameters = new HashMap<>(); 372 } 373 return parameters.keySet(); 374 } 375 376 /** {@inheritDoc} */ 377 @Override 378 @OverridingMethodsMustInvokeSuper 379 public void removeProperty(String key) { 380 if (parameters == null || Objects.isNull(key)) { 381 return; 382 } 383 parameters.remove(key); 384 } 385 386 private HashMap<String, Object> parameters = null; 387 388 /** {@inheritDoc} */ 389 @Override 390 public void vetoableChange(java.beans.PropertyChangeEvent evt) throws java.beans.PropertyVetoException { 391 } 392 393 /** 394 * {@inheritDoc} 395 * <p> 396 * This implementation tests that 397 * {@link jmri.NamedBean#getSystemName()} 398 * is equal for this and obj. 399 * 400 * @param obj the reference object with which to compare. 401 * @return {@code true} if this object is the same as the obj argument; 402 * {@code false} otherwise. 403 */ 404 @Override 405 public boolean equals(Object obj) { 406 if (obj == this) return true; // for efficiency 407 if (obj == null) return false; // by contract 408 409 if (obj instanceof AbstractNamedBean) { // NamedBeans are not equal to things of other types 410 AbstractNamedBean b = (AbstractNamedBean) obj; 411 return this.getSystemName().equals(b.getSystemName()); 412 } 413 return false; 414 } 415 416 /** 417 * {@inheritDoc} 418 * 419 * @return hash code value is based on the system name. 420 */ 421 @Override 422 public int hashCode() { 423 return getSystemName().hashCode(); // as the 424 } 425 426 /** 427 * {@inheritDoc} 428 * 429 * By default, does an alphanumeric-by-chunks comparison. 430 */ 431 @CheckReturnValue 432 @Override 433 public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull NamedBean n) { 434 jmri.util.AlphanumComparator ac = new jmri.util.AlphanumComparator(); 435 return ac.compare(suffix1, suffix2); 436 } 437 438}