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    // Used by ConnectionStatusTest
054    static synchronized void clearInstance() {
055        _instance = null;
056    }
057
058    private ConnectionStatus() {
059        // Private constructor to protect singleton
060    }
061
062    /**
063     * Add a connection with a given name and port name to the portStatus set
064     * if not yet present in the set.
065     *
066     * @param systemName human-readable name for system like "LocoNet 2"
067     *                   which can be obtained from i.e. {@link SystemConnectionMemo#getUserName}.
068     * @param portName   the port name
069     */
070    public synchronized void addConnection(String systemName, @Nonnull String portName) {
071        log.debug("addConnection systemName {} portName {}", systemName, portName);
072        ConnectionKey newKey = new ConnectionKey(systemName, portName);
073        if (!portStatus.containsKey(newKey)) {
074            portStatus.put(newKey, CONNECTION_UNKNOWN);
075            firePropertyChange("add", null, portName);
076        }
077    }
078
079    /**
080     * Set the connection state of a communication port.
081     *
082     * @param systemName human-readable name for system like "LocoNet 2"
083     *                      which can be obtained from i.e. {@link SystemConnectionMemo#getUserName}.
084     * @param portName   the port name
085     * @param state      one of ConnectionStatus.UP, ConnectionStatus.DOWN, or
086     *                   ConnectionStatus.UNKNOWN.
087     */
088    public synchronized void setConnectionState(String systemName, @Nonnull String portName, @Nonnull String state) {
089        log.debug("setConnectionState systemName: {} portName: {} state: {}", systemName, portName, state);
090        ConnectionKey newKey = new ConnectionKey(systemName, portName);
091        if (!portStatus.containsKey(newKey)) {
092            portStatus.put(newKey, state);
093            firePropertyChange("add", null, portName);
094            log.debug("New Connection added: {} ", newKey);
095        } else {
096            firePropertyChange("change", portStatus.put(newKey, state), portName);
097        }
098    }
099
100    /**
101     * Get the status of a communication port with a given name.
102     *
103     * @param systemName human-readable name for system like "LocoNet 2"
104     *                      which can be obtained from i.e. {@link SystemConnectionMemo#getUserName}.
105     * @param portName   the port name
106     * @return the status
107     */
108    public synchronized String getConnectionState(String systemName, @Nonnull String portName) {
109        log.debug("getConnectionState systemName: {} portName: {}", systemName, portName);
110        String stateText = CONNECTION_UNKNOWN;
111        ConnectionKey newKey = new ConnectionKey(systemName, portName);
112        if (portStatus.containsKey(newKey)) {
113            stateText = portStatus.get(newKey);
114            log.debug("connection found : {}", stateText);
115        } else {
116            log.debug("connection systemName {} portName {} not found, {}", systemName, portName, stateText);
117        }
118        return stateText;
119    }
120
121    /**
122     * Get the status of a communication port, based on the system name only.
123     *
124     * @param systemName human-readable name for system like "LocoNet 2"
125     *                      which can be obtained from i.e. {@link SystemConnectionMemo#getUserName}.
126     * @return the status
127     */
128    public synchronized String getSystemState(@Nonnull String systemName) {
129        log.debug("getSystemState systemName: {} ", systemName);
130        // see if there is a key that has systemName as the port value.
131        for (Map.Entry<ConnectionKey, String> entry : portStatus.entrySet()) {
132            if ((entry.getKey().getSystemName() != null) && (entry.getKey().getSystemName().equals(systemName))) {
133                // if we find a match, return it
134                return entry.getValue();
135            }
136        }
137        // If we still don't find a match, then we don't know the status
138        log.warn("Didn't find system status for system {}", systemName);
139        return CONNECTION_UNKNOWN;
140    }
141
142    /**
143     * Confirm status of a communication port is not down.
144     *
145     * @param systemName human-readable name for system like "LocoNet 2"
146     *                   which can be obtained from i.e. {@link SystemConnectionMemo#getUserName}.
147     * @param portName   the port name
148     * @return true if port connection is operational or unknown, false if not
149     */
150    public synchronized boolean isConnectionOk(String systemName, @Nonnull String portName) {
151        String stateText = getConnectionState(systemName, portName);
152        return !stateText.equals(CONNECTION_DOWN);
153    }
154
155    /**
156     * Confirm status of a communication port is not down, based on the system name.
157     *
158     * @param systemName human-readable name for system like "LocoNet 2"
159     *                      which can be obtained from i.e. {@link SystemConnectionMemo#getUserName}.
160     * @return true if port connection is operational or unknown, false if not. This includes
161     *                      returning true if the connection is not recognized.
162     */
163    public synchronized boolean isSystemOk(@Nonnull String systemName) {
164        // see if there is a key that has systemName as the port value.
165        for (Map.Entry<ConnectionKey, String> entry : portStatus.entrySet()) {
166            if ((entry.getKey().getSystemName() != null) && (entry.getKey().getSystemName().equals(systemName))) {
167                // if we find a match, return it
168                return !portStatus.get(entry.getKey()).equals(CONNECTION_DOWN);
169            }
170        }
171        // and if we still don't find a match, go ahead and reply true
172        // as we consider the state unknown.
173        return true;
174    }
175
176    java.beans.PropertyChangeSupport pcs = new java.beans.PropertyChangeSupport(this);
177
178    public synchronized void addPropertyChangeListener(java.beans.PropertyChangeListener l) {
179        pcs.addPropertyChangeListener(l);
180    }
181
182    protected void firePropertyChange(@Nonnull String p, Object old, Object n) {
183        log.debug("firePropertyChange {} old: {} new: {}", p, old, n);
184        pcs.firePropertyChange(p, old, n);
185    }
186
187    public synchronized void removePropertyChangeListener(java.beans.PropertyChangeListener l) {
188        pcs.removePropertyChangeListener(l);
189    }
190
191    /**
192     * ConnectionKey is an internal class containing the port name and system
193     * name of a connection.
194     * <p>
195     * ConnectionKey is used as a key in a HashMap of the connections on the
196     * system.
197     * <p>
198     * It is allowable for either the port name or the system name to be null,
199     * but not both.
200     */
201    static private class ConnectionKey {
202
203        String portName = null;
204        String systemName = null;  // human-readable name for system
205
206        /**
207         * constructor
208         *
209         * @param system human-readable name for system like "LocoNet 2"
210         *                      which can be obtained from i.e. {@link SystemConnectionMemo#getUserName}.
211         * @param port   port name
212         * @throws IllegalArgumentException if both system and port are null;
213         */
214        public ConnectionKey(String system, @Nonnull String port) {
215            if (system == null && port == null) {
216                throw new IllegalArgumentException("At least the port name must be provided");
217            }
218            systemName = system;
219            portName = port;
220        }
221
222        public String getSystemName() {
223            return systemName;
224        }
225
226        public String getPortName() {
227            return portName;
228        }
229
230        @Override
231        public boolean equals(Object o) {
232            if (o == null || !(o instanceof ConnectionKey)) {
233                return false;
234            }
235            ConnectionKey other = (ConnectionKey) o;
236
237            return (systemName == null ? other.getSystemName() == null : systemName.equals(other.getSystemName()))
238                    && (portName == null ? other.getPortName() == null : portName.equals(other.getPortName()));
239        }
240
241        @Override
242        public int hashCode() {
243            if (systemName == null) {
244                return portName.hashCode();
245            } else if (portName == null) {
246                return systemName.hashCode();
247            } else {
248                return (systemName.hashCode() + portName.hashCode());
249            }
250        }
251
252    }
253
254    private final static Logger log = LoggerFactory.getLogger(ConnectionStatus.class);
255
256}