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