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