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    // Auto names. The atomic integer is always created even if not used, to
053    // simplify concurrency.
054    AtomicInteger lastAutoNamedBeanRef = new AtomicInteger(0);
055    DecimalFormat paddedNumber = new DecimalFormat("0000");
056
057    public AbstractManager(SystemConnectionMemo memo) {
058        this.memo = memo;
059        this._beans = new TreeSet<>(memo.getNamedBeanComparator(getNamedBeanClass()));
060        silenceableProperties.add(PROPERTY_BEANS);
061        setRegisterSelf();
062    }
063
064    final void setRegisterSelf(){
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    /** {@inheritDoc} */
118    @CheckForNull
119    @Override
120    public E getBySystemName(@Nonnull String systemName) {
121        return _tsys.get(systemName);
122    }
123
124    /**
125     * Protected method used by subclasses to over-ride the default behavior of
126     * getBySystemName when a simple string lookup is not sufficient.
127     *
128     * @param systemName the system name to check
129     * @param comparator a Comparator encapsulating the system specific comparison behavior
130     * @return a named bean of the appropriate type, or null if not found
131     */
132    @CheckForNull
133    protected E getBySystemName(String systemName, Comparator<String> comparator){
134        for (Map.Entry<String,E> e : _tsys.entrySet()) {
135            if (0 == comparator.compare(e.getKey(), systemName)) {
136                return e.getValue();
137            }
138        }
139        return null;
140    }
141
142    /** {@inheritDoc} */
143    @Override
144    @CheckForNull
145    public E getByUserName(@Nonnull String userName) {
146        String normalizedUserName = NamedBean.normalizeUserName(userName);
147        return normalizedUserName != null ? _tuser.get(normalizedUserName) : null;
148    }
149
150    /** {@inheritDoc} */
151    @CheckForNull
152    @Override
153    public E getNamedBean(@Nonnull String name) {
154        String normalizedUserName = NamedBean.normalizeUserName(name);
155        if (normalizedUserName != null) {
156            E b = getByUserName(normalizedUserName);
157            if (b != null) {
158                return b;
159            }
160        }
161        return getBySystemName(name);
162    }
163
164    /** {@inheritDoc} */
165    @Override
166    @OverridingMethodsMustInvokeSuper
167    public void deleteBean(@Nonnull E bean, @Nonnull String property) throws PropertyVetoException {
168        // throws PropertyVetoException if vetoed
169        fireVetoableChange(property, bean, null);
170        if ( PROPERTY_DO_DELETE.equals(property) ) {
171            deregister(bean);
172            bean.dispose();
173        }
174    }
175
176    /** {@inheritDoc} */
177    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
178        justification="String already built for use in exception text")
179    @Override
180    @OverridingMethodsMustInvokeSuper
181    public void register(@Nonnull E s) {
182        String systemName = s.getSystemName();
183
184        E existingBean = getBySystemName(systemName);
185        if (existingBean != null) {
186            if (s == existingBean) {
187                log.debug("the named bean is registered twice: {}", systemName);
188            } else {
189                log.error("systemName is already registered: {}", systemName);
190                throw new DuplicateSystemNameException("systemName is already registered: " + systemName);
191            }
192        } else {
193            // Check if the manager already has a bean with a system name that is
194            // not equal to the system name of the new bean, but there the two
195            // system names are treated as the same. For example LT1 and LT01.
196            if (_beans.contains(s)) {
197                final AtomicReference<String> oldSysName = new AtomicReference<>();
198                Comparator<E> c = memo.getNamedBeanComparator(getNamedBeanClass());
199                _beans.forEach(t -> {
200                    if (c.compare(s, t) == 0) {
201                        oldSysName.set(t.getSystemName());
202                    }
203                });
204                if (!systemName.equals(oldSysName.get())) {
205                    String msg = String.format("systemName is already registered. Current system name: %s. New system name: %s",
206                            oldSysName, systemName);
207                    log.error(msg);
208                    throw new DuplicateSystemNameException(msg);
209                }
210            }
211        }
212
213        // save this bean
214        _beans.add(s);
215        _tsys.put(systemName, s);
216        registerUserName(s);
217
218        // notifications
219        int position = isPositionNeeded() ? getPosition(s) : -1;
220        fireDataListenersAdded(position, position, s);
221        if (!silencedProperties.getOrDefault(PROPERTY_BEANS, false)) {
222            fireIndexedPropertyChange(PROPERTY_BEANS, position, null, s);
223        }
224        firePropertyChange(PROPERTY_LENGTH, null, _beans.size());
225        // listen for name and state changes to forward
226        s.addPropertyChangeListener(this);
227    }
228
229    // not efficient, but does job for now
230    private int getPosition(E s) {
231        if (_beans.contains(s)) {
232            // beans are often added in order, check the ends before counting
233            if (_beans.last() == s) {
234                return _beans.size() - 1;
235            }
236            if (_beans.first() == s) {
237                return 0;
238            }
239            return _beans.headSet(s, false).size();
240        } else {
241            return -1;
242        }
243    }
244
245    // the position is only sent to data listeners and "beans" listeners
246    private boolean isPositionNeeded() {
247        return (!muted && !listeners.isEmpty())
248                || (!silencedProperties.getOrDefault(PROPERTY_BEANS, false)
249                        && propertyChangeSupport.hasListeners(PROPERTY_BEANS));
250    }
251
252    /**
253     * Invoked by {@link #register(NamedBean)} to register the user name of the
254     * bean.
255     *
256     * @param s the bean to register
257     */
258    protected void registerUserName(E s) {
259        String userName = s.getUserName();
260        if (userName == null) {
261            return;
262        }
263
264        handleUserNameUniqueness(s);
265        // since we've handled uniqueness,
266        // store the new bean under the name
267        _tuser.put(userName, s);
268    }
269
270    /**
271     * Invoked by {@link #registerUserName(NamedBean)} to ensure uniqueness of
272     * the NamedBean during registration.
273     *
274     * @param s the bean to register
275     */
276    protected void handleUserNameUniqueness(E s) {
277        String userName = s.getUserName();
278        // enforce uniqueness of user names
279        // by setting username to null in any existing bean with the same name
280        // Note that this is not a "move" operation for the user name
281        if (userName != null && _tuser.get(userName) != null && _tuser.get(userName) != s) {
282            _tuser.get(userName).setUserName(null);
283        }
284    }
285
286    /** {@inheritDoc} */
287    @Override
288    @OverridingMethodsMustInvokeSuper
289    public void deregister(@Nonnull E s) {
290        int position = isPositionNeeded() ? getPosition(s) : -1;
291
292        // stop listening for user name changes
293        s.removePropertyChangeListener(this);
294
295        // remove bean from local storage
296        String systemName = s.getSystemName();
297        _beans.remove(s);
298        _tsys.remove(systemName);
299        String userName = s.getUserName();
300        if (userName != null) {
301            _tuser.remove(userName);
302        }
303
304        // notifications
305        fireDataListenersRemoved(position, position, s);
306        if (!silencedProperties.getOrDefault(PROPERTY_BEANS, false)) {
307            fireIndexedPropertyChange(PROPERTY_BEANS, position, s, null);
308        }
309        firePropertyChange(PROPERTY_LENGTH, null, _beans.size());
310    }
311
312    /**
313     * By default there are no custom properties.
314     *
315     * @return empty list
316     */
317    @Override
318    @Nonnull
319    public List<NamedBeanPropertyDescriptor<?>> getKnownBeanProperties() {
320        return new LinkedList<>();
321    }
322
323    /**
324     * Get the outer bean of an encapsulated bean.
325     * Some managers encapsulates the beans and those managers needs to
326     * override this method.
327     * @param  bean the bean
328     * @return the most outer bean or the bean itself if there is no
329     *         outer bean
330     */
331    protected E getOuterBean(E bean) {
332        return bean;
333    }
334
335    /**
336     * The PropertyChangeListener interface in this class is intended to keep
337     * track of user name changes to individual NamedBeans. It is not completely
338     * implemented yet. In particular, listeners are not added to newly
339     * registered objects.
340     *
341     * @param e the event
342     */
343    @Override
344    @SuppressWarnings("unchecked") // The cast of getSource() to E can't be checked due to type erasure, but we catch errors
345    @OverridingMethodsMustInvokeSuper
346    public void propertyChange(PropertyChangeEvent e) {
347        if ( NamedBean.PROPERTY_USERNAME.equals( e.getPropertyName())) {
348            String old = (String) e.getOldValue();  // previous user name
349            String now = (String) e.getNewValue();  // current user name
350            try { // really should always succeed
351                E t = getOuterBean((E) e.getSource());
352                if (old != null) {
353                    _tuser.remove(old); // remove old name for this bean
354                }
355                if (now != null) {
356                    // was there previously a bean with the new name?
357                    if (_tuser.get(now) != null && _tuser.get(now) != t) {
358                        // If so, clear. Note that this is not a "move" operation
359                        _tuser.get(now).setUserName(null);
360                    }
361                    _tuser.put(now, t); // put new name for this bean
362                }
363            } catch (ClassCastException ex) {
364                log.error("Received event of wrong type {}", e.getSource().getClass().getName(), ex);
365            }
366
367            // called DisplayListName, as DisplayName might get used at some point by a NamedBean
368            firePropertyChange(PROPERTY_DISPLAY_LIST_NAME, old, now); // NOI18N
369        }
370    }
371
372    /** {@inheritDoc} */
373    @Override
374    @CheckReturnValue
375    public int getObjectCount() { return _beans.size();}
376
377    /** {@inheritDoc} */
378    @Override
379    @Nonnull
380    public SortedSet<E> getNamedBeanSet() {
381        return Collections.unmodifiableSortedSet(_beans);
382    }
383
384    /**
385     * Inform all registered listeners of a vetoable change. If the
386     * propertyName is "CanDelete" ALL listeners with an interest in the bean
387     * will throw an exception, which is recorded returned back to the invoking
388     * method, so that it can be presented back to the user. However if a
389     * listener decides that the bean can not be deleted then it should throw an
390     * exception with a property name of "DoNotDelete", this is thrown back up
391     * to the user and the delete process should be aborted.
392     *
393     * @param p   The programmatic name of the property that is to be changed.
394     *            "CanDelete" will inquire with all listeners if the item can
395     *            be deleted. "DoDelete" tells the listener to delete the item.
396     * @param old The old value of the property.
397     * @param n   The new value of the property.
398     * @throws PropertyVetoException if the recipients wishes the delete to be
399     *                               aborted.
400     */
401    @OverridingMethodsMustInvokeSuper
402    @Override
403    public void fireVetoableChange(String p, Object old, Object n) throws PropertyVetoException {
404        PropertyChangeEvent evt = new PropertyChangeEvent(this, p, old, n);
405        if ( PROPERTY_CAN_DELETE.equals(p) ) {
406            StringBuilder message = new StringBuilder();
407            for (VetoableChangeListener vc : vetoableChangeSupport.getVetoableChangeListeners()) {
408                try {
409                    vc.vetoableChange(evt);
410                } catch (PropertyVetoException e) {
411                    if (PROPERTY_DO_NOT_DELETE.equals(e.getPropertyChangeEvent().getPropertyName())) {
412                        log.info("Do Not Delete : {}", e.getMessage());
413                        throw e;
414                    }
415                    message.append(e.getMessage()).append("<hr>"); // NOI18N
416                }
417            }
418            throw new PropertyVetoException(message.toString(), evt);
419        } else {
420            try {
421                vetoableChangeSupport.fireVetoableChange(evt);
422            } catch (PropertyVetoException e) {
423                log.error("Change vetoed.", e);
424            }
425        }
426    }
427
428    /** {@inheritDoc} */
429    @Override
430    @OverridingMethodsMustInvokeSuper
431    public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
432
433        if (PROPERTY_CAN_DELETE.equals(evt.getPropertyName())) {
434            StringBuilder message = new StringBuilder();
435            message.append(Bundle.getMessage("VetoFoundIn", getBeanTypeHandled()))
436                    .append("<ul>");
437            boolean found = false;
438            for (NamedBean nb : _beans) {
439                try {
440                    nb.vetoableChange(evt);
441                } catch (PropertyVetoException e) {
442                    if (PROPERTY_DO_NOT_DELETE.equals(e.getPropertyChangeEvent().getPropertyName())) {
443                        throw e;
444                    }
445                    found = true;
446                    message.append("<li>")
447                            .append(e.getMessage())
448                            .append("</li>");
449                }
450            }
451            message.append("</ul>")
452                    .append(Bundle.getMessage("VetoWillBeRemovedFrom", getBeanTypeHandled()));
453            if (found) {
454                throw new PropertyVetoException(message.toString(), evt);
455            }
456        } else {
457            for (NamedBean nb : _beans) {
458                // throws PropertyVetoException if vetoed
459                nb.vetoableChange(evt);
460            }
461        }
462    }
463
464    /**
465     * {@inheritDoc}
466     *
467     * @return {@link jmri.Manager.NameValidity#INVALID} if system name does not
468     *         start with
469     *         {@link #getSystemNamePrefix()}; {@link jmri.Manager.NameValidity#VALID_AS_PREFIX_ONLY}
470     *         if system name equals {@link #getSystemNamePrefix()}; otherwise
471     *         {@link jmri.Manager.NameValidity#VALID} to allow Managers that do
472     *         not perform more specific validation to be considered valid.
473     */
474    @Override
475    public NameValidity validSystemNameFormat(@Nonnull String systemName) {
476        if (getSystemNamePrefix().equals(systemName)) {
477            return NameValidity.VALID_AS_PREFIX_ONLY;
478        }
479        return systemName.startsWith(getSystemNamePrefix()) ? NameValidity.VALID : NameValidity.INVALID;
480    }
481
482    /**
483     * {@inheritDoc}
484     *
485     * The implementation in {@link AbstractManager} should be final, but is not
486     * for four managers that have arbitrary prefixes.
487     */
488    @Override
489    @Nonnull
490    public final String getSystemPrefix() {
491        return memo.getSystemPrefix();
492    }
493
494    /**
495     * {@inheritDoc}
496     */
497    @Override
498    @OverridingMethodsMustInvokeSuper
499    public void setPropertyChangesSilenced(@Nonnull String propertyName, boolean silenced) {
500        if (!silenceableProperties.contains(propertyName)) {
501            throw new IllegalArgumentException("Property " + propertyName + " cannot be silenced.");
502        }
503        silencedProperties.put(propertyName, silenced);
504        if (PROPERTY_BEANS.equals(propertyName) && !silenced) {
505            fireIndexedPropertyChange(PROPERTY_BEANS, _beans.size(), null, null);
506        }
507    }
508
509    /** {@inheritDoc} */
510    @Override
511    public void addDataListener(ManagerDataListener<E> e) {
512        if (e != null) listeners.add(e);
513    }
514
515    /** {@inheritDoc} */
516    @Override
517    public void removeDataListener(ManagerDataListener<E> e) {
518        if (e != null) listeners.remove(e);
519    }
520
521    private final List<ManagerDataListener<E>> listeners = new ArrayList<>();
522
523    private boolean muted = false;
524
525    /** {@inheritDoc} */
526    @Override
527    public void setDataListenerMute(boolean m) {
528        if (muted && !m) {
529            // send a total update, as we haven't kept track of specifics
530            ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.CONTENTS_CHANGED, 0, getObjectCount()-1, null);
531            listeners.forEach(listener -> listener.contentsChanged(e));
532        }
533        this.muted = m;
534    }
535
536    protected void fireDataListenersAdded(int start, int end, E changedBean) {
537        if (muted) return;
538        ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.INTERVAL_ADDED, start, end, changedBean);
539        listeners.forEach(m -> m.intervalAdded(e));
540    }
541
542    protected void fireDataListenersRemoved(int start, int end, E changedBean) {
543        if (muted) return;
544        ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.INTERVAL_REMOVED, start, end, changedBean);
545        listeners.forEach(m -> m.intervalRemoved(e));
546    }
547
548    public void updateAutoNumber(String systemName) {
549        /* The following keeps track of the last created auto system name.
550         currently we do not reuse numbers, although there is nothing to stop the
551         user from manually recreating them */
552        String autoPrefix = getSubSystemNamePrefix() + ":AUTO:";
553        if (systemName.startsWith(autoPrefix)) {
554            try {
555                int autoNumber = Integer.parseInt(systemName.substring(autoPrefix.length()));
556                lastAutoNamedBeanRef.accumulateAndGet(autoNumber, Math::max);
557            } catch (NumberFormatException e) {
558                log.warn("Auto generated SystemName {} is not in the correct format", systemName);
559            }
560        }
561    }
562
563    public String getAutoSystemName() {
564        int nextAutoBlockRef = lastAutoNamedBeanRef.incrementAndGet();
565        StringBuilder b = new StringBuilder(getSubSystemNamePrefix() + ":AUTO:");
566        String nextNumber = paddedNumber.format(nextAutoBlockRef);
567        b.append(nextNumber);
568        return b.toString();
569    }
570
571    /**
572     * Create a System Name from hardware address and system letter prefix.
573     * AbstractManager performs no validation.
574     * @param curAddress hardware address, no system prefix or type letter.
575     * @param prefix - just system prefix, not including Type Letter.
576     * @return full system name with system prefix, type letter and hardware address.
577     * @throws JmriException if unable to create a system name.
578     */
579    public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException {
580        return prefix + typeLetter() + curAddress;
581    }
582
583    /**
584     * checks for numeric-only system names.
585     * @param curAddress the System name ( excluding both prefix and type letter) to check.
586     * @return unchanged if is numeric string.
587     * @throws JmriException if not numeric.
588     */
589    protected String checkNumeric(@Nonnull String curAddress) throws JmriException {
590        try {
591            Integer.parseInt(curAddress);
592        } catch (java.lang.NumberFormatException ex) {
593            throw new JmriException("Hardware Address passed "+curAddress+" should be a number");
594        }
595        return curAddress;
596    }
597
598    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractManager.class);
599
600}