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}