001package jmri.jmrix;
002
003import java.io.DataInputStream;
004import java.io.DataOutputStream;
005import java.io.IOException;
006import java.net.Socket;
007
008import jmri.SystemConnectionMemo;
009
010/**
011 * Enables basic setup of a network client interface for a jmrix implementation.
012 *
013 * @author Kevin Dickerson Copyright (C) 2010
014 * @author Based upon work originally done by Paul Bender Copyright (C) 2009
015 * @see jmri.jmrix.NetworkConfigException
016 */
017abstract public class AbstractNetworkPortController extends AbstractPortController implements NetworkPortAdapter {
018
019    // the host name and port number identify what we are talking to.
020    protected String m_HostName = null;
021    private String m_HostAddress = null;  // Internal IP address for  ZeroConf
022    // configured clients.
023    protected int m_port = 0;
024    // keep the socket provides our connection.
025    protected Socket socketConn = null;
026    protected int connTimeout = 0; // connection timeout for read operations.
027    // Default is 0, an infinite timeout.
028
029    protected AbstractNetworkPortController(SystemConnectionMemo connectionMemo) {
030        super(connectionMemo);
031        setHostName(""); // give the host name a default value of the empty string.
032    }
033
034    @Override
035    public void connect(String host, int port) throws IOException {
036        setHostName(host);
037        setPort(port);
038        connect();
039    }
040
041    @Override
042    public void connect() throws IOException {
043        log.debug("connect() starts to {}:{}", getHostName(), getPort());
044        opened = false;
045        if (getHostAddress() == null || m_port == 0) {
046            log.error("No host name or port set: {}:{}", m_HostName, m_port);
047            return;
048        }
049        try {
050            var address = getHostAddress();
051            log.info("Attempting to open connecton to {}:{}", address, m_port);
052            socketConn = new Socket(address, m_port);
053            socketConn.setKeepAlive(true);
054            socketConn.setSoTimeout(getConnectionTimeout());
055            opened = true;
056        } catch (IOException e) {
057            log.error("Error opening network connection to {} because {}", getHostName(), e.getMessage()); // nothing to help user in full exception
058            if (m_port != 0) {
059                ConnectionStatus.instance().setConnectionState(
060                        getUserName(), m_HostName + ":" + m_port, ConnectionStatus.CONNECTION_DOWN);
061            } else {
062                ConnectionStatus.instance().setConnectionState(
063                        getUserName(), m_HostName, ConnectionStatus.CONNECTION_DOWN);
064            }
065            throw (e);
066        }
067        if (opened && m_port != 0) {
068            ConnectionStatus.instance().setConnectionState(
069                    getUserName(), m_HostName + ":" + m_port, ConnectionStatus.CONNECTION_UP);
070        } else if (opened) {
071            ConnectionStatus.instance().setConnectionState(
072                    getUserName(), m_HostName, ConnectionStatus.CONNECTION_UP);
073        }
074        log.trace("connect ends");
075    }
076
077    /**
078     * Remember the associated host name.
079     *
080     * @param s the host name; if empty will use MDNS to get host name
081     */
082    @Override
083    public void setHostName(String s) {
084        log.trace("setHostName({})", s, new Exception("traceback only"));
085        m_HostName = s;
086        if ((s == null || s.equals("")) && !getMdnsConfigure()) {
087            m_HostName = JmrixConfigPane.NONE;
088        }
089    }
090
091    @Override
092    public String getHostName() {
093        final String envVar = "JMRI_HOSTNAME";
094        String fromEnv = System.getenv(envVar);
095        log.debug("Environment {} {} was {}", envVar, fromEnv, m_HostName);
096        String fromProp = System.getProperty(envVar);
097        log.debug("Property {} {} was {}", envVar, fromProp, m_HostName);
098        if (fromEnv != null ) {
099            jmri.util.LoggingUtil.infoOnce(log,"{} set, using environment \"{}\" as Host Name", envVar, fromEnv);
100            return fromEnv;
101        }
102        if (fromProp != null ) {
103            jmri.util.LoggingUtil.infoOnce(log,"{} set, using property \"{}\" as Host Name", envVar, fromProp);
104            return fromProp;
105        }
106        return m_HostName;
107    }
108
109    /**
110     * Remember the associated IP Address This is used internally for mDNS
111     * configuration. Public access to the IP address is through the hostname
112     * field.
113     *
114     * @param s the address; if empty, will use the host name
115     */
116    protected void setHostAddress(String s) {
117        log.trace("setHostAddress({})", s);
118        m_HostAddress = s;
119        if (s == null || s.equals("")) {
120            m_HostAddress = m_HostName;
121        }
122    }
123
124    protected String getHostAddress() {
125        if (m_HostAddress == null) {
126            return getHostName();
127        }
128        log.info("getHostAddress is {}", m_HostAddress);
129        return m_HostAddress;
130    }
131
132    /**
133     * Remember the associated port number.
134     *
135     * @param p the port
136     */
137    @Override
138    public void setPort(int p) {
139        log.trace("setPort(int {})", p);
140        m_port = p;
141    }
142
143    @Override
144    public void setPort(String p) {
145        log.trace("setPort(String {})", p);
146        m_port = Integer.parseInt(p);
147    }
148
149    @Override
150    public int getPort() {
151        return m_port;
152    }
153
154    /**
155     * Return the connection name for the network connection in the format of
156     * ip_address:port
157     *
158     * @return ip_address:port
159     */
160    @Override
161    public String getCurrentPortName() {
162        String t;
163        if (getMdnsConfigure()) {
164            t = getHostAddress();
165        } else {
166            t = getHostName();
167        }
168        int p = getPort();
169        if (t != null && !t.equals("")) {
170            if (p != 0) {
171                return t + ":" + p;
172            }
173            return t;
174        } else {
175            return JmrixConfigPane.NONE;
176        }
177    }
178
179    /*
180     * Set whether or not this adapter should be
181     * configured automatically via MDNS.
182     * Note: Default implementation ignores the parameter.
183     *
184     * @param autoconfig boolean value
185     */
186    @Override
187    public void setMdnsConfigure(boolean autoconfig) {
188    }
189
190    /*
191     * Get whether or not this adapter is configured
192     * to use autoconfiguration via MDNS
193     * Default implemntation always returns false.
194     *
195     * @return true if configured using MDNS
196     */
197    @Override
198    public boolean getMdnsConfigure() {
199        return false;
200    }
201
202    /*
203     * Set the server's host name and port
204     * using MDNS autoconfiguration.
205     * Default implementation does nothing.
206     */
207    @Override
208    public void autoConfigure() {
209    }
210
211    /*
212     * Get and set the ZeroConf/mDNS advertisement name.
213     * Default implementation does nothing.
214     */
215    @Override
216    public void setAdvertisementName(String AdName) {
217    }
218
219    @Override
220    public String getAdvertisementName() {
221        return null;
222    }
223
224    /*
225     * Get and set the ZeroConf/mDNS service type.
226     * Default implementation does nothing.
227     */
228    @Override
229    public void setServiceType(String ServiceType) {
230    }
231
232    @Override
233    public String getServiceType() {
234        return null;
235    }
236
237    /**
238     * {@inheritDoc}
239     */
240    @Override
241    public DataInputStream getInputStream() {
242        log.trace("getInputStream() starts");
243        if (socketConn == null) {
244            log.error("getInputStream invoked with null socketConn");
245        }
246        if (!opened) {
247            log.error("getInputStream called before load(), stream not available");
248            if (m_port != 0) {
249                ConnectionStatus.instance().setConnectionState(
250                        getUserName(), m_HostName + ":" + m_port, ConnectionStatus.CONNECTION_DOWN);
251            } else {
252                ConnectionStatus.instance().setConnectionState(
253                        getUserName(), m_HostName, ConnectionStatus.CONNECTION_DOWN);
254            }
255        }
256        try {
257            log.trace("getInputStream() returns normally");
258            return new DataInputStream(socketConn.getInputStream());
259        } catch (java.io.IOException ex1) {
260            log.error("Exception getting input stream:", ex1);
261            return null;
262        }
263    }
264
265    /**
266     * {@inheritDoc}
267     */
268    @Override
269    public DataOutputStream getOutputStream() {
270        if (!opened) {
271            log.error("getOutputStream called before load(), stream not available");
272        }
273        try {
274            return new DataOutputStream(socketConn.getOutputStream());
275        } catch (java.io.IOException e) {
276            log.error("getOutputStream exception:", e);
277            if (m_port != 0) {
278                ConnectionStatus.instance().setConnectionState(
279                        getUserName(), m_HostName + ":" + m_port, ConnectionStatus.CONNECTION_DOWN);
280            } else {
281                ConnectionStatus.instance().setConnectionState(
282                        getUserName(), m_HostName, ConnectionStatus.CONNECTION_DOWN);
283            }
284        }
285        return null;
286    }
287    
288    /**
289     * {@inheritDoc}
290     */
291    @Override
292    protected void closeConnection(){
293        try {
294            socketConn.close();
295        } catch (IOException e) {
296            log.trace("Unable to close socket", e);
297        }
298        opened=false;
299    }
300
301    /**
302     * Customizable method to deal with resetting a system connection after a
303     * successful recovery of a connection.
304     */
305    @Override
306    protected void resetupConnection() {
307    }
308    
309    /**
310     * {@inheritDoc}
311     */
312    @Override
313    protected void reconnectFromLoop(int retryNum){
314        try {
315            // if the device allows autoConfiguration,
316            // we need to run the autoConfigure() call
317            // before we try to reconnect.
318            if (getMdnsConfigure()) {
319                autoConfigure();
320            }
321            connect();
322        } catch (IOException ex) {
323            log.trace("restart failed", ex); // main warning to log.error done within connect();
324            // if returned on exception stops thread and connection attempts
325        }
326    }
327
328    /*
329     * Set the connection timeout to the specified value.
330     * If the socket is not null, set the SO_TIMEOUT option on the
331     * socket as well.
332     *
333     * @param t timeout value in milliseconds
334     */
335    protected void setConnectionTimeout(int t) {
336        connTimeout = t;
337        try {
338            if (socketConn != null) {
339                socketConn.setSoTimeout(getConnectionTimeout());
340            }
341        } catch (java.net.SocketException se) {
342            log.debug("Unable to set socket timeout option on socket");
343        }
344    }
345
346    /*
347     * Get the connection timeout value.
348     *
349     * @return timeout value in milliseconds
350     */
351    protected int getConnectionTimeout() {
352        return connTimeout;
353    }
354
355    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractNetworkPortController.class);
356
357}