001package jmri.jmrix;
002
003import java.util.Map;
004import java.util.HashMap;
005import javax.annotation.Nonnull;
006
007import jmri.SystemConnectionMemo;
008
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012/**
013 * Interface for classes that wish to get notification when the connection to
014 * the layout changes.
015 * <p>
016 * Maintains a single instance, as there is only one set of connections for the
017 * running program.
018 * <p>
019 * The "system name" referred to here is the human-readable name like "LocoNet 2"
020 * which can be obtained from i.e. 
021 * {@link jmri.SystemConnectionMemo#getUserName}. 
022 * Not clear whether {@link ConnectionConfig#getConnectionName} is correct.
023 * It's not intended to be the prefix from i.e. {@link PortAdapter#getSystemPrefix}.
024 * Maybe the right thing is to pass in the SystemConnectionMemo?
025 *
026 * @author Daniel Boudreau Copyright (C) 2007
027 * @author Paul Bender Copyright (C) 2016
028 */
029public class ConnectionStatus {
030
031    public static final String CONNECTION_UNKNOWN = "Unknown";
032    public static final String CONNECTION_UP = "Connected";
033    public static final String CONNECTION_DOWN = "Not Connected";
034
035    // hashmap of ConnectionKey objects and their status
036    private final HashMap<ConnectionKey, String> portStatus = new HashMap<>();
037
038    /**
039     * Record the single instance *
040     */
041    private static ConnectionStatus _instance = null;
042
043    public static synchronized ConnectionStatus instance() {
044        if (_instance == null) {
045            log.debug("ConnectionStatus creating instance");
046            // create and load
047            _instance = new ConnectionStatus();
048        }
049        // log.debug("ConnectionStatus returns instance {}", _instance);
050        return _instance;
051    }
052
053    /**
054     * Add a connection with a given name and port name to the portStatus set
055     * if not yet present in the set.
056     *
057     * @param systemName human-readable name for system like "LocoNet 2"
058     *                   which can be obtained from i.e. {@link SystemConnectionMemo#getUserName}.
059     * @param portName   the port name
060     */
061    public synchronized void addConnection(String systemName, @Nonnull String portName) {
062        log.debug("addConnection systemName {} portName {}", systemName, portName);
063        ConnectionKey newKey = new ConnectionKey(systemName, portName);
064        if (!portStatus.containsKey(newKey)) {
065            portStatus.put(newKey, CONNECTION_UNKNOWN);
066            firePropertyChange("add", null, portName);
067        }
068    }
069
070    /**
071     * Set the connection state of a communication port.
072     *
073     * @param systemName human-readable name for system like "LocoNet 2"
074     *                      which can be obtained from i.e. {@link SystemConnectionMemo#getUserName}.
075     * @param portName   the port name
076     * @param state      one of ConnectionStatus.UP, ConnectionStatus.DOWN, or
077     *                   ConnectionStatus.UNKNOWN.
078     */
079    public synchronized void setConnectionState(String systemName, @Nonnull String portName, @Nonnull String state) {
080        log.debug("setConnectionState systemName: {} portName: {} state: {}", systemName, portName, state);
081        ConnectionKey newKey = new ConnectionKey(systemName, portName);
082        if (!portStatus.containsKey(newKey)) {
083            portStatus.put(newKey, state);
084            firePropertyChange("add", null, portName);
085            log.debug("New Connection added: {} ", newKey);
086        } else {
087            firePropertyChange("change", portStatus.put(newKey, state), portName);
088        }
089    }
090
091    /**
092     * Get the status of a communication port with a given name.
093     *
094     * @param systemName human-readable name for system like "LocoNet 2"
095     *                      which can be obtained from i.e. {@link SystemConnectionMemo#getUserName}.
096     * @param portName   the port name
097     * @return the status
098     */
099    public synchronized String getConnectionState(String systemName, @Nonnull String portName) {
100        log.debug("getConnectionState systemName: {} portName: {}", systemName, portName);
101        String stateText = CONNECTION_UNKNOWN;
102        ConnectionKey newKey = new ConnectionKey(systemName, portName);
103        if (portStatus.containsKey(newKey)) {
104            stateText = portStatus.get(newKey);
105            log.debug("connection found : {}", stateText);
106        } else {
107            log.debug("connection systemName {} portName {} not found, {}", systemName, portName, stateText);
108        }
109        return stateText;
110    }
111
112    /**
113     * Get the status of a communication port, based on the system name only.
114     *
115     * @param systemName human-readable name for system like "LocoNet 2"
116     *                      which can be obtained from i.e. {@link SystemConnectionMemo#getUserName}.
117     * @return the status
118     */
119    public synchronized String getSystemState(@Nonnull String systemName) {
120        log.debug("getSystemState systemName: {} ", systemName);
121        // see if there is a key that has systemName as the port value.
122        for (Map.Entry<ConnectionKey, String> entry : portStatus.entrySet()) {
123            if ((entry.getKey().getSystemName() != null) && (entry.getKey().getSystemName().equals(systemName))) {
124                // if we find a match, return it
125                return entry.getValue();
126            }
127        }
128        // If we still don't find a match, then we don't know the status
129        log.warn("Didn't find system status for system {}", systemName);
130        return CONNECTION_UNKNOWN;
131    }
132
133    /**
134     * Confirm status of a communication port is not down.
135     *
136     * @param systemName human-readable name for system like "LocoNet 2"
137     *                   which can be obtained from i.e. {@link SystemConnectionMemo#getUserName}.
138     * @param portName   the port name
139     * @return true if port connection is operational or unknown, false if not
140     */
141    public synchronized boolean isConnectionOk(String systemName, @Nonnull String portName) {
142        String stateText = getConnectionState(systemName, portName);
143        return !stateText.equals(CONNECTION_DOWN);
144    }
145
146    /**
147     * Confirm status of a communication port is not down, based on the system name.
148     *
149     * @param systemName human-readable name for system like "LocoNet 2"
150     *                      which can be obtained from i.e. {@link SystemConnectionMemo#getUserName}.
151     * @return true if port connection is operational or unknown, false if not. This includes
152     *                      returning true if the connection is not recognized.
153     */
154    public synchronized boolean isSystemOk(@Nonnull String systemName) {
155        // see if there is a key that has systemName as the port value.
156        for (Map.Entry<ConnectionKey, String> entry : portStatus.entrySet()) {
157            if ((entry.getKey().getSystemName() != null) && (entry.getKey().getSystemName().equals(systemName))) {
158                // if we find a match, return it
159                return !portStatus.get(entry.getKey()).equals(CONNECTION_DOWN);
160            }
161        }
162        // and if we still don't find a match, go ahead and reply true
163        // as we consider the state unknown.
164        return true;
165    }
166
167    java.beans.PropertyChangeSupport pcs = new java.beans.PropertyChangeSupport(this);
168
169    public synchronized void addPropertyChangeListener(java.beans.PropertyChangeListener l) {
170        pcs.addPropertyChangeListener(l);
171    }
172
173    protected void firePropertyChange(@Nonnull String p, Object old, Object n) {
174        log.debug("firePropertyChange {} old: {} new: {}", p, old, n);
175        pcs.firePropertyChange(p, old, n);
176    }
177
178    public synchronized void removePropertyChangeListener(java.beans.PropertyChangeListener l) {
179        pcs.removePropertyChangeListener(l);
180    }
181
182    /**
183     * ConnectionKey is an internal class containing the port name and system
184     * name of a connection.
185     * <p>
186     * ConnectionKey is used as a key in a HashMap of the connections on the
187     * system.
188     * <p>
189     * It is allowable for either the port name or the system name to be null,
190     * but not both.
191     */
192    static private class ConnectionKey {
193
194        String portName = null;
195        String systemName = null;  // human-readable name for system
196
197        /**
198         * constructor
199         *
200         * @param system human-readable name for system like "LocoNet 2"
201         *                      which can be obtained from i.e. {@link SystemConnectionMemo#getUserName}.
202         * @param port   port name
203         * @throws IllegalArgumentException if both system and port are null;
204         */
205        public ConnectionKey(String system, @Nonnull String port) {
206            if (system == null && port == null) {
207                throw new IllegalArgumentException("At least the port name must be provided");
208            }
209            systemName = system;
210            portName = port;
211        }
212
213        public String getSystemName() {
214            return systemName;
215        }
216
217        public String getPortName() {
218            return portName;
219        }
220
221        @Override
222        public boolean equals(Object o) {
223            if (o == null || !(o instanceof ConnectionKey)) {
224                return false;
225            }
226            ConnectionKey other = (ConnectionKey) o;
227
228            return (systemName == null ? other.getSystemName() == null : systemName.equals(other.getSystemName()))
229                    && (portName == null ? other.getPortName() == null : portName.equals(other.getPortName()));
230        }
231
232        @Override
233        public int hashCode() {
234            if (systemName == null) {
235                return portName.hashCode();
236            } else if (portName == null) {
237                return systemName.hashCode();
238            } else {
239                return (systemName.hashCode() + portName.hashCode());
240            }
241        }
242
243    }
244
245    private final static Logger log = LoggerFactory.getLogger(ConnectionStatus.class);
246
247}