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