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("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.equals("DoDelete")) { // NOI18N
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 = getPosition(s);
220        fireDataListenersAdded(position, position, s);
221        if (!silencedProperties.getOrDefault("beans", false)) {
222            fireIndexedPropertyChange("beans", position, null, s);
223        }
224        firePropertyChange("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            return _beans.headSet(s, false).size();
233        } else {
234            return -1;
235        }
236    }
237
238    /**
239     * Invoked by {@link #register(NamedBean)} to register the user name of the
240     * bean.
241     *
242     * @param s the bean to register
243     */
244    protected void registerUserName(E s) {
245        String userName = s.getUserName();
246        if (userName == null) {
247            return;
248        }
249
250        handleUserNameUniqueness(s);
251        // since we've handled uniqueness,
252        // store the new bean under the name
253        _tuser.put(userName, s);
254    }
255
256    /**
257     * Invoked by {@link #registerUserName(NamedBean)} to ensure uniqueness of
258     * the NamedBean during registration.
259     *
260     * @param s the bean to register
261     */
262    protected void handleUserNameUniqueness(E s) {
263        String userName = s.getUserName();
264        // enforce uniqueness of user names
265        // by setting username to null in any existing bean with the same name
266        // Note that this is not a "move" operation for the user name
267        if (userName != null && _tuser.get(userName) != null && _tuser.get(userName) != s) {
268            _tuser.get(userName).setUserName(null);
269        }
270    }
271
272    /** {@inheritDoc} */
273    @Override
274    @OverridingMethodsMustInvokeSuper
275    public void deregister(@Nonnull E s) {
276        int position = getPosition(s);
277
278        // stop listening for user name changes
279        s.removePropertyChangeListener(this);
280
281        // remove bean from local storage
282        String systemName = s.getSystemName();
283        _beans.remove(s);
284        _tsys.remove(systemName);
285        String userName = s.getUserName();
286        if (userName != null) {
287            _tuser.remove(userName);
288        }
289
290        // notifications
291        fireDataListenersRemoved(position, position, s);
292        if (!silencedProperties.getOrDefault("beans", false)) {
293            fireIndexedPropertyChange("beans", position, s, null);
294        }
295        firePropertyChange("length", null, _beans.size());
296    }
297
298    /**
299     * By default there are no custom properties.
300     *
301     * @return empty list
302     */
303    @Override
304    @Nonnull
305    public List<NamedBeanPropertyDescriptor<?>> getKnownBeanProperties() {
306        return new LinkedList<>();
307    }
308
309    /**
310     * The PropertyChangeListener interface in this class is intended to keep
311     * track of user name changes to individual NamedBeans. It is not completely
312     * implemented yet. In particular, listeners are not added to newly
313     * registered objects.
314     *
315     * @param e the event
316     */
317    @Override
318    @SuppressWarnings("unchecked") // The cast of getSource() to E can't be checked due to type erasure, but we catch errors
319    @OverridingMethodsMustInvokeSuper
320    public void propertyChange(PropertyChangeEvent e) {
321        if (e.getPropertyName().equals("UserName")) {
322            String old = (String) e.getOldValue();  // previous user name
323            String now = (String) e.getNewValue();  // current user name
324            try { // really should always succeed
325                E t = (E) e.getSource();
326                if (old != null) {
327                    _tuser.remove(old); // remove old name for this bean
328                }
329                if (now != null) {
330                    // was there previously a bean with the new name?
331                    if (_tuser.get(now) != null && _tuser.get(now) != t) {
332                        // If so, clear. Note that this is not a "move" operation
333                        _tuser.get(now).setUserName(null);
334                    }
335                    _tuser.put(now, t); // put new name for this bean
336                }
337            } catch (ClassCastException ex) {
338                log.error("Received event of wrong type {}", e.getSource().getClass().getName(), ex);
339            }
340
341            // called DisplayListName, as DisplayName might get used at some point by a NamedBean
342            firePropertyChange("DisplayListName", old, now); // NOI18N
343        }
344    }
345
346    /** {@inheritDoc} */
347    @Override
348    @CheckReturnValue
349    public int getObjectCount() { return _beans.size();}
350
351    /** {@inheritDoc} */
352    @Override
353    @Nonnull
354    public SortedSet<E> getNamedBeanSet() {
355        return Collections.unmodifiableSortedSet(_beans);
356    }
357
358    /**
359     * Inform all registered listeners of a vetoable change. If the
360     * propertyName is "CanDelete" ALL listeners with an interest in the bean
361     * will throw an exception, which is recorded returned back to the invoking
362     * method, so that it can be presented back to the user. However if a
363     * listener decides that the bean can not be deleted then it should throw an
364     * exception with a property name of "DoNotDelete", this is thrown back up
365     * to the user and the delete process should be aborted.
366     *
367     * @param p   The programmatic name of the property that is to be changed.
368     *            "CanDelete" will inquire with all listeners if the item can
369     *            be deleted. "DoDelete" tells the listener to delete the item.
370     * @param old The old value of the property.
371     * @param n   The new value of the property.
372     * @throws PropertyVetoException if the recipients wishes the delete to be
373     *                               aborted.
374     */
375    @OverridingMethodsMustInvokeSuper
376    @Override
377    public void fireVetoableChange(String p, Object old, Object n) throws PropertyVetoException {
378        PropertyChangeEvent evt = new PropertyChangeEvent(this, p, old, n);
379        if (p.equals("CanDelete")) { // NOI18N
380            StringBuilder message = new StringBuilder();
381            for (VetoableChangeListener vc : vetoableChangeSupport.getVetoableChangeListeners()) {
382                try {
383                    vc.vetoableChange(evt);
384                } catch (PropertyVetoException e) {
385                    if (e.getPropertyChangeEvent().getPropertyName().equals("DoNotDelete")) { // NOI18N
386                        log.info("Do Not Delete : {}", e.getMessage());
387                        throw e;
388                    }
389                    message.append(e.getMessage()).append("<hr>"); // NOI18N
390                }
391            }
392            throw new PropertyVetoException(message.toString(), evt);
393        } else {
394            try {
395                vetoableChangeSupport.fireVetoableChange(evt);
396            } catch (PropertyVetoException e) {
397                log.error("Change vetoed.", e);
398            }
399        }
400    }
401
402    /** {@inheritDoc} */
403    @Override
404    @OverridingMethodsMustInvokeSuper
405    public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
406
407        if ("CanDelete".equals(evt.getPropertyName())) { // NOI18N
408            StringBuilder message = new StringBuilder();
409            message.append(Bundle.getMessage("VetoFoundIn", getBeanTypeHandled()))
410                    .append("<ul>");
411            boolean found = false;
412            for (NamedBean nb : _beans) {
413                try {
414                    nb.vetoableChange(evt);
415                } catch (PropertyVetoException e) {
416                    if (e.getPropertyChangeEvent().getPropertyName().equals("DoNotDelete")) { // NOI18N
417                        throw e;
418                    }
419                    found = true;
420                    message.append("<li>")
421                            .append(e.getMessage())
422                            .append("</li>");
423                }
424            }
425            message.append("</ul>")
426                    .append(Bundle.getMessage("VetoWillBeRemovedFrom", getBeanTypeHandled()));
427            if (found) {
428                throw new PropertyVetoException(message.toString(), evt);
429            }
430        } else {
431            for (NamedBean nb : _beans) {
432                // throws PropertyVetoException if vetoed
433                nb.vetoableChange(evt);
434            }
435        }
436    }
437
438    /**
439     * {@inheritDoc}
440     *
441     * @return {@link jmri.Manager.NameValidity#INVALID} if system name does not
442     *         start with
443     *         {@link #getSystemNamePrefix()}; {@link jmri.Manager.NameValidity#VALID_AS_PREFIX_ONLY}
444     *         if system name equals {@link #getSystemNamePrefix()}; otherwise
445     *         {@link jmri.Manager.NameValidity#VALID} to allow Managers that do
446     *         not perform more specific validation to be considered valid.
447     */
448    @Override
449    public NameValidity validSystemNameFormat(@Nonnull String systemName) {
450        if (getSystemNamePrefix().equals(systemName)) {
451            return NameValidity.VALID_AS_PREFIX_ONLY;
452        }
453        return systemName.startsWith(getSystemNamePrefix()) ? NameValidity.VALID : NameValidity.INVALID;
454    }
455
456    /**
457     * {@inheritDoc}
458     *
459     * The implementation in {@link AbstractManager} should be final, but is not
460     * for four managers that have arbitrary prefixes.
461     */
462    @Override
463    @Nonnull
464    public final String getSystemPrefix() {
465        return memo.getSystemPrefix();
466    }
467
468    /**
469     * {@inheritDoc}
470     */
471    @Override
472    @OverridingMethodsMustInvokeSuper
473    public void setPropertyChangesSilenced(@Nonnull String propertyName, boolean silenced) {
474        if (!silenceableProperties.contains(propertyName)) {
475            throw new IllegalArgumentException("Property " + propertyName + " cannot be silenced.");
476        }
477        silencedProperties.put(propertyName, silenced);
478        if (propertyName.equals("beans") && !silenced) {
479            fireIndexedPropertyChange("beans", _beans.size(), null, null);
480        }
481    }
482
483    /** {@inheritDoc} */
484    @Override
485    public void addDataListener(ManagerDataListener<E> e) {
486        if (e != null) listeners.add(e);
487    }
488
489    /** {@inheritDoc} */
490    @Override
491    public void removeDataListener(ManagerDataListener<E> e) {
492        if (e != null) listeners.remove(e);
493    }
494
495    private final List<ManagerDataListener<E>> listeners = new ArrayList<>();
496
497    private boolean muted = false;
498
499    /** {@inheritDoc} */
500    @Override
501    public void setDataListenerMute(boolean m) {
502        if (muted && !m) {
503            // send a total update, as we haven't kept track of specifics
504            ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.CONTENTS_CHANGED, 0, getObjectCount()-1, null);
505            listeners.forEach(listener -> listener.contentsChanged(e));
506        }
507        this.muted = m;
508    }
509
510    protected void fireDataListenersAdded(int start, int end, E changedBean) {
511        if (muted) return;
512        ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.INTERVAL_ADDED, start, end, changedBean);
513        listeners.forEach(m -> m.intervalAdded(e));
514    }
515
516    protected void fireDataListenersRemoved(int start, int end, E changedBean) {
517        if (muted) return;
518        ManagerDataEvent<E> e = new ManagerDataEvent<>(this, ManagerDataEvent.INTERVAL_REMOVED, start, end, changedBean);
519        listeners.forEach(m -> m.intervalRemoved(e));
520    }
521
522    public void updateAutoNumber(String systemName) {
523        /* The following keeps track of the last created auto system name.
524         currently we do not reuse numbers, although there is nothing to stop the
525         user from manually recreating them */
526        String autoPrefix = getSubSystemNamePrefix() + ":AUTO:";
527        if (systemName.startsWith(autoPrefix)) {
528            try {
529                int autoNumber = Integer.parseInt(systemName.substring(autoPrefix.length()));
530                lastAutoNamedBeanRef.accumulateAndGet(autoNumber, Math::max);
531            } catch (NumberFormatException e) {
532                log.warn("Auto generated SystemName {} is not in the correct format", systemName);
533            }
534        }
535    }
536
537    public String getAutoSystemName() {
538        int nextAutoBlockRef = lastAutoNamedBeanRef.incrementAndGet();
539        StringBuilder b = new StringBuilder(getSubSystemNamePrefix() + ":AUTO:");
540        String nextNumber = paddedNumber.format(nextAutoBlockRef);
541        b.append(nextNumber);
542        return b.toString();
543    }
544
545    /**
546     * Create a System Name from hardware address and system letter prefix.
547     * AbstractManager performs no validation.
548     * @param curAddress hardware address, no system prefix or type letter.
549     * @param prefix - just system prefix, not including Type Letter.
550     * @return full system name with system prefix, type letter and hardware address.
551     * @throws JmriException if unable to create a system name.
552     */
553    public String createSystemName(@Nonnull String curAddress, @Nonnull String prefix) throws JmriException {
554        return prefix + typeLetter() + curAddress;
555    }
556
557    /**
558     * checks for numeric-only system names.
559     * @param curAddress the System name ( excluding both prefix and type letter) to check.
560     * @return unchanged if is numeric string.
561     * @throws JmriException if not numeric.
562     */
563    protected String checkNumeric(@Nonnull String curAddress) throws JmriException {
564        try {
565            Integer.parseInt(curAddress);
566        } catch (java.lang.NumberFormatException ex) {
567            throw new JmriException("Hardware Address passed "+curAddress+" should be a number");
568        }
569        return curAddress;
570    }
571
572    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractManager.class);
573
574}