001package jmri;
002
003
004import edu.umd.cs.findbugs.annotations.OverrideMustInvoke;
005
006import java.beans.*;
007import java.util.*;
008
009import javax.annotation.CheckForNull;
010import javax.annotation.CheckReturnValue;
011import javax.annotation.Nonnull;
012
013import jmri.NamedBean.BadSystemNameException;
014import jmri.NamedBean.DuplicateSystemNameException;
015import jmri.beans.SilenceablePropertyChangeProvider;
016import jmri.beans.VetoableChangeProvider;
017
018/**
019 * Basic interface for access to named, managed objects.
020 * <p>
021 * {@link NamedBean} objects represent various real elements, and have a "system
022 * name" and perhaps "user name". A specific Manager object provides access to
023 * them by name, and serves as a factory for new objects.
024 * <p>
025 * Right now, this interface just contains the members needed by
026 * {@link InstanceManager} to handle managers for more than one system.
027 * <p>
028 * Although they are not defined here because their return type differs, any
029 * specific Manager subclass provides "get" methods to locate specific objects,
030 * and a "new" method to create a new one via the Factory pattern. The "get"
031 * methods will return an existing object or null, and will never create a new
032 * object. The "new" method will log a warning if an object already exists with
033 * that system name.
034 * <p>
035 * add/remove PropertyChangeListener methods are provided. At a minimum,
036 * subclasses must notify of changes to the list of available NamedBeans; they
037 * may have other properties that will also notify.
038 * <p>
039 * Probably should have been called NamedBeanManager
040 * <hr>
041 * This file is part of JMRI.
042 * <p>
043 * JMRI is free software; you can redistribute it and/or modify it under the
044 * terms of version 2 of the GNU General Public License as published by the Free
045 * Software Foundation. See the "COPYING" file for a copy of this license.
046 * <p>
047 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
048 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
049 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
050 *
051 * @param <E> the type of NamedBean supported by this manager
052 * @author Bob Jacobsen Copyright (C) 2003
053 */
054public interface Manager<E extends NamedBean> extends SilenceablePropertyChangeProvider, VetoableChangeProvider {
055
056    /**
057     * String constant to represent if a Bean can be deleted.
058     */
059    String PROPERTY_CAN_DELETE = "CanDelete";
060
061    /**
062     * String constant to tell the Manager to actually delete the Bean.
063     */
064    String PROPERTY_DO_DELETE = "DoDelete";
065
066    /**
067     * String constant to represent if a Bean should NOT be deleted.
068     */
069    String PROPERTY_DO_NOT_DELETE = "DoNotDelete";
070
071    /**
072     * String constant for changes to the number of managed Beans,
073     * normally silenced during panel load.
074     */
075    String PROPERTY_BEANS = "beans";
076
077    /**
078     * String constant for number of managed Beans
079     */
080    String PROPERTY_LENGTH = "length";
081
082    /**
083     * String constant for DisplayListName.
084     */
085    String PROPERTY_DISPLAY_LIST_NAME = "DisplayListName";
086
087    /**
088     * Get the system connection for this manager.
089     *
090     * @return the system connection for this manager
091     */
092    @CheckReturnValue
093    @Nonnull
094    SystemConnectionMemo getMemo();
095
096    /**
097     * Provide access to the system prefix string. This was previously called
098     * the "System letter"
099     *
100     * @return the system prefix
101     */
102    @CheckReturnValue
103    @Nonnull
104    String getSystemPrefix();
105
106    /**
107     * @return The type letter for a specific implementation
108     */
109    @CheckReturnValue
110    char typeLetter();
111
112    /**
113     * Get the class of NamedBean supported by this Manager. This should be the
114     * generic class used in the Manager's class declaration.
115     *
116     * @return the class supported by this Manager.
117     */
118    abstract Class<E> getNamedBeanClass();
119
120    /**
121     * Get the prefix and type for the system name of the NamedBeans handled by
122     * this manager.
123     *
124     * @return the prefix generated by concatenating the result of
125     * {@link #getSystemPrefix() } and {@link #typeLetter() }
126     */
127    default String getSystemNamePrefix() {
128        return getSystemPrefix() + typeLetter();
129    }
130
131    /**
132     * Get the sub system prefix of this manager.
133     * The sub system prefix is the system name prefix and possibly some extra
134     * characters of the NamedBeans handled by this manager.
135     * <P>
136     * For most managers, this is the same as {@link #getSystemNamePrefix() },
137     * but for some like the managers in LogixNG, it differs.
138     *
139     * @return the sub system prefix
140     */
141    default String getSubSystemNamePrefix() {
142        return getSystemNamePrefix();
143    }
144
145    /**
146     * Create a SystemName by prepending the system name prefix to the name if
147     * not already present.
148     * <p>
149     * <strong>Note:</strong> implementations <em>must</em> call
150     * {@link #validateSystemNameFormat(java.lang.String, java.util.Locale)} to
151     * ensure the returned name is valid.
152     *
153     * @param name the item to make the system name for
154     * @return A system name from a user input, typically a number.
155     * @throws BadSystemNameException if a valid name can't be created
156     */
157    @Nonnull
158    default String makeSystemName(@Nonnull String name) throws BadSystemNameException {
159        return makeSystemName(name, true);
160    }
161
162    /**
163     * Create a SystemName by prepending the system name prefix to the name if
164     * not already present.
165     * <p>
166     * The {@code logErrors} parameter is present to allow user interface input
167     * validation to use this method without logging system name validation
168     * errors as the user types.
169     * <p>
170     * <strong>Note:</strong> implementations <em>must</em> call
171     * {@link #validateSystemNameFormat(java.lang.String, java.util.Locale)} to ensure
172     * the returned name is valid.
173     *
174     * @param name      the item to make the system name for
175     * @param logErrors true to log errors; false to not log errors
176     * @return a valid system name
177     * @throws BadSystemNameException if a valid name can't be created
178     */
179    @Nonnull
180    default String makeSystemName(@Nonnull String name, boolean logErrors) throws BadSystemNameException {
181        return makeSystemName(name, logErrors, Locale.getDefault());
182    }
183
184    /**
185     * Create a SystemName by prepending the system name prefix to the name if
186     * not already present.
187     * <p>
188     * The {@code logErrors} parameter is present to allow user interface input
189     * validation to use this method without logging system name validation
190     * errors as the user types.
191     * <p>
192     * <strong>Note:</strong> implementations <em>must</em> call
193     * {@link #validateSystemNameFormat(java.lang.String, java.util.Locale)} to ensure
194     * the returned name is valid.
195     *
196     * @param name      the item to make the system name for
197     * @param logErrors true to log errors; false to not log errors
198     * @param locale    the locale for a localized exception; this is needed for
199     *                  the JMRI web server, which supports multiple locales
200     * @return a valid system name
201     * @throws BadSystemNameException if a valid name can't be created
202     */
203    @Nonnull
204    default String makeSystemName(@Nonnull String name, boolean logErrors, Locale locale) throws BadSystemNameException {
205        String prefix = getSystemNamePrefix();
206        // the one special case that is not caught by validation here
207        if (name.trim().isEmpty()) { // In Java 9+ use name.isBlank() instead
208            throw new NamedBean.BadSystemNameException(Locale.getDefault(), "InvalidSystemNameInvalidPrefix", prefix);
209        }
210        return validateSystemNameFormat(name.startsWith(prefix) ? name : prefix + name, locale);
211    }
212
213    /**
214     * Validate the format of a system name, returning it unchanged if valid.
215     * <p>
216     * This is a convenience form of {@link #validateSystemNameFormat(java.lang.String, java.util.Locale)}.
217     * <p>
218     * This method should not be overridden;
219     * {@link #validateSystemNameFormat(java.lang.String, java.util.Locale)}
220     * should be overridden instead.
221     *
222     * @param name the system name, including system prefix and Type Letter to validate
223     * @return the system name unchanged from its input so that this method can
224     *         be chained or used as an parameter to another method
225     * @throws BadSystemNameException if the name is not valid with error
226     *                                messages in the default locale
227     */
228    @Nonnull
229    default String validateSystemNameFormat(@Nonnull String name) throws BadSystemNameException {
230        return Manager.this.validateSystemNameFormat(name, Locale.getDefault());
231    }
232
233    /**
234     * Validate the format of name, returning it unchanged if valid.
235     * <p>
236     * Although further restrictions may be added by system-specific
237     * implementations, at a minimum, the implementation must consider a name
238     * that does not start with the System Name prefix for this manager to be
239     * invalid, and must consider a name that is the same as the System Name
240     * prefix to be invalid.
241     * <p>
242     * Overriding implementations may rely on
243     * {@link #validSystemNameFormat(java.lang.String)}, however they must
244     * provide an actionable message in the thrown exception if that method does
245     * not return {@link NameValidity#VALID}. When overriding implementations
246     * of this method rely on validSystemNameFormat(), implementations of
247     * that method <em>must not</em> throw an exception, log an error, or
248     * otherwise disrupt the user.
249     *
250     * @param name   the system name to validate
251     * @param locale the locale for a localized exception; this is needed for
252     *               the JMRI web server, which supports multiple locales
253     * @return the unchanged value of the name parameter
254     * @throws BadSystemNameException if provided name is an invalid format
255     */
256    @Nonnull
257    default String validateSystemNameFormat(@Nonnull String name, @Nonnull Locale locale) throws BadSystemNameException {
258        return validateSystemNamePrefix(name, locale);
259    }
260
261    /**
262     * Basic validation that the system name prefix is correct. Used within the
263     * default implementation of
264     * {@link #validateSystemNameFormat(java.lang.String, java.util.Locale)} and
265     * abstracted out of that method so this can be used by validation
266     * implementations in {@link jmri.SystemConnectionMemo}s to avoid
267     * duplicating code in all managers relying on a single subclass of
268     * SystemConnectionMemo.
269     *
270     * @param name   the system name to validate
271     * @param locale the locale for a localized exception; this is needed for
272     *               the JMRI web server, which supports multiple locales
273     * @return the unchanged value of the name parameter
274     * @throws BadSystemNameException if provided name is an invalid format
275     */
276    @Nonnull
277    default String validateSystemNamePrefix(@Nonnull String name, @Nonnull Locale locale) throws BadSystemNameException {
278        String prefix = getSystemNamePrefix();
279        if (name.equals(prefix)) {
280            throw new NamedBean.BadSystemNameException(locale, "InvalidSystemNameMatchesPrefix", name);
281        }
282        if (!name.startsWith(prefix)) {
283            throw new NamedBean.BadSystemNameException(locale, "InvalidSystemNameInvalidPrefix", prefix);
284        }
285        return name;
286    }
287
288    /**
289     * Convenience implementation of
290     * {@link #validateSystemNameFormat(java.lang.String, java.util.Locale)}
291     * that verifies name has no trailing white space and no white space between
292     * the prefix and suffix.
293     * <p>
294     * <strong>Note</strong> this <em>must</em> only be used if the connection
295     * type is externally documented to require these restrictions.
296     *
297     * @param name   the system name to validate
298     * @param locale the locale for a localized exception; this is needed for
299     *               the JMRI web server, which supports multiple locales
300     * @return the unchanged value of the name parameter
301     * @throws BadSystemNameException if provided name is an invalid format
302     */
303    @Nonnull
304    default String validateTrimmedSystemNameFormat(@Nonnull String name, @Nonnull Locale locale) throws BadSystemNameException {
305        name = validateSystemNamePrefix(name, locale);
306        String prefix = getSystemNamePrefix();
307        String suffix = name.substring(prefix.length());
308        if (!suffix.equals(suffix.trim())) {
309            throw new NamedBean.BadSystemNameException(locale, "InvalidSystemNameTrailingWhitespace", name, prefix);
310        }
311        return name;
312    }
313
314    /**
315     * Convenience implementation of
316     * {@link #validateSystemNameFormat(java.lang.String, java.util.Locale)}
317     * that verifies name has has at least 1 number in the String.
318     *
319     * @param name   the system name to validate
320     * @param locale the locale for a localized exception; this is needed for
321     *               the JMRI web server, which supports multiple locales
322     * @return the unchanged value of the name parameter
323     * @throws BadSystemNameException if provided name is an invalid format
324     */
325    @Nonnull
326    default String validateTrimmedMin1NumberSystemNameFormat(@Nonnull String name, @Nonnull Locale locale) throws BadSystemNameException {
327        name = validateTrimmedSystemNameFormat(name, locale);
328        if (!name.matches(".*\\d+.*")) {
329            throw new jmri.NamedBean.BadSystemNameException(locale, "InvalidSystemNameMin1Number",name);
330        }
331        return name;
332    }
333
334    /**
335     * Convenience implementation of
336     * {@link #validateSystemNameFormat(java.lang.String, java.util.Locale)}
337     * that verifies name String is purely numeric.
338     *
339     * @param name   the system name to validate
340     * @param locale the locale for a localized exception; this is needed for
341     *               the JMRI web server, which supports multiple locales
342     * @return the unchanged value of the name parameter
343     * @throws BadSystemNameException if provided name is an invalid format
344     */
345    default String validateSystemNameFormatOnlyNumeric(@Nonnull String name, @Nonnull Locale locale) {
346        name = validateTrimmedSystemNameFormat(name, locale);
347        try {
348            Integer.parseInt(name.substring(getSystemNamePrefix().length()));
349        }
350        catch (NumberFormatException ex) {
351            throw new jmri.NamedBean.BadSystemNameException(locale, "InvalidSystemNameNotInteger",name,getSystemNamePrefix());
352        }
353        return name;
354    }
355
356    /**
357     * Convenience implementation of
358     * {@link #validateSystemNameFormat(java.lang.String, java.util.Locale)}
359     * that verifies name has no invalid characters in the string.
360     * <p>
361     * Also checks validateSystemNamePrefix(name,locale);
362     *
363     * @param name   the system name to validate
364     * @param locale the locale for a localized exception; this is needed for
365     *               the JMRI web server, which supports multiple locales
366     * @param invalidChars array of invalid characters which cannot be in the system name.
367     * @return the unchanged value of the name parameter
368     * @throws BadSystemNameException if provided name is an invalid format
369     */
370    @Nonnull
371    default String validateBadCharsInSystemNameFormat(@Nonnull String name, @Nonnull Locale locale, @Nonnull String[] invalidChars) throws BadSystemNameException {
372        name = validateSystemNamePrefix(name, locale);
373        for (String s : invalidChars) {
374            if (name.contains(s)) {
375                throw new jmri.NamedBean.BadSystemNameException(locale, "InvalidSystemNameCharacter",name,s);
376            }
377        }
378        return name;
379    }
380
381    /**
382     * Convenience implementation of
383     * {@link #validateSystemNameFormat(java.lang.String, java.util.Locale)}
384     * that verifies name is upper case and has no trailing white space and not
385     * white space between the prefix and suffix.
386     * <p>
387     * <strong>Note</strong> this <em>must</em> only be used if the connection
388     * type is externally documented to require these restrictions.
389     *
390     * @param name   the system name to validate
391     * @param locale the locale for a localized exception; this is needed for
392     *               the JMRI web server, which supports multiple locales
393     * @return the unchanged value of the name parameter
394     * @throws BadSystemNameException if provided name is an invalid format
395     */
396    @Nonnull
397    default String validateUppercaseTrimmedSystemNameFormat(@Nonnull String name, @Nonnull Locale locale) throws BadSystemNameException {
398        name = validateTrimmedSystemNameFormat(name, locale);
399        String prefix = getSystemNamePrefix();
400        String suffix = name.substring(prefix.length());
401        String upper = suffix.toUpperCase();
402        if (!suffix.equals(upper)) {
403            throw new NamedBean.BadSystemNameException(locale, "InvalidSystemNameNotUpperCase", name, prefix);
404        }
405        return name;
406    }
407
408    /**
409     * Convenience implementation of
410     * {@link #validateSystemNameFormat(java.lang.String, java.util.Locale)}
411     * that verifies name is an integer after the prefix.
412     * <p>
413     * <strong>Note</strong> this <em>must</em> only be used if the connection
414     * type is externally documented to require these restrictions.
415     *
416     * @param name   the system name to validate
417     * @param min    the minimum valid integer value
418     * @param max    the maximum valid integer value
419     * @param locale the locale for a localized exception; this is needed for
420     *               the JMRI web server, which supports multiple locales
421     * @return the unchanged value of the name parameter
422     * @throws BadSystemNameException if provided name is an invalid format
423     */
424    @Nonnull
425    default String validateIntegerSystemNameFormat(@Nonnull String name, int min, int max, @Nonnull Locale locale) throws BadSystemNameException {
426        name = validateTrimmedSystemNameFormat(name, locale);
427        String prefix = getSystemNamePrefix();
428        String suffix = name.substring(prefix.length());
429        try {
430            int number = Integer.parseInt(suffix);
431            if (number < min) {
432                throw new BadSystemNameException(locale, "InvalidSystemNameIntegerLessThan", name, min);
433            } else if (number > max) {
434                throw new BadSystemNameException(locale, "InvalidSystemNameIntegerGreaterThan", name, max);
435            }
436        } catch (NumberFormatException ex) {
437            throw new BadSystemNameException(locale, "InvalidSystemNameNotInteger", name, prefix);
438        }
439        return name;
440    }
441
442    /**
443     * Convenience implementation of
444     * {@link #validateSystemNameFormat(java.lang.String, java.util.Locale)}
445     * that verifies name is a valid NMRA Accessory address after the prefix. A
446     * name is considered a valid NMRA accessory address if it is an integer
447     * between {@value NmraPacket#accIdLowLimit} and
448     * {@value NmraPacket#accIdHighLimit}, inclusive.
449     * <p>
450     * <strong>Note</strong> this <em>must</em> only be used if the connection
451     * type is externally documented to require these restrictions.
452     *
453     * @param name   the system name to validate
454     * @param locale the locale for a localized exception; this is needed for
455     *               the JMRI web server, which supports multiple locales
456     * @return the unchanged value of the name parameter
457     * @throws BadSystemNameException if provided name is an invalid format
458     */
459    @Nonnull
460    default String validateNmraAccessorySystemNameFormat(@Nonnull String name, @Nonnull Locale locale) throws BadSystemNameException {
461        return this.validateIntegerSystemNameFormat(name, NmraPacket.accIdLowLimit, NmraPacket.accIdHighLimit, locale);
462    }
463
464    /**
465     * Code the validity (including just as a prefix) of a proposed name string.
466     *
467     * @since 4.9.5
468     */
469    enum NameValidity {
470        /**
471         * Indicates the name is valid as is, and can also be a valid prefix for
472         * longer names
473         */
474        VALID,
475        /**
476         * Indicates name is not valid as-is, nor can it be made valid by adding
477         * more characters; just a bad name.
478         */
479        INVALID,
480        /**
481         * Indicates that adding additional characters might (or might not) turn
482         * this into a valid name; it is not a valid name now.
483         */
484        VALID_AS_PREFIX_ONLY
485    }
486
487    /**
488     * Test if parameter is a properly formatted system name. Implementations of
489     * this method <em>must not</em> throw an exception, log an error, or
490     * otherwise disrupt the user.
491     *
492     * @since 4.9.5, although similar methods existed previously in lower-level
493     * classes
494     * @param systemName the system name
495     * @return enum indicating current validity, which might be just as a prefix
496     */
497    @CheckReturnValue
498    @OverrideMustInvoke
499    default NameValidity validSystemNameFormat(@Nonnull String systemName) {
500        String prefix = getSystemNamePrefix();
501        if (prefix.equals(systemName)) {
502            return NameValidity.VALID_AS_PREFIX_ONLY;
503        }
504        return systemName.startsWith(prefix)
505                ? NameValidity.VALID
506                : NameValidity.INVALID;
507    }
508
509    /**
510     * Test if a given name is in a valid format for this Manager.
511     *
512     * @param systemName the name to check
513     * @return {@code true} if {@link #validSystemNameFormat(java.lang.String)}
514     *         equals {@link NameValidity#VALID}; {@code false} otherwise
515     */
516    default boolean isValidSystemNameFormat(@Nonnull String systemName) {
517        return validSystemNameFormat(systemName) == NameValidity.VALID;
518    }
519
520    /**
521     * Free resources when no longer used. Specifically, remove all references
522     * to and from this object, so it can be garbage-collected.
523     */
524    void dispose();
525
526    /**
527     * Get the count of managed objects.
528     *
529     * @return the number of managed objects
530     */
531    @CheckReturnValue
532    int getObjectCount();
533
534    /**
535     * Provide an
536     * {@linkplain java.util.Collections#unmodifiableSet unmodifiable} SortedSet
537     * of NamedBeans in system-name order.
538     * <p>
539     * Note: This is the fastest of the accessors, and is the only long-term
540     * form.
541     * <p>
542     * Note: This is a live set; the contents are kept up to date
543     *
544     * @return Unmodifiable access to a SortedSet of NamedBeans
545     */
546    @CheckReturnValue
547    @Nonnull
548    SortedSet<E> getNamedBeanSet();
549
550    /**
551     * Locate an existing instance based on a system name.
552     *
553     * @param systemName System Name of the required NamedBean
554     * @return requested NamedBean object or null if none exists
555     * @throws IllegalArgumentException if provided name is invalid
556     */
557    @CheckReturnValue
558    @CheckForNull
559    E getBySystemName(@Nonnull String systemName);
560
561    /**
562     * Locate an existing instance based on a user name.
563     *
564     * @param userName System Name of the required NamedBean
565     * @return requested NamedBean object or null if none exists
566     */
567    @CheckReturnValue
568    @CheckForNull
569    E getByUserName(@Nonnull String userName);
570
571    /**
572     * Locate an existing instance based on a name.
573     *
574     * @param name User Name or System Name of the required NamedBean
575     * @return requested NamedBean object or null if none exists
576     */
577    @CheckReturnValue
578    @CheckForNull
579    E getNamedBean(@Nonnull String name);
580
581    /**
582     * Return the descriptors for the system-specific properties of the
583     * NamedBeans that are kept in this manager.
584     *
585     * @return list of known properties, or empty list if there are none
586     */
587    @Nonnull
588    default List<NamedBeanPropertyDescriptor<?>> getKnownBeanProperties() {
589        return new LinkedList<>();
590    }
591
592    /**
593     * Method for a UI to delete a bean.
594     * <p>
595     * The UI should first request a "CanDelete", this will return a list of
596     * locations (and descriptions) where the bean is in use via throwing a
597     * VetoException, then if that comes back clear, or the user agrees with the
598     * actions, then a "DoDelete" can be called which inform the listeners to
599     * delete the bean, then it will be deregistered and disposed of.
600     * <p>
601     * If a property name of "DoNotDelete" is thrown back in the VetoException
602     * then the delete process should be aborted.
603     *
604     * @param n        The NamedBean to be deleted
605     * @param property The programmatic name of the request. "CanDelete" will
606     *                 enquire with all listeners if the item can be deleted.
607     *                 "DoDelete" tells the listener to delete the item
608     * @throws java.beans.PropertyVetoException If the recipients wishes the
609     *                                          delete to be aborted (see above)
610     */
611    void deleteBean(@Nonnull E n, @Nonnull String property) throws PropertyVetoException;
612
613    /**
614     * Remember a NamedBean Object created outside the manager.
615     * <p>
616     * The non-system-specific SignalHeadManagers use this method extensively.
617     *
618     * @param n the bean
619     * @throws DuplicateSystemNameException if a different bean with the same
620     *                                      system name is already registered in
621     *                                      the manager
622     */
623    void register(@Nonnull E n);
624
625    /**
626     * Forget a NamedBean Object created outside the manager.
627     * <p>
628     * The non-system-specific RouteManager uses this method.
629     *
630     * @param n the bean
631     */
632    void deregister(@Nonnull E n);
633
634    /**
635     * The order in which things get saved to the xml file.
636     */
637    static final int SENSORS = 10;
638    static final int TURNOUTS = SENSORS + 10;
639    static final int LIGHTS = TURNOUTS + 10;
640    static final int REPORTERS = LIGHTS + 10;
641    static final int MEMORIES = REPORTERS + 10;
642    static final int SENSORGROUPS = MEMORIES + 10;
643    static final int SIGNALHEADS = SENSORGROUPS + 10;
644    static final int SIGNALMASTS = SIGNALHEADS + 10;
645    static final int SIGNALGROUPS = SIGNALMASTS + 10;
646    static final int BLOCKS = SIGNALGROUPS + 10;
647    static final int OBLOCKS = BLOCKS + 10;
648    static final int LAYOUTBLOCKS = OBLOCKS + 10;
649    static final int SECTIONS = LAYOUTBLOCKS + 10;
650    static final int TRANSITS = SECTIONS + 10;
651    static final int BLOCKBOSS = TRANSITS + 10;
652    static final int ROUTES = BLOCKBOSS + 10;
653    static final int WARRANTS = ROUTES + 10;
654    static final int SIGNALMASTLOGICS = WARRANTS + 10;
655    static final int IDTAGS = SIGNALMASTLOGICS + 10;
656    static final int ANALOGIOS = IDTAGS + 10;
657    static final int METERS = ANALOGIOS + 10;
658    static final int STRINGIOS = METERS + 10;
659    static final int LOGIXS = STRINGIOS + 10;
660    static final int CONDITIONALS = LOGIXS + 10;
661    static final int AUDIO = CONDITIONALS + 10;
662    static final int TIMEBASE = AUDIO + 10;
663    // All LogixNG beans share the "Q" letter. For example, a digital expression
664    // has a system name like "IQDE001".
665    static final int LOGIXNGS = TIMEBASE + 10;                              // LogixNG
666    static final int LOGIXNG_GLOBAL_VARIABLES = LOGIXNGS + 10;               // LogixNG Global Variables
667    static final int LOGIXNG_CONDITIONALNGS = LOGIXNG_GLOBAL_VARIABLES + 10; // LogixNG ConditionalNG
668    static final int LOGIXNG_MODULES = LOGIXNG_CONDITIONALNGS + 10;          // LogixNG Modules
669    static final int LOGIXNG_TABLES = LOGIXNG_MODULES + 10;                  // LogixNG Tables (not bean tables)
670    static final int LOGIXNG_DIGITAL_EXPRESSIONS = LOGIXNG_TABLES + 10;          // LogixNG Expression
671    static final int LOGIXNG_DIGITAL_ACTIONS = LOGIXNG_DIGITAL_EXPRESSIONS + 10; // LogixNG Action
672    static final int LOGIXNG_DIGITAL_BOOLEAN_ACTIONS = LOGIXNG_DIGITAL_ACTIONS + 10;   // LogixNG Digital Boolean Action
673    static final int LOGIXNG_ANALOG_EXPRESSIONS = LOGIXNG_DIGITAL_BOOLEAN_ACTIONS + 10;  // LogixNG AnalogExpression
674    static final int LOGIXNG_ANALOG_ACTIONS = LOGIXNG_ANALOG_EXPRESSIONS + 10;   // LogixNG AnalogAction
675    static final int LOGIXNG_STRING_EXPRESSIONS = LOGIXNG_ANALOG_ACTIONS + 10;   // LogixNG StringExpression
676    static final int LOGIXNG_STRING_ACTIONS = LOGIXNG_STRING_EXPRESSIONS + 10;   // LogixNG StringAction
677    static final int PANELFILES = LOGIXNG_STRING_ACTIONS + 10;
678    static final int ENTRYEXIT = PANELFILES + 10;
679    static final int METERFRAMES = ENTRYEXIT + 10;
680    static final int CTCDATA = METERFRAMES + 10;
681
682    /**
683     * Determine the order that types should be written when storing panel
684     * files. Uses one of the constants defined in this class.
685     * <p>
686     * Yes, that's an overly-centralized methodology, but it works for now.
687     *
688     * @return write order for this Manager; larger is later.
689     */
690    @CheckReturnValue
691    int getXMLOrder();
692
693    /**
694     * Get the user-readable name of the type of NamedBean handled by this
695     * manager.
696     * <p>
697     * For instance, in the code where we are dealing with just a bean and a
698     * message that needs to be passed to the user or in a log.
699     *
700     * @return a string of the bean type that the manager handles, eg Turnout,
701     *         Sensor etc
702     */
703    @CheckReturnValue
704    @Nonnull
705    default String getBeanTypeHandled() {
706        return getBeanTypeHandled(false);
707    }
708
709    /**
710     * Get the user-readable name of the type of NamedBean handled by this
711     * manager.
712     * <p>
713     * For instance, in the code where we are dealing with just a bean and a
714     * message that needs to be passed to the user or in a log.
715     *
716     * @param plural true to return plural form of the type; false to return
717     *               singular form
718     *
719     * @return a string of the bean type that the manager handles, eg Turnout,
720     *         Sensor etc
721     */
722    @CheckReturnValue
723    @Nonnull
724    String getBeanTypeHandled(boolean plural);
725
726    /**
727     * Provide length of the system prefix of the given system name.
728     * <p>
729     * This is a common operation across JMRI, as the system prefix can be
730     * parsed out without knowledge of the type of NamedBean involved.
731     *
732     * @param inputName System Name to provide the prefix
733     * @throws NamedBean.BadSystemNameException If the inputName is not
734     *                                          in normalized form
735     * @return The length of the system-prefix part of the system name in
736     *         standard normalized form
737     */
738    @CheckReturnValue
739    static int getSystemPrefixLength(@Nonnull String inputName) {
740        if (inputName.isEmpty()) {
741            throw new NamedBean.BadSystemNameException();
742        }
743        if (!Character.isLetter(inputName.charAt(0))) {
744            throw new NamedBean.BadSystemNameException();
745        }
746
747        int i;
748        for (i = 1; i < inputName.length(); i++) {
749            if (!Character.isDigit(inputName.charAt(i))) {
750                break;
751            }
752        }
753        return i;
754    }
755
756    /**
757     * Provides the system prefix of the given system name.
758     * <p>
759     * This is a common operation across JMRI, as the system prefix can be
760     * parsed out without knowledge of the type of NamedBean involved.
761     *
762     * @param inputName System name to provide the prefix
763     * @throws NamedBean.BadSystemNameException If the inputName is not
764     *                                          in normalized form
765     * @return The system-prefix part of the system name in standard normalized
766     *         form
767     */
768    @CheckReturnValue
769    @Nonnull
770    static String getSystemPrefix(@Nonnull String inputName) {
771        return inputName.substring(0, getSystemPrefixLength(inputName));
772    }
773
774    /**
775     * Provides the type letter of the given system name.
776     * <p>
777     * This is a common operation across JMRI, as the system prefix can be
778     * parsed out without knowledge of the type of NamedBean involved.
779     *
780     * @param inputName System name to provide the type letter
781     * @throws NamedBean.BadSystemNameException If the inputName is not
782     *                                          in normalized form
783     * @return The type letter of the system name
784     */
785    @CheckReturnValue
786    @Nonnull
787    static String getTypeLetter(@Nonnull String inputName) {
788        return inputName.substring(getSystemPrefixLength(inputName), getSystemPrefixLength(inputName)+1);
789    }
790
791    /**
792     * Provides the suffix (part after the type letter) of the given system name.
793     * <p>
794     * This is a common operation across JMRI, as the system prefix can be
795     * parsed out without knowledge of the type of NamedBean involved.
796     *
797     * @param inputName System name to provide the suffix
798     * @throws NamedBean.BadSystemNameException If the inputName is not
799     *                                          in normalized form
800     * @return The suffix part of the system name
801     */
802    @CheckReturnValue
803    @Nonnull
804    static String getSystemSuffix(@Nonnull String inputName) {
805        return inputName.substring(getSystemPrefixLength(inputName)+1);
806    }
807
808
809    /**
810     * Get a manager-specific tool tip for adding an entry to the manager.
811     *
812     * @return the tool tip or null to disable the tool tip
813     */
814    default String getEntryToolTip() {
815        return null;
816    }
817
818    /**
819     * Register a {@link ManagerDataListener} to hear about adding or removing
820     * items from the list of NamedBeans.
821     *
822     * @param e the data listener to add
823     */
824    void addDataListener(ManagerDataListener<E> e);
825
826    /**
827     * Unregister a previously-added {@link ManagerDataListener}.
828     *
829     * @param e the data listener to remove
830     * @see #addDataListener(ManagerDataListener)
831     */
832    void removeDataListener(ManagerDataListener<E> e);
833
834    /**
835     * Temporarily suppress DataListener notifications.
836     * <p>
837     * This avoids O(N^2) behavior when doing bulk updates, i.e. when loading
838     * lots of Beans. Note that this is (1) optional, in the sense that the
839     * manager is not required to mute and (2) if present, its' temporary, in
840     * the sense that the manager must do a cumulative notification when done.
841     *
842     * @param muted true if notifications should be suppressed; false otherwise
843     */
844    default void setDataListenerMute(boolean muted) {
845    }
846
847    /**
848     * Intended to be equivalent to {@link javax.swing.event.ListDataListener}
849     * without introducing a Swing dependency into core JMRI.
850     *
851     * @param <E> the type to support listening for
852     * @since JMRI 4.11.4 - for use in DataModel code
853     */
854    interface ManagerDataListener<E extends NamedBean> {
855
856        /**
857         * Sent when the contents of the list has changed in a way that's too
858         * complex to characterize with the previous methods.
859         *
860         * @param e encapsulates event information
861         */
862        void contentsChanged(ManagerDataEvent<E> e);
863
864        /**
865         * Sent after the indices in the index0,index1 interval have been
866         * inserted in the data model.
867         *
868         * @param e encapsulates the event information
869         */
870        void intervalAdded(ManagerDataEvent<E> e);
871
872        /**
873         * Sent after the indices in the index0,index1 interval have been
874         * removed from the data model.
875         *
876         * @param e encapsulates the event information
877         */
878        void intervalRemoved(ManagerDataEvent<E> e);
879    }
880
881    /**
882     * Define an event that encapsulates changes to a list.
883     * <p>
884     * Intended to be equivalent to {@link javax.swing.event.ListDataEvent}
885     * without introducing a Swing dependency into core JMRI.
886     *
887     * @param <E> the type to support in the event
888     * @since JMRI 4.11.4 - for use in DataModel code
889     */
890    @javax.annotation.concurrent.Immutable
891    final class ManagerDataEvent<E extends NamedBean> extends java.util.EventObject {
892
893        /**
894         * Equal to {@link javax.swing.event.ListDataEvent#CONTENTS_CHANGED}
895         */
896        public static final int CONTENTS_CHANGED = 0;
897        /**
898         * Equal to {@link javax.swing.event.ListDataEvent#INTERVAL_ADDED}
899         */
900        public static final int INTERVAL_ADDED = 1;
901        /**
902         * Equal to {@link javax.swing.event.ListDataEvent#INTERVAL_REMOVED}
903         */
904        public static final int INTERVAL_REMOVED = 2;
905
906        private final int type;
907        private final int index0;
908        private final int index1;
909        private final transient E changedBean; // used when just one bean is added or removed as an efficiency measure
910        private final transient Manager<E> sourceManager;
911
912        /**
913         * Create a <code>ListDataEvent</code> object.
914         *
915         * @param source      the source of the event (<code>null</code> not
916         *                    permitted).
917         * @param type        the type of the event (should be one of
918         *                    {@link #CONTENTS_CHANGED}, {@link #INTERVAL_ADDED}
919         *                    or {@link #INTERVAL_REMOVED}, although this is not
920         *                    enforced).
921         * @param index0      the index for one end of the modified range of
922         *                    list elements.
923         * @param index1      the index for the other end of the modified range
924         *                    of list elements.
925         * @param changedBean used when just one bean is added or removed,
926         *                    otherwise null
927         */
928        public ManagerDataEvent(@Nonnull Manager<E> source, int type, int index0, int index1, E changedBean) {
929            super(source);
930            this.sourceManager = source;
931            this.type = type;
932            this.index0 = Math.min(index0, index1);  // from javax.swing.event.ListDataEvent implementation
933            this.index1 = Math.max(index0, index1);  // from javax.swing.event.ListDataEvent implementation
934            this.changedBean = changedBean;
935        }
936
937        /**
938         * Get the source of the event in a type-safe manner.
939         *
940         * @return the event source
941         */
942        @Override
943        public Manager<E> getSource() {
944            return sourceManager;
945        }
946
947        /**
948         * Get the index of the first item in the range of modified list
949         * items.
950         *
951         * @return index of the first item in the range of modified list items
952         */
953        public int getIndex0() {
954            return index0;
955        }
956
957        /**
958         * Get the index of the last item in the range of modified list
959         * items.
960         *
961         * @return index of the last item in the range of modified list items
962         */
963        public int getIndex1() {
964            return index1;
965        }
966
967        /**
968         * Get the changed bean or null.
969         *
970         * @return null if more than one bean was changed
971         */
972        public E getChangedBean() {
973            return changedBean;
974        }
975
976        /**
977         * Get a code representing the type of this event, which is usually
978         * one of {@link #CONTENTS_CHANGED}, {@link #INTERVAL_ADDED} or
979         * {@link #INTERVAL_REMOVED}.
980         *
981         * @return the event type
982         */
983        public int getType() {
984            return type;
985        }
986
987        /**
988         * Get a string representing the state of this event.
989         *
990         * @return event state as a string
991         */
992        @Override
993        public String toString() {
994            return getClass().getName() + "[type=" + type + ", index0=" + index0 + ", index1=" + index1 + "]";
995        }
996    }
997
998}