001package jmri;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.beans.PropertyVetoException;
006import java.util.ArrayList;
007import java.util.List;
008import java.util.Locale;
009import java.util.Objects;
010
011import javax.annotation.CheckForNull;
012import javax.annotation.CheckReturnValue;
013import javax.annotation.Nonnull;
014
015import jmri.beans.PropertyChangeProvider;
016
017/**
018 * Provides common services for classes representing objects on the layout, and
019 * allows a common form of access by their Managers.
020 * <p>
021 * Each object has two types of names:
022 * <p>
023 * The "system" name is provided by the system-specific implementations, and
024 * provides a unique mapping to the layout control system (for example LocoNet
025 * or NCE) and address within that system. It must be present and unique across
026 * the JMRI instance. Two beans are identical if they have the same system name;
027 * if not, not.
028 * <p>
029 * The "user" name is optional. It's free form text except for two restrictions:
030 * <ul>
031 * <li>It can't be the empty string "". (A non-existant user name is coded as a
032 * null)
033 * <li>And eventually, we may insist on normalizing user names to a specific
034 * form, e.g. remove leading and trailing white space; see the
035 * {@link #normalizeUserName(java.lang.String)} method
036 * </ul>
037 * <p>
038 * Each of these two names must be unique for every NamedBean of the same type
039 * on the layout and a single NamedBean cannot have a user name that is the same
040 * as the system name of another NamedBean of the same type. (The complex
041 * wording is saying that a single NamedBean object is allowed to have its
042 * system name and user name be the same, but that's the only non-uniqueness
043 * that's allowed within a specific type). Note that the uniqueness restrictions
044 * are currently not completely enforced, only warned about; a future version of
045 * JMRI will enforce this restriction.
046 * <p>
047 * For more information, see the
048 * <a href="http://jmri.org/help/en/html/doc/Technical/Names.shtml">Names and
049 * Naming</a> page in the
050 * <a href="http://jmri.org/help/en/html/doc/Technical/index.shtml">Technical
051 * Info</a> pages.
052 * <hr>
053 * This file is part of JMRI.
054 * <p>
055 * JMRI is free software; you can redistribute it and/or modify it under the
056 * terms of version 2 of the GNU General Public License as published by the Free
057 * Software Foundation. See the "COPYING" file for a copy of this license.
058 * <p>
059 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
060 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
061 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
062 *
063 * @author Bob Jacobsen Copyright (C) 2001, 2002, 2003, 2004
064 * @see jmri.Manager
065 */
066public interface NamedBean extends Comparable<NamedBean>, PropertyChangeProvider {
067
068    /**
069     * Constant representing an "unknown" state, indicating that the object's
070     * state is not necessarily that of the actual layout hardware. This is the
071     * initial state of a newly created object before communication with the
072     * layout.
073     */
074    static final int UNKNOWN = 0x01;
075
076    /**
077     * Constant representing an "inconsistent" state, indicating that some
078     * inconsistency has been detected in the hardware readback.
079     */
080    static final int INCONSISTENT = 0x08;
081
082    /**
083     * Format used for {@link #getDisplayName(DisplayOptions)} when displaying
084     * the user name and system name without quoation marks around the user
085     * name.
086     */
087    final static String DISPLAY_NAME_FORMAT = "%s (%s)";
088
089    /**
090     * Format used for {@link #getDisplayName(DisplayOptions)} when displaying
091     * the user name and system name with quoation marks around the user name.
092     */
093    final static String QUOTED_NAME_FORMAT = "\"%s\" (%s)";
094
095    /**
096     * Property of changed state.
097     */
098    final static String PROPERTY_STATE = "state";
099
100    /**
101     * User's identification for the item. Bound parameter so manager(s) can
102     * listen to changes. Any given user name must be unique within the layout.
103     * Must not match the system name.
104     *
105     * @return null if not set
106     */
107    @CheckReturnValue
108    @CheckForNull
109    String getUserName();
110
111    /**
112     * Set the user name, normalizing it if needed.
113     *
114     * @param s the new user name
115     * @throws jmri.NamedBean.BadUserNameException if the user name can not be
116     *                                                 normalized
117     */
118    void setUserName(@CheckForNull String s) throws BadUserNameException;
119
120    /**
121     * Get a system-specific name. This encodes the hardware addressing
122     * information. Any given system name must be unique within the layout.
123     *
124     * @return the system-specific name
125     */
126    @CheckReturnValue
127    @Nonnull
128    String getSystemName();
129
130    /**
131     * Display the system-specific name.
132     * <p>Note that this is a firm contract:  toString() in
133     * all implementing classes must return the system name
134     * followed by optional additional information.
135     * Using code can assume that the result of toString() will always be
136     * or start with the system name followed by some kind of separator character.
137     *
138     * @return the system-specific name
139     */
140    @Nonnull
141    @Override
142    String toString();
143
144    /**
145     * Get user name if it exists, otherwise return System name.
146     *
147     * @return the user name or system-specific name
148     */
149    @CheckReturnValue
150    @Nonnull
151    default String getDisplayName() {
152        return getDisplayName(DisplayOptions.DISPLAYNAME);
153    }
154
155    /**
156     * Get the name to display, formatted per {@link NamedBean.DisplayOptions}.
157     *
158     * @param options the DisplayOptions to use
159     * @return the display name formatted per options
160     */
161    @CheckReturnValue
162    @Nonnull
163    default String getDisplayName(DisplayOptions options) {
164        String userName = getUserName();
165        String systemName = getSystemName();
166        // since there are two undisplayable states for the user name,
167        // empty or null, if user name is empty, make it null to avoid
168        // repeatedly checking for both those states later
169        if (userName != null && userName.isEmpty()) {
170            userName = null;
171        }
172        switch (options) {
173            case USERNAME_SYSTEMNAME:
174                return userName != null ? String.format(DISPLAY_NAME_FORMAT, userName, systemName) : systemName;
175            case QUOTED_USERNAME_SYSTEMNAME:
176                return userName != null ? String.format(QUOTED_NAME_FORMAT, userName, systemName) : getDisplayName(DisplayOptions.QUOTED_SYSTEMNAME);
177            case SYSTEMNAME:
178                return systemName;
179            case QUOTED_SYSTEMNAME:
180                return String.format("\"%s\"", systemName);
181            case QUOTED_USERNAME:
182            case QUOTED_DISPLAYNAME:
183                return String.format("\"%s\"", userName != null ? userName : systemName);
184            case USERNAME:
185            case DISPLAYNAME:
186            default:
187                return userName != null ? userName : systemName;
188        }
189    }
190
191    /**
192     * Request a call-back when a bound property changes. Bound properties are
193     * the known state, commanded state, user and system names.
194     *
195     * @param listener    The listener. This may change in the future to be a
196     *                        subclass of NamedProprtyChangeListener that
197     *                        carries the name and listenerRef values internally
198     * @param name        The name (either system or user) that the listener
199     *                        uses for this namedBean, this parameter is used to
200     *                        help determine when which listeners should be
201     *                        moved when the username is moved from one bean to
202     *                        another
203     * @param listenerRef A textual reference for the listener, that can be
204     *                        presented to the user when a delete is called
205     */
206    void addPropertyChangeListener(@Nonnull PropertyChangeListener listener, String name, String listenerRef);
207
208    /**
209     * Request a call-back when a bound property changes. Bound properties are
210     * the known state, commanded state, user and system names.
211     *
212     * @param propertyName The name of the property to listen to
213     * @param listener     The listener. This may change in the future to be a
214     *                         subclass of NamedProprtyChangeListener that
215     *                         carries the name and listenerRef values
216     *                         internally
217     * @param name         The name (either system or user) that the listener
218     *                         uses for this namedBean, this parameter is used
219     *                         to help determine when which listeners should be
220     *                         moved when the username is moved from one bean to
221     *                         another
222     * @param listenerRef  A textual reference for the listener, that can be
223     *                         presented to the user when a delete is called
224     */
225    void addPropertyChangeListener(@Nonnull String propertyName, @Nonnull PropertyChangeListener listener,
226            String name, String listenerRef);
227
228    void updateListenerRef(@Nonnull PropertyChangeListener l, String newName);
229
230    void vetoableChange(@Nonnull PropertyChangeEvent evt) throws PropertyVetoException;
231
232    /**
233     * Get the textual reference for the specific listener
234     *
235     * @param l the listener of interest
236     * @return the textual reference
237     */
238    @CheckReturnValue
239    String getListenerRef(@Nonnull PropertyChangeListener l);
240
241    /**
242     * Returns a list of all the listeners references
243     *
244     * @return a list of textual references
245     */
246    @CheckReturnValue
247    ArrayList<String> getListenerRefs();
248
249    /**
250     * Number of current listeners. May return -1 if the information is not
251     * available for some reason.
252     *
253     * @return the number of listeners.
254     */
255    @CheckReturnValue
256    int getNumPropertyChangeListeners();
257
258    /**
259     * Get a list of all the property change listeners that are registered using
260     * a specific name
261     *
262     * @param name The name (either system or user) that the listener has
263     *                 registered as referencing this namedBean
264     * @return empty list if none
265     */
266    @CheckReturnValue
267    @Nonnull
268    PropertyChangeListener[] getPropertyChangeListenersByReference(@Nonnull String name);
269
270    /**
271     * Deactivate this object, so that it releases as many resources as possible
272     * and no longer effects others.
273     * <p>
274     * For example, if this object has listeners, after a call to this method it
275     * should no longer notify those listeners. Any native or system-wide
276     * resources it maintains should be released, including threads, files, etc.
277     * <p>
278     * It is an error to invoke any other methods on this object once dispose()
279     * has been called. Note, however, that there is no guarantee about behavior
280     * in that case.
281     * <p>
282     * Afterwards, references to this object may still exist elsewhere,
283     * preventing its garbage collection. But it's formally dead, and shouldn't
284     * be keeping any other objects alive. Therefore, this method should null
285     * out any references to other objects that this NamedBean contained.
286     */
287    void dispose(); // remove _all_ connections!
288
289    /**
290     * Provide generic access to internal state.
291     * <p>
292     * This generally shouldn't be used by Java code; use the class-specific
293     * form instead (e.g. setCommandedState in Turnout). This is provided to
294     * make scripts access easier to read.
295     *
296     * @param s the state
297     * @throws JmriException general error when setting the state fails
298     */
299    @InvokeOnLayoutThread
300    public void setState(int s) throws JmriException;
301
302    /**
303     * Provide generic access to internal state.
304     * <p>
305     * This generally shouldn't be used by Java code; use the class-specific
306     * form instead (e.g. getCommandedState in Turnout). This is provided to
307     * make scripts easier to read.
308     *
309     * @return the state
310     */
311    @CheckReturnValue
312    public int getState();
313
314    /**
315     * Provide human-readable, localized version of state value.
316     * <p>
317     * This method is intended for use when presenting to a human operator.
318     *
319     * @param state the state to describe
320     * @return the state in localized form
321     */
322    @CheckReturnValue
323    public String describeState(int state);
324
325    /**
326     * Get associated comment text.
327     *
328     * @return the comment or null
329     */
330    @CheckReturnValue
331    @CheckForNull
332    public String getComment();
333
334    /**
335     * Set associated comment text.
336     * <p>
337     * Comments can be any valid text.
338     *
339     * @param comment the comment or null to remove an existing comment
340     */
341    public void setComment(@CheckForNull String comment);
342
343    /**
344     * Get a list of references for the specified bean.
345     *
346     * @param bean The bean to be checked.
347     * @return a list of NamedBeanUsageReports or an empty ArrayList.
348     */
349    default List<NamedBeanUsageReport> getUsageReport(@CheckForNull NamedBean bean) { return (new ArrayList<>()); }
350
351    /**
352     * Attach a key/value pair to the NamedBean, which can be retrieved later.
353     * These are not bound properties as yet, and don't throw events on
354     * modification. Key must not be null.
355     * <p>
356     * Prior to JMRI 4.3, the key was of Object type. It was constrained to
357     * String to make these more like normal Java Beans.
358     *
359     * @param key   the property to set
360     * @param value the value of the property
361     */
362    public void setProperty(@Nonnull String key, Object value);
363
364    /**
365     * Retrieve the value associated with a key. If no value has been set for
366     * that key, returns null.
367     *
368     * @param key the property to get
369     * @return The value of the property or null.
370     */
371    @CheckReturnValue
372    @CheckForNull
373    public Object getProperty(@Nonnull String key);
374
375    /**
376     * Remove the key/value pair against the NamedBean.
377     *
378     * @param key the property to remove
379     */
380    public void removeProperty(@Nonnull String key);
381
382    /**
383     * Retrieve the complete current set of keys.
384     *
385     * @return empty set if none
386     */
387    @CheckReturnValue
388    @Nonnull
389    public java.util.Set<String> getPropertyKeys();
390
391    /**
392     * For instances in the code where we are dealing with just a bean and a
393     * message needs to be passed to the user or in a log.
394     *
395     * @return a string of the bean type, eg Turnout, Sensor etc
396     */
397    @CheckReturnValue
398    @Nonnull
399    public String getBeanType();
400
401    /**
402     * Enforces, and as a user convenience converts to, the standard form for a
403     * user name.
404     * <p>
405     * This implementation just does a trim(), but later versions might e.g. do
406     * more extensive things.
407     *
408     * @param inputName User name to be normalized
409     * @throws BadUserNameException If the inputName can't be converted to
410     *                                  normalized form
411     * @return A user name in standard normalized form or null if inputName was
412     *         null
413     */
414    @CheckReturnValue
415    @CheckForNull
416    static public String normalizeUserName(@CheckForNull String inputName) throws BadUserNameException {
417        String result = inputName;
418        if (result != null) {
419            result = result.trim();
420        }
421        return result;
422    }
423
424    /**
425     * Provide a comparison between the system names of two beans. This provides
426     * a implementation for e.g. {@link java.util.Comparator}. Returns 0 if the
427     * names are the same, -1 if the first argument orders before the second
428     * argument's name, +1 if the first argument's name orders after the second
429     * argument's name. The comparison is alphanumeric on the system prefix,
430     * then alphabetic on the type letter, then system-specific comparison on
431     * the two suffix parts via the {@link #compareSystemNameSuffix} method.
432     *
433     * @param n2 The second NamedBean in the comparison ("this" is the first
434     *               one)
435     * @return -1,0,+1 for ordering if the names are well-formed; may not
436     *         provide proper ordering if the names are not well-formed.
437     */
438    @CheckReturnValue
439    @Override
440    public default int compareTo(NamedBean n2) {
441        Objects.requireNonNull(n2);
442        jmri.util.AlphanumComparator ac = new jmri.util.AlphanumComparator();
443        String o1 = this.getSystemName();
444        String o2 = n2.getSystemName();
445
446        int p1len = Manager.getSystemPrefixLength(o1);
447        int p2len = Manager.getSystemPrefixLength(o2);
448
449        int comp = ac.compare(o1.substring(0, p1len), o2.substring(0, p2len));
450        if (comp != 0)
451            return comp;
452
453        char c1 = o1.charAt(p1len);
454        char c2 = o2.charAt(p2len);
455
456        if (c1 != c2) {
457            return (c1 > c2) ? +1 : -1;
458        } else {
459            return this.compareSystemNameSuffix(o1.substring(p1len + 1), o2.substring(p2len + 1), n2);
460        }
461    }
462
463    /**
464     * Compare the suffix of this NamedBean's name with the suffix of the
465     * argument NamedBean's name for the {@link #compareTo} operation. This is
466     * intended to be a system-specific comparison that understands the various
467     * formats, etc.
468     *
469     * @param suffix1 The suffix for the 1st bean in the comparison
470     * @param suffix2 The suffix for the 2nd bean in the comparison
471     * @param n2      The other (second) NamedBean in the comparison
472     * @return -1,0,+1 for ordering if the names are well-formed; may not
473     *         provide proper ordering if the names are not well-formed.
474     */
475    @CheckReturnValue
476    public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull NamedBean n2);
477
478    /**
479     * Parent class for a set of classes that describe if a user name or system
480     * name is a bad name.
481     */
482    public class BadNameException extends IllegalArgumentException {
483
484        private final String localizedMessage;
485
486        /**
487         * Create an exception with no message to the user or for logging.
488         */
489        protected BadNameException() {
490            super();
491            localizedMessage = super.getMessage();
492        }
493
494        /**
495         * Create a localized exception, suitable for display to the user.This
496         * takes the non-localized message followed by the localized message.
497         * <p>
498         * Use {@link #getLocalizedMessage()} to display the message to the
499         * user, and use {@link #getMessage()} to record the message in logs.
500         *
501         * @param logging the English message for logging
502         * @param display the localized message for display
503         */
504        protected BadNameException(String logging, String display) {
505            super(logging);
506            localizedMessage = display;
507        }
508
509        @Override
510        public String getLocalizedMessage() {
511            return localizedMessage;
512        }
513
514    }
515
516    public class BadUserNameException extends BadNameException {
517
518        /**
519         * Create an exception with no message to the user or for logging. Use
520         * only when calling methods likely have alternate mechanism for
521         * allowing user to understand why exception was thrown.
522         */
523        public BadUserNameException() {
524            super();
525        }
526
527        /**
528         * Create a localized exception, suitable for display to the user. This
529         * takes the same arguments as
530         * {@link jmri.Bundle#getMessage(java.util.Locale, java.lang.String, java.lang.Object...)}
531         * as it uses that method to create both the localized and loggable
532         * messages.
533         * <p>
534         * Use {@link #getLocalizedMessage()} to display the message to the
535         * user, and use {@link #getMessage()} to record the message in logs.
536         * <p>
537         * <strong>Note</strong> the message must be accessible by
538         * {@link jmri.Bundle}.
539         *
540         * @param locale  the locale to be used
541         * @param message bundle key to be translated
542         * @param subs    One or more objects to be inserted into the message
543         */
544        public BadUserNameException(Locale locale, String message, Object... subs) {
545            super(Bundle.getMessage(Locale.ENGLISH, message, subs),
546                    Bundle.getMessage(locale, message, subs));
547        }
548
549        /**
550         * Create a localized exception, suitable for display to the user. This
551         * takes the non-localized message followed by the localized message.
552         * <p>
553         * Use {@link #getLocalizedMessage()} to display the message to the
554         * user, and use {@link #getMessage()} to record the message in logs.
555         *
556         * @param logging the English message for logging
557         * @param display the localized message for display
558         */
559        public BadUserNameException(String logging, String display) {
560            super(logging, display);
561        }
562    }
563
564    public class BadSystemNameException extends BadNameException {
565
566        /**
567         * Create an exception with no message to the user or for logging. Use
568         * only when calling methods likely have alternate mechanism for
569         * allowing user to understand why exception was thrown.
570         */
571        public BadSystemNameException() {
572            super();
573        }
574
575        /**
576         * Create a localized exception, suitable for display to the user. This
577         * takes the same arguments as
578         * {@link jmri.Bundle#getMessage(java.util.Locale, java.lang.String, java.lang.Object...)}
579         * as it uses that method to create both the localized and loggable
580         * messages.
581         * <p>
582         * Use {@link #getLocalizedMessage()} to display the message to the
583         * user, and use {@link #getMessage()} to record the message in logs.
584         * <p>
585         * <strong>Note</strong> the message must be accessible by
586         * {@link jmri.Bundle}.
587         *
588         * @param locale  the locale to be used
589         * @param message bundle key to be translated
590         * @param subs    One or more objects to be inserted into the message
591         */
592        public BadSystemNameException(Locale locale, String message, Object... subs) {
593            this(Bundle.getMessage(Locale.ENGLISH, message, subs),
594                    Bundle.getMessage(locale, message, subs));
595        }
596
597        /**
598         * Create a localized exception, suitable for display to the user. This
599         * takes the non-localized message followed by the localized message.
600         * <p>
601         * Use {@link #getLocalizedMessage()} to display the message to the
602         * user, and use {@link #getMessage()} to record the message in logs.
603         *
604         * @param logging the English message for logging
605         * @param display the localized message for display
606         */
607        public BadSystemNameException(String logging, String display) {
608            super(logging, display);
609        }
610    }
611
612    public class DuplicateSystemNameException extends IllegalArgumentException {
613
614        private final String localizedMessage;
615
616        /**
617         * Create an exception with no message to the user or for logging. Use
618         * only when calling methods likely have alternate mechanism for
619         * allowing user to understand why exception was thrown.
620         */
621        public DuplicateSystemNameException() {
622            super();
623            localizedMessage = super.getMessage();
624        }
625
626        /**
627         * Create a exception.
628         *
629         * @param message bundle key to be translated
630         */
631        public DuplicateSystemNameException(String message) {
632            super(message);
633            localizedMessage = super.getMessage();
634        }
635
636        /**
637         * Create a localized exception, suitable for display to the user. This
638         * takes the same arguments as
639         * {@link jmri.Bundle#getMessage(java.util.Locale, java.lang.String, java.lang.Object...)}
640         * as it uses that method to create both the localized and loggable
641         * messages.
642         * <p>
643         * Use {@link #getLocalizedMessage()} to display the message to the
644         * user, and use {@link #getMessage()} to record the message in logs.
645         * <p>
646         * <strong>Note</strong> the message must be accessible by
647         * {@link jmri.Bundle}.
648         *
649         * @param locale  the locale to be used
650         * @param message bundle key to be translated
651         * @param subs    One or more objects to be inserted into the message
652         */
653        public DuplicateSystemNameException(Locale locale, String message, Object... subs) {
654            this(Bundle.getMessage(locale, message, subs),
655                    Bundle.getMessage(locale, message, subs));
656        }
657
658        /**
659         * Create a localized exception, suitable for display to the user. This
660         * takes the non-localized message followed by the localized message.
661         * <p>
662         * Use {@link #getLocalizedMessage()} to display the message to the
663         * user, and use {@link #getMessage()} to record the message in logs.
664         *
665         * @param logging the English message for logging
666         * @param display the localized message for display
667         */
668        public DuplicateSystemNameException(String logging, String display) {
669            super(logging);
670            localizedMessage = display;
671        }
672
673        @Override
674        public String getLocalizedMessage() {
675            return localizedMessage;
676        }
677    }
678
679    /**
680     * Display options for {@link #getDisplayName(DisplayOptions)}. The quoted
681     * forms are intended to be used in sentences and messages, while the
682     * unquoted forms are intended for use in user interface elements like lists
683     * and combo boxes.
684     */
685    public enum DisplayOptions {
686        /**
687         * Display the user name; if the user name is null or empty, display the
688         * system name.
689         */
690        DISPLAYNAME,
691        /**
692         * Display the user name in quotes; if the user name is null or empty,
693         * display the system name in quotes.
694         */
695        QUOTED_DISPLAYNAME,
696        /**
697         * Display the user name; if the user name is null or empty, display the
698         * system name.
699         */
700        USERNAME,
701        /**
702         * Display the user name in quotes; if the user name is null or empty,
703         * display the system name in quotes.
704         */
705        QUOTED_USERNAME,
706        /**
707         * Display the system name. This should be used only when the context
708         * would cause displaying the user name to be more confusing than not or
709         * in text input fields for editing the system name.
710         */
711        SYSTEMNAME,
712        /**
713         * Display the system name in quotes. This should be used only when the
714         * context would cause displaying the user name to be more confusing
715         * than not or in text input fields for editing the system name.
716         */
717        QUOTED_SYSTEMNAME,
718        /**
719         * Display the user name followed by the system name in parenthesis. If
720         * the user name is null or empty, display the system name without
721         * parenthesis.
722         */
723        USERNAME_SYSTEMNAME,
724        /**
725         * Display the user name in quotes followed by the system name in
726         * parenthesis. If the user name is null or empty, display the system
727         * name in quotes.
728         */
729        QUOTED_USERNAME_SYSTEMNAME;
730    }
731
732}