001package jmri.managers;
002
003import java.beans.*;
004import java.text.DecimalFormat;
005import java.util.*;
006import java.util.concurrent.atomic.AtomicInteger;
007import java.util.concurrent.atomic.AtomicReference;
008
009import javax.annotation.CheckReturnValue;
010import javax.annotation.CheckForNull;
011import javax.annotation.Nonnull;
012import javax.annotation.OverridingMethodsMustInvokeSuper;
013
014import jmri.*;
015import jmri.beans.VetoableChangeSupport;
016import jmri.NamedBean.DuplicateSystemNameException;
017
018/**
019 * Abstract partial implementation for all Manager-type classes.
020 * <p>
021 * Note that this does not enforce any particular system naming convention at
022 * the present time. They're just names...
023 * <p>
024 * It does include, with AbstractNamedBean, the implementation of the normalized
025 * user name.
026 * <p>
027 * See source file for extensive implementation notes.
028 *
029 * @param <E> the class this manager supports
030 * @see jmri.NamedBean#normalizeUserName
031 *
032 * @author Bob Jacobsen Copyright (C) 2003
033 */
034public abstract class AbstractManager<E extends NamedBean> extends VetoableChangeSupport implements Manager<E>, PropertyChangeListener, VetoableChangeListener {
035
036    // The data model consists of several components:
037    // * The primary reference is _beans, a SortedSet of NamedBeans, sorted automatically on system name.
038    //      Currently that's implemented as a TreeSet; further performance work might change that
039    //      Live access is available as an unmodifiableSortedSet via getNamedBeanSet()
040    // * The manager also maintains synchronized maps from SystemName -> NamedBean (_tsys) and UserName -> NamedBean (_tuser)
041    //      These are not made available: get access through the manager calls
042    //      These use regular HashMaps instead of some sorted form for efficiency
043    // * Caches for the List<String> getSystemNameList() and List<E> getNamedBeanList() calls
044
045    protected final SystemConnectionMemo memo;
046    protected final TreeSet<E> _beans;
047    protected final Hashtable<String, E> _tsys = new Hashtable<>();   // stores known E (NamedBean, i.e. Turnout) instances by system name
048    protected final Hashtable<String, E> _tuser = new Hashtable<>();  // stores known E (NamedBean, i.e. Turnout) instances by user name
049    protected final Map<String, Boolean> silencedProperties = new HashMap<>();
050    protected final Set<String> silenceableProperties = new HashSet<>();
051
052    // caches
053    private ArrayList<String> cachedSystemNameList = null;
054    private ArrayList<E> cachedNamedBeanList = null;
055
056    // Auto names. The atomic integer is always created even if not used, to
057    // simplify concurrency.
058    AtomicInteger lastAutoNamedBeanRef = new AtomicInteger(0);
059    DecimalFormat paddedNumber = new DecimalFormat("0000");
060
061    public AbstractManager(SystemConnectionMemo memo) {
062        this.memo = memo;
063        this._beans = new TreeSet<>(memo.getNamedBeanComparator(getNamedBeanClass()));
064        silenceableProperties.add("beans");
065        registerSelf();
066    }
067
068    public AbstractManager() {
069        // create and use a reference to an internal connection
070        this(InstanceManager.getDefault(jmri.jmrix.internal.InternalSystemConnectionMemo.class));
071    }
072    
073    /**
074     * By default, register this manager to store as configuration information.
075     * Override to change that.
076     */
077    @OverridingMethodsMustInvokeSuper
078    protected void registerSelf() {
079        log.debug("registerSelf for config of type {}", getClass());
080        InstanceManager.getOptionalDefault(ConfigureManager.class).ifPresent(cm -> {
081            cm.registerConfig(this, getXMLOrder());
082            log.debug("registering for config of type {}", getClass());
083        });
084    }
085
086    /** {@inheritDoc} */
087    @Override
088    @Nonnull
089    public SystemConnectionMemo getMemo() {
090        return memo;
091    }
092
093    /** {@inheritDoc} */
094    @Override
095    @Nonnull
096    public String makeSystemName(@Nonnull String s, boolean logErrors, Locale locale) {
097        try {
098            return Manager.super.makeSystemName(s, logErrors, locale);
099        } catch (IllegalArgumentException ex) {
100            if (logErrors || log.isTraceEnabled()) {
101                log.error("Invalid system name for {}: {}", getBeanTypeHandled(), ex.getMessage());
102            }
103            throw ex;
104        }
105    }
106
107    /** {@inheritDoc} */
108    @Override
109    @OverridingMethodsMustInvokeSuper
110    public void dispose() {
111        InstanceManager.getOptionalDefault(ConfigureManager.class).ifPresent(cm -> cm.deregister(this));
112        _beans.clear();
113        _tsys.clear();
114        _tuser.clear();
115    }
116
117    /**
118     * Get a NamedBean by its system name.
119     *
120     * @param systemName the system name
121     * @return the result of {@link #getBySystemName(java.lang.String)}
122     *         with systemName
123     * @deprecated since 4.15.6; use
124     * {@link #getBySystemName(java.lang.String)} instead
125     */
126    @Deprecated
127    protected E getInstanceBySystemName(String systemName) {
128        return getBySystemName(systemName);
129    }
130
131    /**
132     * Get a NamedBean by its user name.
133     *
134     * @param userName the user name
135     * @return the result of {@link #getByUserName(java.lang.String)} call,
136     *         with userName
137     * @deprecated since 4.15.6; use
138     * {@link #getByUserName(java.lang.String)} instead
139     */
140    @Deprecated
141    protected E getInstanceByUserName(String userName) {
142        return getByUserName(userName);
143    }
144
145    /** {@inheritDoc} */
146    @Override
147    public E getBySystemName(@Nonnull String systemName) {
148        return _tsys.get(systemName);
149    }
150
151    /**
152     * Protected method used by subclasses to over-ride the default behavior of
153     * getBySystemName when a simple string lookup is not sufficient.
154     *
155     * @param systemName the system name to check
156     * @param comparator a Comparator encapsulating the system specific comparison behavior
157     * @return a named bean of the appropriate type, or null if not found
158     */
159    protected E getBySystemName(String systemName, Comparator<String> comparator){
160        for (Map.Entry<String,E> e : _tsys.entrySet()) {
161            if (0 == comparator.compare(e.getKey(), systemName)) {
162                return e.getValue();
163            }
164        }
165        return null;
166    }
167
168    /** {@inheritDoc} */
169    @Override
170    @CheckForNull
171    public E getByUserName(@Nonnull String userName) {
172        String normalizedUserName = NamedBean.normalizeUserName(userName);
173        return normalizedUserName != null ? _tuser.get(normalizedUserName) : null;
174    }
175
176    /** {@inheritDoc} */
177    @Override
178    public E getNamedBean(@Nonnull String name) {
179        String normalizedUserName = NamedBean.normalizeUserName(name);
180        if (normalizedUserName != null) {
181            E b = getByUserName(normalizedUserName);
182            if (b != null) {
183                return b;
184            }
185        }
186        return getBySystemName(name);
187    }
188
189    /** {@inheritDoc} */
190    @Override
191    @OverridingMethodsMustInvokeSuper
192    public void deleteBean(@Nonnull E bean, @Nonnull String property) throws PropertyVetoException {
193        // throws PropertyVetoException if vetoed
194        fireVetoableChange(property, bean, null);
195        if (property.equals("DoDelete")) { // NOI18N
196            deregister(bean);
197            bean.dispose();
198        }
199    }
200
201    /** {@inheritDoc} */
202    @Override
203    @OverridingMethodsMustInvokeSuper
204    public void register(@Nonnull E s) {
205        String systemName = s.getSystemName();
206
207        E existingBean = getBySystemName(systemName);
208        if (existingBean != null) {
209            if (s == existingBean) {
210                log.debug("the named bean is registered twice: {}", systemName);
211            } else {
212                log.error("systemName is already registered: {}", systemName);
213                throw new DuplicateSystemNameException("systemName is already registered: " + systemName);
214            }
215        } else {
216            // Check if the manager already has a bean with a system name that is
217            // not equal to the system name of the new bean, but there the two
218            // system names are treated as the same. For example LT1 and LT01.
219            if (_beans.contains(s)) {
220                final AtomicReference<String> oldSysName = new AtomicReference<>();
221                Comparator<E> c = memo.getNamedBeanComparator(getNamedBeanClass());
222                _beans.forEach(t -> {
223                    if (c.compare(s, t) == 0) {
224                        oldSysName.set(t.getSystemName());
225                    }
226                });
227                if (!systemName.equals(oldSysName.get())) {
228                    String msg = String.format("systemName is already registered. Current system name: %s. New system name: %s",
229                            oldSysName, systemName);
230                    log.error(msg);
231                    throw new DuplicateSystemNameException(msg);
232                }
233            }
234        }
235
236        // clear caches
237        cachedSystemNameList = null;
238        cachedNamedBeanList = null;
239        
240        // save this bean
241        _beans.add(s);
242        _tsys.put(systemName, s);
243        registerUserName(s);
244
245        // notifications
246        int position = getPosition(s);
247        fireDataListenersAdded(position, position, s);
248        if (!silencedProperties.getOrDefault("beans", false)) {
249            fireIndexedPropertyChange("beans", position, null, s);
250        }
251        firePropertyChange("length", null, _beans.size());
252        // listen for name and state changes to forward
253        s.addPropertyChangeListener(this);
254    }
255
256    // not efficient, but does job for now
257    private int getPosition(E s) {
258        if (_beans.contains(s)) {
259            return _beans.headSet(s, false).size();
260        } else {
261            return -1;
262        }
263    }
264
265    /**
266     * Invoked by {@link #register(NamedBean)} to register the user name of the
267     * bean.
268     *
269     * @param s the bean to register
270     */
271    protected void registerUserName(E s) {
272        String userName = s.getUserName();
273        if (userName == null) {
274            return;
275        }
276
277        handleUserNameUniqueness(s);
278        // since we've handled uniqueness,
279        // store the new bean under the name
280        _tuser.put(userName, s);
281    }
282
283    /**
284     * Invoked by {@link #registerUserName(NamedBean)} to ensure uniqueness of
285     * the NamedBean during registration.
286     *
287     * @param s the bean to register
288     */
289    protected void handleUserNameUniqueness(E s) {
290        String userName = s.getUserName();
291        // enforce uniqueness of user names
292        // by setting username to null in any existing bean with the same name
293        // Note that this is not a "move" operation for the user name
294        if (userName != null && _tuser.get(userName) != null && _tuser.get(userName) != s) {
295            _tuser.get(userName).setUserName(null);
296        }
297    }
298
299    /** {@inheritDoc} */
300    @Override
301    @OverridingMethodsMustInvokeSuper
302    public void deregister(@Nonnull E s) {
303        int position = getPosition(s);
304
305        // clear caches
306        cachedSystemNameList = null;
307        cachedNamedBeanList = null;
308
309        // stop listening for user name changes
310        s.removePropertyChangeListener(this);
311        
312        // remove bean from local storage
313        String systemName = s.getSystemName();
314        _beans.remove(s);
315        _tsys.remove(systemName);
316        String userName = s.getUserName();
317        if (userName != null) {
318            _tuser.remove(userName);
319        }
320        
321        // notifications
322        fireDataListenersRemoved(position, position, s);
323        if (!silencedProperties.getOrDefault("beans", false)) {
324            fireIndexedPropertyChange("beans", position, s, null);
325        }
326        firePropertyChange("length", null, _beans.size());
327    }
328
329    /**
330     * By default there are no custom properties.
331     *
332     * @return empty list
333     */
334    @Override
335    @Nonnull
336    public List<NamedBeanPropertyDescriptor<?>> getKnownBeanProperties() {
337        return new LinkedList<>();
338    }
339
340    /**
341     * The PropertyChangeListener interface in this class is intended to keep
342     * track of user name changes to individual NamedBeans. It is not completely
343     * implemented yet. In particular, listeners are not added to newly
344     * registered objects.
345     *
346     * @param e the event
347     */
348    @Override
349    @SuppressWarnings("unchecked") // The cast of getSource() to E can't be checked due to type erasure, but we catch errors
350    @OverridingMethodsMustInvokeSuper
351    public void propertyChange(PropertyChangeEvent e) {
352        if (e.getPropertyName().equals("UserName")) {
353            String old = (String) e.getOldValue();  // previous user name
354            String now = (String) e.getNewValue();  // current user name
355            try { // really should always succeed
356                E t = (E) e.getSource();
357                if (old != null) {
358                    _tuser.remove(old); // remove old name for this bean
359                }
360                if (now != null) {
361                    // was there previously a bean with the new name?
362                    if (_tuser.get(now) != null && _tuser.get(now) != t) {
363                        // If so, clear. Note that this is not a "move" operation
364                        _tuser.get(now).setUserName(null);
365                    }
366                    _tuser.put(now, t); // put new name for this bean
367                }
368            } catch (ClassCastException ex) {
369                log.error("Received event of wrong type {}", e.getSource().getClass().getName(), ex);
370            }
371
372            // called DisplayListName, as DisplayName might get used at some point by a NamedBean
373            firePropertyChange("DisplayListName", old, now); // NOI18N
374        }
375    }
376
377    /** {@inheritDoc} */
378    @Override
379    @CheckReturnValue
380    public int getObjectCount() { return _beans.size();}    
381
382    /** {@inheritDoc} */
383    @Override
384    @Nonnull
385    @Deprecated  // will be removed when superclass method is removed due to @Override
386    public List<String> getSystemNameList() {
387        jmri.util.LoggingUtil.deprecationWarning(log, "getSystemNameList");
388        if (cachedSystemNameList == null) {
389            cachedSystemNameList = new ArrayList<>();
390            _beans.forEach(b -> cachedSystemNameList.add(b.getSystemName()));
391        }
392        return Collections.unmodifiableList(cachedSystemNameList);
393    }
394
395    /** {@inheritDoc} */
396    @Override
397    @Nonnull
398    @Deprecated  // will be removed when superclass method is removed due to @Override
399    public List<E> getNamedBeanList() {
400        jmri.util.LoggingUtil.deprecationWarning(log, "getNamedBeanList");
401        if (cachedNamedBeanList == null) {
402            cachedNamedBeanList = new ArrayList<>(_beans);
403        }
404        return Collections.unmodifiableList(cachedNamedBeanList);
405    }
406
407    /** {@inheritDoc} */
408    @Override
409    @Nonnull
410    public SortedSet<E> getNamedBeanSet() {
411        return Collections.unmodifiableSortedSet(_beans);
412    }
413
414    /**
415     * Inform all registered listeners of a vetoable change. If the
416     * propertyName is "CanDelete" ALL listeners with an interest in the bean
417     * will throw an exception, which is recorded returned back to the invoking
418     * method, so that it can be presented back to the user. However if a
419     * listener decides that the bean can not be deleted then it should throw an
420     * exception with a property name of "DoNotDelete", this is thrown back up
421     * to the user and the delete process should be aborted.
422     *
423     * @param p   The programmatic name of the property that is to be changed.
424     *            "CanDelete" will inquire with all listeners if the item can
425     *            be deleted. "DoDelete" tells the listener to delete the item.
426     * @param old The old value of the property.
427     * @param n   The new value of the property.
428     * @throws PropertyVetoException if the recipients wishes the delete to be
429     *                               aborted.
430     */
431    @OverridingMethodsMustInvokeSuper
432    @Override
433    public void fireVetoableChange(String p, Object old, Object n) throws PropertyVetoException {
434        PropertyChangeEvent evt = new PropertyChangeEvent(this, p, old, n);
435        if (p.equals("CanDelete")) { // NOI18N
436            StringBuilder message = new StringBuilder();
437            for (VetoableChangeListener vc : vetoableChangeSupport.getVetoableChangeListeners()) {
438                try {
439                    vc.vetoableChange(evt);
440                } catch (PropertyVetoException e) {
441                    if (e.getPropertyChangeEvent().getPropertyName().equals("DoNotDelete")) { // NOI18N
442                        log.info(e.getMessage());
443                        throw e;
444                    }
445                    message.append(e.getMessage()).append("<hr>"); // NOI18N
446                }
447            }
448            throw new PropertyVetoException(message.toString(), evt);
449        } else {
450            try {
451                vetoableChangeSupport.fireVetoableChange(evt);
452            } catch (PropertyVetoException e) {
453                log.error("Change vetoed.", e);
454            }
455        }
456    }
457
458    /** {@inheritDoc} */
459    @Override
460    @OverridingMethodsMustInvokeSuper
461    public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
462
463        if ("CanDelete".equals(evt.getPropertyName())) { // NOI18N
464            StringBuilder message = new StringBuilder();
465            message.append(Bundle.getMessage("VetoFoundIn", getBeanTypeHandled()))
466                    .append("<ul>");
467            boolean found = false;
468            for (NamedBean nb : _beans) {
469                try {
470                    nb.vetoableChange(evt);
471                } catch (PropertyVetoException e) {
472                    if (e.getPropertyChangeEvent().getPropertyName().equals("DoNotDelete")) { // NOI18N
473                        throw e;
474                    }
475                    found = true;
476                    message.append("<li>")
477                            .append(e.getMessage())
478                            .append("</li>");
479                }
480            }
481            message.append("</ul>")
482                    .append(Bundle.getMessage("VetoWillBeRemovedFrom", getBeanTypeHandled()));
483            if (found) {
484                throw new PropertyVetoException(message.toString(), evt);
485            }
486        } else {
487            for (NamedBean nb : _beans) {
488                // throws PropertyVetoException if vetoed
489                nb.vetoableChange(evt);
490            }
491        }
492    }
493
494    /**
495     * {@inheritDoc}
496     *
497     * @return {@link jmri.Manager.NameValidity#INVALID} if system name does not
498     *         start with
499     *         {@link #getSystemNamePrefix()}; {@link jmri.Manager.NameValidity#VALID_AS_PREFIX_ONLY}
500     *         if system name equals {@link #getSystemNamePrefix()}; otherwise
501     *         {@link jmri.Manager.NameValidity#VALID} to allow Managers that do
502     *         not perform more specific validation to be considered valid.
503     */
504    @Override
505    public NameValidity validSystemNameFormat(@Nonnull String systemName) {
506        if (getSystemNamePrefix().equals(systemName)) {
507            return NameValidity.VALID_AS_PREFIX_ONLY;
508        }
509        return systemName.startsWith(getSystemNamePrefix()) ? NameValidity.VALID : NameValidity.INVALID;
510    }
511
512    /**
513     * {@inheritDoc}
514     * 
515     * The implementation in {@link AbstractManager} should be final, but is not
516     * for four managers that have arbitrary prefixes.
517     */
518    @Override
519    @Nonnull
520    public final String getSystemPrefix() {
521        return memo.getSystemPrefix();
522    }
523
524    /**
525     * {@inheritDoc}
526     */
527    @Override
528    @OverridingMethodsMustInvokeSuper
529    public void setPropertyChangesSilenced(@Nonnull String propertyName, boolean silenced) {
530        if (!silenceableProperties.contains(propertyName)) {
531            throw new IllegalArgumentException("Property " + propertyName + " cannot be silenced.");
532        }
533        silencedProperties.put(propertyName, silenced);
534        if (propertyName.equals("beans") && !silenced) {
535            fireIndexedPropertyChange("beans", _beans.size(), null, null);
536        }
537    }
538
539    /** {@inheritDoc} */
540    @Override
541    @Deprecated
542    public void addDataListener(ManagerDataListener<E> e) {
543        if (e != null) listeners.add(e);
544    }
545
546    /** {@inheritDoc} */
547    @Override
548    @Deprecated
549    public void removeDataListener(ManagerDataListener<E> e) {
550        if (e != null) listeners.remove(e);
551    }
552
553    @SuppressWarnings("deprecation")
554    private final List<ManagerDataListener<E>> listeners = new ArrayList<>();
555
556    private boolean muted = false;
557    
558    /** {@inheritDoc} */
559    @Override
560    @Deprecated
561    @SuppressWarnings("deprecation")
562    public void setDataListenerMute(boolean m) {
563        if (muted && !m) {
564            // send a total update, as we haven't kept track of specifics
565            ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.CONTENTS_CHANGED, 0, getObjectCount()-1, null);
566            listeners.forEach(listener -> listener.contentsChanged(e));          
567        }
568        this.muted = m;
569    }
570
571    @Deprecated
572    @SuppressWarnings("deprecation")
573    protected void fireDataListenersAdded(int start, int end, E changedBean) {
574        if (muted) return;
575        ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.INTERVAL_ADDED, start, end, changedBean);
576        listeners.forEach(m -> m.intervalAdded(e));
577    }
578
579    @Deprecated
580    @SuppressWarnings("deprecation")
581    protected void fireDataListenersRemoved(int start, int end, E changedBean) {
582        if (muted) return;
583        ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.INTERVAL_REMOVED, start, end, changedBean);
584        listeners.forEach(m -> m.intervalRemoved(e));
585    }
586
587    public void updateAutoNumber(String systemName) {
588        /* The following keeps track of the last created auto system name.
589         currently we do not reuse numbers, although there is nothing to stop the
590         user from manually recreating them */
591        String autoPrefix = getSubSystemNamePrefix() + ":AUTO:";
592        if (systemName.startsWith(autoPrefix)) {
593            try {
594                int autoNumber = Integer.parseInt(systemName.substring(autoPrefix.length()));
595                lastAutoNamedBeanRef.accumulateAndGet(autoNumber, Math::max);
596            } catch (NumberFormatException e) {
597                log.warn("Auto generated SystemName {} is not in the correct format", systemName);
598            }
599        }
600    }
601
602    public String getAutoSystemName() {
603        int nextAutoBlockRef = lastAutoNamedBeanRef.incrementAndGet();
604        StringBuilder b = new StringBuilder(getSubSystemNamePrefix() + ":AUTO:");
605        String nextNumber = paddedNumber.format(nextAutoBlockRef);
606        b.append(nextNumber);
607        return b.toString();
608    }
609    
610    /**
611     * Create a System Name from hardware address and system letter prefix.
612     * AbstractManager performs no validation.
613     * @param curAddress hardware address, no system prefix or type letter.
614     * @param prefix - just system prefix, not including Type Letter.
615     * @return full system name with system prefix, type letter and hardware address.
616     * @throws JmriException if unable to create a system name.
617     */
618    public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException {
619        return prefix + typeLetter() + curAddress;
620    }
621    
622    /**
623     * checks for numeric-only system names.
624     * @param curAddress the System name ( excluding both prefix and type letter) to check.
625     * @return unchanged if is numeric string.
626     * @throws JmriException if not numeric.
627     */
628    protected String checkNumeric(@Nonnull String curAddress) throws JmriException {
629        try {
630            Integer.parseInt(curAddress);
631        } catch (java.lang.NumberFormatException ex) {
632            throw new JmriException("Hardware Address passed "+curAddress+" should be a number");
633        }
634        return curAddress;
635    }
636    
637    /**
638     * Get the Next valid hardware address.
639     * Used by the Turnout / Sensor / Reporter / Light Manager classes.
640     * <p>
641     * @param curAddress the starting hardware address to get the next valid from.
642     * @param prefix system prefix, just system name, not type letter.
643     * @return the next valid system name, excluding both system name prefix and type letter.
644     * @throws JmriException    if unable to get the current / next address, 
645     *                          or more than 10 next addresses in use.
646     * @deprecated since 4.21.3; use #getNextValidAddress(String, String, boolean) instead.
647     */
648    @Nonnull
649    @Deprecated
650    public final String getNextValidAddress(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException {
651        jmri.util.LoggingUtil.deprecationWarning(log, "getNextValidAddress");
652        return getNextValidAddress(curAddress, prefix, false);
653    }
654    
655    /**
656     * Get the Next valid hardware address.
657     * Used by the Turnout / Sensor / Reporter / Light Manager classes.
658     * <p>
659     * System-specific methods may want to override getIncrement() rather than this one.
660     * @param curAddress the starting hardware address to get the next valid from.
661     * @param prefix system prefix, just system name, not type letter.
662     * @param ignoreInitialExisting false to return the starting address if it 
663     *                          does not exist, else true to force an increment.
664     * @return the next valid system name not already in use, excluding both system name prefix and type letter.
665     * @throws JmriException    if unable to get the current / next address, 
666     *                          or more than 10 next addresses in use.
667     */
668    @Nonnull
669    public String getNextValidAddress(@Nonnull String curAddress, @Nonnull String prefix, boolean ignoreInitialExisting) throws JmriException {
670        log.debug("getNextValid for address {} ignoring {}", curAddress, ignoreInitialExisting);
671        String testAddr;
672        NamedBean bean;
673        int increment;
674        // If hardware address passed does not already exist then this is the next valid address.
675        try {
676            // System.out.format("curaddress: "+curAddress);
677            testAddr = validateSystemNameFormat(createSystemName(curAddress,prefix));
678            // System.out.format("testaddr: "+testAddr);
679            bean = getBySystemName(testAddr);
680            increment = ( bean instanceof Turnout ? ((Turnout)bean).getNumberOutputBits() : 1);
681            testAddr = testAddr.substring(getSystemNamePrefix().length());
682            
683            // do not check for incrementability here as could be String only
684            // getIncrement(testAddr, increment);
685        }
686        catch ( NamedBean.BadSystemNameException | JmriException ex ){
687            throw new JmriException(ex.getMessage());
688        }
689        if (bean == null && !ignoreInitialExisting) {
690            log.debug("address {} not in use", curAddress);
691            return curAddress;
692        }
693        for (int i = 0; i <10; i++) {
694            testAddr = getIncrement(testAddr, increment);
695            bean = getBySystemName(validateSystemNameFormat(createSystemName(testAddr,prefix)));
696            if ( bean == null) {
697                return testAddr;
698            }
699        }
700        throw new JmriException(Bundle.getMessage("InvalidNextValidTenInUse",getBeanTypeHandled(true),curAddress,testAddr));
701    }
702    
703    /**
704     * Increment a hardware address.
705     * <p>
706     * Default is to increment only an existing number.
707     * Sub-classes may wish to override this.
708     * @param curAddress the address to increment, excluding both system name prefix and type letter.
709     * @param increment the amount to increment by.
710     * @return incremented address, no system prefix or type letter.
711     * @throws JmriException if unable to increment the address.
712     */
713    @Nonnull
714    protected String getIncrement(String curAddress, int increment) throws JmriException {
715        return getIncrementFromExistingNumber(curAddress,increment);
716    }
717    
718    /**
719     * Increment a hardware address with an existing number.
720     * <p>
721     * @param curAddress the address to increment, excluding both system name prefix and type letter
722     * @param increment the amount to increment by.
723     * @return incremented number.
724     * @throws JmriException if unable to increment the address.
725     */
726    @Nonnull
727    protected String getIncrementFromExistingNumber(String curAddress, int increment) throws JmriException {
728        String newIncrement = jmri.util.StringUtil.incrementLastNumberInString(curAddress, increment);
729        if (newIncrement==null) {
730            throw new JmriException("No existing number found when incrementing " + curAddress);
731        }
732        return newIncrement;
733    }
734
735    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractManager.class);
736
737}