001package jmri.jmrix;
002
003import java.util.*;
004
005import javax.annotation.Nonnull;
006import javax.annotation.OverridingMethodsMustInvokeSuper;
007
008import jmri.*;
009import jmri.beans.Bean;
010import jmri.implementation.DccConsistManager;
011import jmri.implementation.NmraConsistManager;
012import jmri.util.NamedBeanComparator;
013import jmri.util.startup.StartupActionFactory;
014
015/**
016 * Lightweight abstract class to denote that a system is active, and provide
017 * general information.
018 * <p>
019 * Objects of specific subtypes of this are registered in the
020 * {@link InstanceManager} to activate their particular system.
021 *
022 * @author Bob Jacobsen Copyright (C) 2010
023 */
024public abstract class DefaultSystemConnectionMemo extends Bean implements SystemConnectionMemo, Disposable {
025
026    private boolean disabled = false;
027    private Boolean disabledAsLoaded = null; // Boolean can be true, false, or null
028    private String prefix;
029    private String prefixAsLoaded;
030    private String userName;
031    private String userNameAsLoaded;
032    protected Map<Class<?>,Object> classObjectMap;
033
034    protected DefaultSystemConnectionMemo(@Nonnull String prefix, @Nonnull String userName) {
035        classObjectMap = new HashMap<>();
036        if (this instanceof CaptiveSystemConnectionMemo) {
037            this.prefix = prefix;
038            this.userName = userName;
039            return;
040        }
041        log.debug("SystemConnectionMemo created for prefix \"{}\" user name \"{}\"", prefix, userName);
042        if (!setSystemPrefix(prefix)) {
043            int x = 2;
044            while (!setSystemPrefix(prefix + x)) {
045                x++;
046            }
047            log.debug("created system prefix {}{}", prefix, x);
048        }
049
050        if (!setUserName(userName)) {
051            int x = 2;
052            while (!setUserName(userName + x)) {
053                x++;
054            }
055            log.debug("created user name {}{}", prefix, x);
056        }
057        // reset to null so these get set by the first setPrefix/setUserName
058        // call after construction
059        this.prefixAsLoaded = null;
060        this.userNameAsLoaded = null;
061    }
062
063    /**
064     * Register with the SystemConnectionMemoManager and InstanceManager with proper
065     * ID for later retrieval as a generic system.
066     * <p>
067     * This operation should occur only when the SystemConnectionMemo is ready for use.
068     */
069    @Override
070    public void register() {
071        log.debug("register as SystemConnectionMemo, really of type {}", this.getClass());
072        SystemConnectionMemoManager.getDefault().register(this);
073    }
074
075    /**
076     * Provide access to the system prefix string.
077     * <p>
078     * This was previously called the "System letter".
079     *
080     * @return System prefix
081     */
082    @Override
083    public String getSystemPrefix() {
084        return prefix;
085    }
086
087    /**
088     * Set the system prefix.
089     *
090     * @param systemPrefix prefix to use for this system connection
091     * @throws java.lang.NullPointerException if systemPrefix is null
092     * @return true if the system prefix could be set
093     */
094    @Override
095    public final boolean setSystemPrefix(@Nonnull String systemPrefix) {
096        Objects.requireNonNull(systemPrefix);
097        // return true if systemPrefix is not being changed
098        if (systemPrefix.equals(prefix)) {
099            if (this.prefixAsLoaded == null) {
100                this.prefixAsLoaded = systemPrefix;
101            }
102            return true;
103        }
104        String oldPrefix = prefix;
105        if (SystemConnectionMemoManager.getDefault().isSystemPrefixAvailable(systemPrefix)) {
106            prefix = systemPrefix;
107            if (this.prefixAsLoaded == null) {
108                this.prefixAsLoaded = systemPrefix;
109            }
110            this.propertyChangeSupport.firePropertyChange(SYSTEM_PREFIX, oldPrefix, systemPrefix);
111            return true;
112        }
113        log.debug("setSystemPrefix false for \"{}\"", systemPrefix);
114        return false;
115    }
116
117    /**
118     * Provide access to the system user name string.
119     * <p>
120     * This was previously fixed at configuration time.
121     *
122     * @return User name of the connection
123     */
124    @Override
125    public String getUserName() {
126        return userName;
127    }
128
129    /**
130     * Set the user name for the system connection.
131     *
132     * @param userName user name to use for this system connection
133     * @throws java.lang.NullPointerException if name is null
134     * @return true if the user name could be set.
135     */
136    @Override
137    public final boolean setUserName(@Nonnull String userName) {
138        Objects.requireNonNull(userName);
139        if (userName.equals(this.userName)) {
140            if (this.userNameAsLoaded == null) {
141                this.userNameAsLoaded = userName;
142            }
143            return true;
144        }
145        String oldUserName = this.userName;
146        if (SystemConnectionMemoManager.getDefault().isUserNameAvailable(userName)) {
147            this.userName = userName;
148            if (this.userNameAsLoaded == null) {
149                this.userNameAsLoaded = userName;
150            }
151            this.propertyChangeSupport.firePropertyChange(USER_NAME, oldUserName, userName);
152            return true;
153        }
154        return false;
155    }
156
157    /**
158     * Check if this connection provides a specific manager type. This method
159     * <strong>must</strong> return false if a manager for the specific type is
160     * not provided, and <strong>must</strong> return true if a manager for the
161     * specific type is provided.
162     *
163     * @param c The class type for the manager to be provided
164     * @return true if the specified manager is provided
165     * @see #get(java.lang.Class)
166     */
167    @OverridingMethodsMustInvokeSuper
168    @Override
169    public boolean provides(Class<?> c) {
170        if (disabled) {
171            return false;
172        }
173        if (c.equals(jmri.ConsistManager.class)) {
174            return classObjectMap.get(c) != null || provides(CommandStation.class) || provides(AddressedProgrammerManager.class);
175        } else {
176            return classObjectMap.containsKey(c);
177        }
178    }
179
180    /**
181     * Get a manager for a specific type. This method <strong>must</strong>
182     * return a non-null value if {@link #provides(java.lang.Class)} is true for
183     * the type, and <strong>must</strong> return null if provides() is false
184     * for the type.
185     *
186     * @param <T>  Type of manager to get
187     * @param type Type of manager to get
188     * @return The manager or null if provides() is false for T
189     * @see #provides(java.lang.Class)
190     */
191    @OverridingMethodsMustInvokeSuper
192    @SuppressWarnings("unchecked") // dynamic checking done on cast of getConsistManager
193    @Override
194    public <T> T get(Class<T> type) {
195        if (disabled) {
196            return null;
197        }
198        if (type.equals(ConsistManager.class)) {
199            return (T) getConsistManager();
200        } else {
201            return (T) classObjectMap.get(type); // nothing, by default
202        }
203    }
204
205    /**
206     * Dispose of System Connection.
207     * <p>
208     * Removes objects from classObjectMap after
209     * calling dispose if Disposable.
210     * Removes these objects from InstanceManager.
211     */
212    @Override
213    public void dispose() {
214        Set<Class<?>> keySet = new HashSet<>(classObjectMap.keySet());
215        keySet.forEach(this::removeRegisteredObject);
216        SystemConnectionMemoManager.getDefault().deregister(this);
217    }
218
219    /**
220     * Remove single class object.
221     * Removes from InstanceManager
222     * Removes from Memo class list
223     * Call object dispose if class implements Disposable
224     * @param <T> class Type
225     * @param c actual class
226     */
227    private <T> void removeRegisteredObject(Class<T> c) {
228        T object = get(c);
229        if (object != null) {
230            InstanceManager.deregister(object, c);
231            deregister(object, c);
232            disposeIfPossible(c, object);
233        }
234    }
235
236    private <T> void disposeIfPossible(Class<T> c, T object) {
237        if(object instanceof Disposable) {
238            try {
239                ((Disposable)object).dispose();
240            } catch (Exception e) {
241                log.warn("Exception while disposing object of type {} in memo of type {}.", c.getName(), this.getClass().getName(), e);
242            }
243        }
244    }
245
246    /**
247     * Get if the System Connection is currently Disabled.
248     *
249     * @return true if Disabled, else false.
250     */
251    @Override
252    public boolean getDisabled() {
253        return disabled;
254    }
255
256    /**
257     * Set if the System Connection is currently Disabled.
258     * <p>
259     * disabledAsLoaded is only set once.
260     * Sends PropertyChange on change of disabled status.
261     *
262     * @param disabled true to disable, false to enable.
263     */
264    @Override
265    public void setDisabled(boolean disabled) {
266        if (this.disabledAsLoaded == null) {
267            // only set first time
268            this.disabledAsLoaded = disabled;
269        }
270        if (disabled != this.disabled) {
271            boolean oldDisabled = this.disabled;
272            this.disabled = disabled;
273            this.propertyChangeSupport.firePropertyChange(DISABLED, oldDisabled, disabled);
274        }
275    }
276
277    /**
278     * Get the Comparator to be used for two NamedBeans. This is typically an
279     * {@link NamedBeanComparator}, but may be any Comparator that works for
280     * this connection type.
281     *
282     * @param <B>  the type of NamedBean
283     * @param type the class of NamedBean
284     * @return the Comparator
285     */
286    @Override
287    public abstract <B extends NamedBean> Comparator<B> getNamedBeanComparator(Class<B> type);
288
289    /**
290     * Provide a factory for getting startup actions.
291     * <p>
292     * This is a bound, read-only, property under the name "actionFactory".
293     *
294     * @return the factory
295     */
296    @Nonnull
297    @Override
298    public StartupActionFactory getActionFactory() {
299        return new ResourceBundleStartupActionFactory(getActionModelResourceBundle());
300    }
301
302    protected abstract ResourceBundle getActionModelResourceBundle();
303
304    /**
305     * Get if connection is dirty.
306     * Checked fields are disabled, prefix, userName
307     *
308     * @return true if changed since loaded
309     */
310    @Override
311    public boolean isDirty() {
312        return ((this.disabledAsLoaded == null || this.disabledAsLoaded != this.disabled)
313                || (this.prefixAsLoaded == null || !this.prefixAsLoaded.equals(this.prefix))
314                || (this.userNameAsLoaded == null || !this.userNameAsLoaded.equals(this.userName)));
315    }
316
317    @Override
318    public boolean isRestartRequired() {
319        return this.isDirty();
320    }
321
322    /**
323     * Provide access to the ConsistManager for this particular connection.
324     *
325     * @return the provided ConsistManager or null if the connection does not
326     *         provide a ConsistManager
327     */
328    public ConsistManager getConsistManager() {
329        return (ConsistManager) classObjectMap.computeIfAbsent(ConsistManager.class,(Class<?> c) -> { return generateDefaultConsistManagerForConnection(); });
330    }
331
332    private ConsistManager generateDefaultConsistManagerForConnection(){
333        if (provides(jmri.CommandStation.class)) {
334            return new NmraConsistManager(get(jmri.CommandStation.class));
335        } else if (provides(jmri.AddressedProgrammerManager.class)) {
336            return new DccConsistManager(get(jmri.AddressedProgrammerManager.class));
337        }
338        return null;
339    }
340
341    public void setConsistManager(@Nonnull ConsistManager c) {
342        store(c, ConsistManager.class);
343        jmri.InstanceManager.store(c, ConsistManager.class);
344    }
345
346    /**
347     * Store a class object to the system connection memo.
348     * <p>
349     * Does NOT register the class with InstanceManager.
350     * <p>
351     * On memo dispose, each class will be removed from InstanceManager,
352     * and if the class implements disposable, the dispose method is called.
353     * @param <T> Class type obtained from item object.
354     * @param item the class object to store, eg. mySensorManager
355     * @param type Class type, eg. SensorManager.class
356     */
357    public <T> void store(@Nonnull T item, @Nonnull Class<T> type){
358        Map<Class<?>,Object> classObjectMapCopy = classObjectMap;
359        classObjectMap.put(type,item);
360        if ( !classObjectMapCopy.containsValue(item) ) {
361            propertyChangeSupport.firePropertyChange(STORE, null, item);
362        }
363    }
364
365    /**
366     * Remove a class object from the system connection memo classObjectMap.
367     * <p>
368     * Does NOT remove the class from InstanceManager.
369     *
370     * @param <T> Class type obtained from item object.
371     * @param item the class object to store, eg. mySensorManager
372     * @param type Class type, eg. SensorManager.class
373     */
374    public <T> void deregister(@Nonnull T item, @Nonnull Class<T> type){
375        Map<Class<?>,Object> classObjectMapCopy = classObjectMap;
376        classObjectMap.remove(type,item);
377        if ( classObjectMapCopy.containsValue(item) ) {
378            propertyChangeSupport.firePropertyChange(DEREGISTER, item, null);
379        }
380    }
381
382    /**
383     * Duration in milliseconds of interval between separate Turnout commands on the same connection.
384     * <p>
385     * Change from e.g. connection config dialog and scripts using {@link #setOutputInterval(int)}
386     */
387    private int _interval = getDefaultOutputInterval();
388
389    /**
390     * Default interval 250ms.
391     * {@inheritDoc}
392     */
393    @Override
394    public int getDefaultOutputInterval(){
395        return 250;
396    }
397
398    /**
399     * Get the connection specific OutputInterval (in ms) to wait between/before commands
400     * are sent, configured in AdapterConfig.
401     * Used in {@link jmri.implementation.AbstractTurnout#setCommandedStateAtInterval(int)}.
402     */
403    @Override
404    public int getOutputInterval() {
405        log.debug("Getting interval {}", _interval);
406        return _interval;
407    }
408
409    @Override
410    public void setOutputInterval(int newInterval) {
411        log.debug("Setting interval from {} to {}", _interval, newInterval);
412        this.propertyChangeSupport.firePropertyChange(INTERVAL, _interval, newInterval);
413        _interval = newInterval;
414    }
415
416    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultSystemConnectionMemo.class);
417
418}