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}