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}