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