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