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     * Get a recommended text for a tooltip when displaying 
193     * the NamedBean, e.g. in a list or table.
194     *
195     * By default, this is the comment from the NamedBean, on the theory
196     * that the system name and/or user name are being displayed directly. 
197     * Specific system implementations may override that.
198     */
199    @CheckReturnValue
200    @Nonnull
201    default String getRecommendedToolTip() {
202        String retval = getComment();
203        if (retval == null) return "";
204        return retval;
205    }
206     
207    /**
208     * Request a call-back when a bound property changes. Bound properties are
209     * the known state, commanded state, user and system names.
210     *
211     * @param listener    The listener. This may change in the future to be a
212     *                        subclass of NamedProprtyChangeListener that
213     *                        carries the name and listenerRef values internally
214     * @param name        The name (either system or user) that the listener
215     *                        uses for this namedBean, this parameter is used to
216     *                        help determine when which listeners should be
217     *                        moved when the username is moved from one bean to
218     *                        another
219     * @param listenerRef A textual reference for the listener, that can be
220     *                        presented to the user when a delete is called
221     */
222    void addPropertyChangeListener(@Nonnull PropertyChangeListener listener, String name, String listenerRef);
223
224    /**
225     * Request a call-back when a bound property changes. Bound properties are
226     * the known state, commanded state, user and system names.
227     *
228     * @param propertyName The name of the property to listen to
229     * @param listener     The listener. This may change in the future to be a
230     *                         subclass of NamedProprtyChangeListener that
231     *                         carries the name and listenerRef values
232     *                         internally
233     * @param name         The name (either system or user) that the listener
234     *                         uses for this namedBean, this parameter is used
235     *                         to help determine when which listeners should be
236     *                         moved when the username is moved from one bean to
237     *                         another
238     * @param listenerRef  A textual reference for the listener, that can be
239     *                         presented to the user when a delete is called
240     */
241    void addPropertyChangeListener(@Nonnull String propertyName, @Nonnull PropertyChangeListener listener,
242            String name, String listenerRef);
243
244    void updateListenerRef(@Nonnull PropertyChangeListener l, String newName);
245
246    void vetoableChange(@Nonnull PropertyChangeEvent evt) throws PropertyVetoException;
247
248    /**
249     * Get the textual reference for the specific listener
250     *
251     * @param l the listener of interest
252     * @return the textual reference
253     */
254    @CheckReturnValue
255    String getListenerRef(@Nonnull PropertyChangeListener l);
256
257    /**
258     * Returns a list of all the listeners references
259     *
260     * @return a list of textual references
261     */
262    @CheckReturnValue
263    ArrayList<String> getListenerRefs();
264
265    /**
266     * Number of current listeners. May return -1 if the information is not
267     * available for some reason.
268     *
269     * @return the number of listeners.
270     */
271    @CheckReturnValue
272    int getNumPropertyChangeListeners();
273
274    /**
275     * Get a list of all the property change listeners that are registered using
276     * a specific name
277     *
278     * @param name The name (either system or user) that the listener has
279     *                 registered as referencing this namedBean
280     * @return empty list if none
281     */
282    @CheckReturnValue
283    @Nonnull
284    PropertyChangeListener[] getPropertyChangeListenersByReference(@Nonnull String name);
285
286    /**
287     * Deactivate this object, so that it releases as many resources as possible
288     * and no longer effects others.
289     * <p>
290     * For example, if this object has listeners, after a call to this method it
291     * should no longer notify those listeners. Any native or system-wide
292     * resources it maintains should be released, including threads, files, etc.
293     * <p>
294     * It is an error to invoke any other methods on this object once dispose()
295     * has been called. Note, however, that there is no guarantee about behavior
296     * in that case.
297     * <p>
298     * Afterwards, references to this object may still exist elsewhere,
299     * preventing its garbage collection. But it's formally dead, and shouldn't
300     * be keeping any other objects alive. Therefore, this method should null
301     * out any references to other objects that this NamedBean contained.
302     */
303    void dispose(); // remove _all_ connections!
304
305    /**
306     * Provide generic access to internal state.
307     * <p>
308     * This generally shouldn't be used by Java code; use the class-specific
309     * form instead (e.g. setCommandedState in Turnout). This is provided to
310     * make scripts access easier to read.
311     *
312     * @param s the state
313     * @throws JmriException general error when setting the state fails
314     */
315    @InvokeOnLayoutThread
316    public void setState(int s) throws JmriException;
317
318    /**
319     * Provide generic access to internal state.
320     * <p>
321     * This generally shouldn't be used by Java code; use the class-specific
322     * form instead (e.g. getCommandedState in Turnout). This is provided to
323     * make scripts easier to read.
324     *
325     * @return the state
326     */
327    @CheckReturnValue
328    public int getState();
329
330    /**
331     * Provide human-readable, localized version of state value.
332     * <p>
333     * This method is intended for use when presenting to a human operator.
334     *
335     * @param state the state to describe
336     * @return the state in localized form
337     */
338    @CheckReturnValue
339    public String describeState(int state);
340
341    /**
342     * Get associated comment text.
343     *
344     * @return the comment or null
345     */
346    @CheckReturnValue
347    @CheckForNull
348    public String getComment();
349
350    /**
351     * Set associated comment text.
352     * <p>
353     * Comments can be any valid text.
354     *
355     * @param comment the comment or null to remove an existing comment
356     */
357    public void setComment(@CheckForNull String comment);
358
359    /**
360     * Get a list of references for the specified bean.
361     *
362     * @param bean The bean to be checked.
363     * @return a list of NamedBeanUsageReports or an empty ArrayList.
364     */
365    default List<NamedBeanUsageReport> getUsageReport(@CheckForNull NamedBean bean) { return (new ArrayList<>()); }
366
367    /**
368     * Attach a key/value pair to the NamedBean, which can be retrieved later.
369     * These are not bound properties as yet, and don't throw events on
370     * modification. Key must not be null.
371     * <p>
372     * The key is constrained to
373     * String to make these behave like normal Java Beans.
374     *
375     * @param key   the property to set
376     * @param value the value of the property
377     */
378    public void setProperty(@Nonnull String key, Object value);
379
380    /**
381     * Retrieve the value associated with a key. If no value has been set for
382     * that key, returns null.
383     *
384     * @param key the property to get
385     * @return The value of the property or null.
386     */
387    @CheckReturnValue
388    @CheckForNull
389    public Object getProperty(@Nonnull String key);
390
391    /**
392     * Remove the key/value pair against the NamedBean.
393     *
394     * @param key the property to remove
395     */
396    public void removeProperty(@Nonnull String key);
397
398    /**
399     * Retrieve the complete current set of keys.
400     *
401     * @return empty set if none
402     */
403    @CheckReturnValue
404    @Nonnull
405    public java.util.Set<String> getPropertyKeys();
406
407    /**
408     * For instances in the code where we are dealing with just a bean and a
409     * message needs to be passed to the user or in a log.
410     *
411     * @return a string of the bean type, eg Turnout, Sensor etc
412     */
413    @CheckReturnValue
414    @Nonnull
415    public String getBeanType();
416
417    /**
418     * Enforces, and as a user convenience converts to, the standard form for a
419     * user name.
420     * <p>
421     * This implementation just does a trim(), but later versions might e.g. do
422     * more extensive things.
423     *
424     * @param inputName User name to be normalized
425     * @throws BadUserNameException If the inputName can't be converted to
426     *                                  normalized form
427     * @return A user name in standard normalized form or null if inputName was
428     *         null
429     */
430    @CheckReturnValue
431    @CheckForNull
432    static public String normalizeUserName(@CheckForNull String inputName) throws BadUserNameException {
433        String result = inputName;
434        if (result != null) {
435            result = result.trim();
436        }
437        return result;
438    }
439
440    /**
441     * Provide a comparison between the system names of two beans. This provides
442     * a implementation for e.g. {@link java.util.Comparator}. Returns 0 if the
443     * names are the same, -1 if the first argument orders before the second
444     * argument's name, +1 if the first argument's name orders after the second
445     * argument's name. The comparison is alphanumeric on the system prefix,
446     * then alphabetic on the type letter, then system-specific comparison on
447     * the two suffix parts via the {@link #compareSystemNameSuffix} method.
448     *
449     * @param n2 The second NamedBean in the comparison ("this" is the first
450     *               one)
451     * @return -1,0,+1 for ordering if the names are well-formed; may not
452     *         provide proper ordering if the names are not well-formed.
453     */
454    @CheckReturnValue
455    @Override
456    public default int compareTo(NamedBean n2) {
457        Objects.requireNonNull(n2);
458        jmri.util.AlphanumComparator ac = new jmri.util.AlphanumComparator();
459        String o1 = this.getSystemName();
460        String o2 = n2.getSystemName();
461
462        int p1len = Manager.getSystemPrefixLength(o1);
463        int p2len = Manager.getSystemPrefixLength(o2);
464
465        int comp = ac.compare(o1.substring(0, p1len), o2.substring(0, p2len));
466        if (comp != 0)
467            return comp;
468
469        char c1 = o1.charAt(p1len);
470        char c2 = o2.charAt(p2len);
471
472        if (c1 != c2) {
473            return (c1 > c2) ? +1 : -1;
474        } else {
475            return this.compareSystemNameSuffix(o1.substring(p1len + 1), o2.substring(p2len + 1), n2);
476        }
477    }
478
479    /**
480     * Compare the suffix of this NamedBean's name with the suffix of the
481     * argument NamedBean's name for the {@link #compareTo} operation. This is
482     * intended to be a system-specific comparison that understands the various
483     * formats, etc.
484     *
485     * @param suffix1 The suffix for the 1st bean in the comparison
486     * @param suffix2 The suffix for the 2nd bean in the comparison
487     * @param n2      The other (second) NamedBean in the comparison
488     * @return -1,0,+1 for ordering if the names are well-formed; may not
489     *         provide proper ordering if the names are not well-formed.
490     */
491    @CheckReturnValue
492    public int compareSystemNameSuffix(@Nonnull String suffix1, @Nonnull String suffix2, @Nonnull NamedBean n2);
493
494    /**
495     * Parent class for a set of classes that describe if a user name or system
496     * name is a bad name.
497     */
498    public class BadNameException extends IllegalArgumentException {
499
500        private final String localizedMessage;
501
502        /**
503         * Create an exception with no message to the user or for logging.
504         */
505        protected BadNameException() {
506            super();
507            localizedMessage = super.getMessage();
508        }
509
510        /**
511         * Create a localized exception, suitable for display to the user.This
512         * takes the non-localized message followed by the localized message.
513         * <p>
514         * Use {@link #getLocalizedMessage()} to display the message to the
515         * user, and use {@link #getMessage()} to record the message in logs.
516         *
517         * @param logging the English message for logging
518         * @param display the localized message for display
519         */
520        protected BadNameException(String logging, String display) {
521            super(logging);
522            localizedMessage = display;
523        }
524
525        @Override
526        public String getLocalizedMessage() {
527            return localizedMessage;
528        }
529
530    }
531
532    public class BadUserNameException extends BadNameException {
533
534        /**
535         * Create an exception with no message to the user or for logging. Use
536         * only when calling methods likely have alternate mechanism for
537         * allowing user to understand why exception was thrown.
538         */
539        public BadUserNameException() {
540            super();
541        }
542
543        /**
544         * Create a localized exception, suitable for display to the user. This
545         * takes the same arguments as
546         * {@link jmri.Bundle#getMessage(java.util.Locale, java.lang.String, java.lang.Object...)}
547         * as it uses that method to create both the localized and loggable
548         * messages.
549         * <p>
550         * Use {@link #getLocalizedMessage()} to display the message to the
551         * user, and use {@link #getMessage()} to record the message in logs.
552         * <p>
553         * <strong>Note</strong> the message must be accessible by
554         * {@link jmri.Bundle}.
555         *
556         * @param locale  the locale to be used
557         * @param message bundle key to be translated
558         * @param subs    One or more objects to be inserted into the message
559         */
560        public BadUserNameException(Locale locale, String message, Object... subs) {
561            super(Bundle.getMessage(Locale.ENGLISH, message, subs),
562                    Bundle.getMessage(locale, message, subs));
563        }
564
565        /**
566         * Create a localized exception, suitable for display to the user. This
567         * takes the non-localized message followed by the localized message.
568         * <p>
569         * Use {@link #getLocalizedMessage()} to display the message to the
570         * user, and use {@link #getMessage()} to record the message in logs.
571         *
572         * @param logging the English message for logging
573         * @param display the localized message for display
574         */
575        public BadUserNameException(String logging, String display) {
576            super(logging, display);
577        }
578    }
579
580    public class BadSystemNameException extends BadNameException {
581
582        /**
583         * Create an exception with no message to the user or for logging. Use
584         * only when calling methods likely have alternate mechanism for
585         * allowing user to understand why exception was thrown.
586         */
587        public BadSystemNameException() {
588            super();
589        }
590
591        /**
592         * Create a localized exception, suitable for display to the user. This
593         * takes the same arguments as
594         * {@link jmri.Bundle#getMessage(java.util.Locale, java.lang.String, java.lang.Object...)}
595         * as it uses that method to create both the localized and loggable
596         * messages.
597         * <p>
598         * Use {@link #getLocalizedMessage()} to display the message to the
599         * user, and use {@link #getMessage()} to record the message in logs.
600         * <p>
601         * <strong>Note</strong> the message must be accessible by
602         * {@link jmri.Bundle}.
603         *
604         * @param locale  the locale to be used
605         * @param message bundle key to be translated
606         * @param subs    One or more objects to be inserted into the message
607         */
608        public BadSystemNameException(Locale locale, String message, Object... subs) {
609            this(Bundle.getMessage(Locale.ENGLISH, message, subs),
610                    Bundle.getMessage(locale, message, subs));
611        }
612
613        /**
614         * Create a localized exception, suitable for display to the user. This
615         * takes the non-localized message followed by the localized message.
616         * <p>
617         * Use {@link #getLocalizedMessage()} to display the message to the
618         * user, and use {@link #getMessage()} to record the message in logs.
619         *
620         * @param logging the English message for logging
621         * @param display the localized message for display
622         */
623        public BadSystemNameException(String logging, String display) {
624            super(logging, display);
625        }
626    }
627
628    public class DuplicateSystemNameException extends IllegalArgumentException {
629
630        private final String localizedMessage;
631
632        /**
633         * Create an exception with no message to the user or for logging. Use
634         * only when calling methods likely have alternate mechanism for
635         * allowing user to understand why exception was thrown.
636         */
637        public DuplicateSystemNameException() {
638            super();
639            localizedMessage = super.getMessage();
640        }
641
642        /**
643         * Create a exception.
644         *
645         * @param message bundle key to be translated
646         */
647        public DuplicateSystemNameException(String message) {
648            super(message);
649            localizedMessage = super.getMessage();
650        }
651
652        /**
653         * Create a localized exception, suitable for display to the user. This
654         * takes the same arguments as
655         * {@link jmri.Bundle#getMessage(java.util.Locale, java.lang.String, java.lang.Object...)}
656         * as it uses that method to create both the localized and loggable
657         * messages.
658         * <p>
659         * Use {@link #getLocalizedMessage()} to display the message to the
660         * user, and use {@link #getMessage()} to record the message in logs.
661         * <p>
662         * <strong>Note</strong> the message must be accessible by
663         * {@link jmri.Bundle}.
664         *
665         * @param locale  the locale to be used
666         * @param message bundle key to be translated
667         * @param subs    One or more objects to be inserted into the message
668         */
669        public DuplicateSystemNameException(Locale locale, String message, Object... subs) {
670            this(Bundle.getMessage(locale, message, subs),
671                    Bundle.getMessage(locale, message, subs));
672        }
673
674        /**
675         * Create a localized exception, suitable for display to the user. This
676         * takes the non-localized message followed by the localized message.
677         * <p>
678         * Use {@link #getLocalizedMessage()} to display the message to the
679         * user, and use {@link #getMessage()} to record the message in logs.
680         *
681         * @param logging the English message for logging
682         * @param display the localized message for display
683         */
684        public DuplicateSystemNameException(String logging, String display) {
685            super(logging);
686            localizedMessage = display;
687        }
688
689        @Override
690        public String getLocalizedMessage() {
691            return localizedMessage;
692        }
693    }
694
695    /**
696     * Display options for {@link #getDisplayName(DisplayOptions)}. The quoted
697     * forms are intended to be used in sentences and messages, while the
698     * unquoted forms are intended for use in user interface elements like lists
699     * and combo boxes.
700     */
701    public enum DisplayOptions {
702        /**
703         * Display the user name; if the user name is null or empty, display the
704         * system name.
705         */
706        DISPLAYNAME,
707        /**
708         * Display the user name in quotes; if the user name is null or empty,
709         * display the system name in quotes.
710         */
711        QUOTED_DISPLAYNAME,
712        /**
713         * Display the user name; if the user name is null or empty, display the
714         * system name.
715         */
716        USERNAME,
717        /**
718         * Display the user name in quotes; if the user name is null or empty,
719         * display the system name in quotes.
720         */
721        QUOTED_USERNAME,
722        /**
723         * Display the system name. This should be used only when the context
724         * would cause displaying the user name to be more confusing than not or
725         * in text input fields for editing the system name.
726         */
727        SYSTEMNAME,
728        /**
729         * Display the system name in quotes. This should be used only when the
730         * context would cause displaying the user name to be more confusing
731         * than not or in text input fields for editing the system name.
732         */
733        QUOTED_SYSTEMNAME,
734        /**
735         * Display the user name followed by the system name in parenthesis. If
736         * the user name is null or empty, display the system name without
737         * parenthesis.
738         */
739        USERNAME_SYSTEMNAME,
740        /**
741         * Display the user name in quotes followed by the system name in
742         * parenthesis. If the user name is null or empty, display the system
743         * name in quotes.
744         */
745        QUOTED_USERNAME_SYSTEMNAME;
746    }
747
748}