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 connection 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            ConnectionStatus.instance().setConnectionState(
059                    getSystemConnectionMemo(), ConnectionStatus.CONNECTION_DOWN);
060            throw (e);
061        }
062        if (opened) {
063            ConnectionStatus.instance().setConnectionState(
064                    getSystemConnectionMemo(), ConnectionStatus.CONNECTION_UP);
065        }
066        log.trace("connect ends");
067    }
068
069    /**
070     * Remember the associated host name.
071     *
072     * @param s the host name; if empty will use MDNS to get host name
073     */
074    @Override
075    public void setHostName(String s) {
076        log.trace("setHostName({})", s, new Exception("traceback only"));
077        m_HostName = s;
078        if ((s == null || s.equals("")) && !getMdnsConfigure()) {
079            m_HostName = JmrixConfigPane.NONE;
080        }
081    }
082
083    @Override
084    public String getHostName() {
085        final String envVar = "JMRI_HOSTNAME";
086        String fromEnv = System.getenv(envVar);
087        log.debug("Environment {} {} was {}", envVar, fromEnv, m_HostName);
088        String fromProp = System.getProperty(envVar);
089        log.debug("Property {} {} was {}", envVar, fromProp, m_HostName);
090        if (fromEnv != null ) {
091            jmri.util.LoggingUtil.infoOnce(log,"{} set, using environment \"{}\" as Host Name", envVar, fromEnv);
092            return fromEnv;
093        }
094        if (fromProp != null ) {
095            jmri.util.LoggingUtil.infoOnce(log,"{} set, using property \"{}\" as Host Name", envVar, fromProp);
096            return fromProp;
097        }
098        return m_HostName;
099    }
100
101    /**
102     * Remember the associated IP Address This is used internally for mDNS
103     * configuration. Public access to the IP address is through the hostname
104     * field.
105     *
106     * @param s the address; if empty, will use the host name
107     */
108    protected void setHostAddress(String s) {
109        log.trace("setHostAddress({})", s);
110        m_HostAddress = s;
111        if (s == null || s.equals("")) {
112            m_HostAddress = m_HostName;
113        }
114    }
115
116    protected String getHostAddress() {
117        if (m_HostAddress == null) {
118            return getHostName();
119        }
120        log.info("getHostAddress is {}", m_HostAddress);
121        return m_HostAddress;
122    }
123
124    /**
125     * Remember the associated port number.
126     *
127     * @param p the port
128     */
129    @Override
130    public void setPort(int p) {
131        log.trace("setPort(int {})", p);
132        m_port = p;
133    }
134
135    @Override
136    public void setPort(String p) {
137        log.trace("setPort(String {})", p);
138        m_port = Integer.parseInt(p);
139    }
140
141    @Override
142    public int getPort() {
143        return m_port;
144    }
145
146    /**
147     * Return the connection name for the network connection in the format of
148     * ip_address:port
149     *
150     * @return ip_address:port
151     */
152    @Override
153    public String getCurrentPortName() {
154        String t;
155        if (getMdnsConfigure()) {
156            t = getHostAddress();
157        } else {
158            t = getHostName();
159        }
160        int p = getPort();
161        if (t != null && !t.equals("")) {
162            if (p != 0) {
163                return t + ":" + p;
164            }
165            return t;
166        } else {
167            return JmrixConfigPane.NONE;
168        }
169    }
170
171    /*
172     * Set whether or not this adapter should be
173     * configured automatically via MDNS.
174     * Note: Default implementation ignores the parameter.
175     *
176     * @param autoconfig boolean value
177     */
178    @Override
179    public void setMdnsConfigure(boolean autoconfig) {
180    }
181
182    /*
183     * Get whether or not this adapter is configured
184     * to use autoconfiguration via MDNS
185     * Default implemntation always returns false.
186     *
187     * @return true if configured using MDNS
188     */
189    @Override
190    public boolean getMdnsConfigure() {
191        return false;
192    }
193
194    /*
195     * Set the server's host name and port
196     * using MDNS autoconfiguration.
197     * Default implementation does nothing.
198     */
199    @Override
200    public void autoConfigure() {
201    }
202
203    /*
204     * Get and set the ZeroConf/mDNS advertisement name.
205     * Default implementation does nothing.
206     */
207    @Override
208    public void setAdvertisementName(String AdName) {
209    }
210
211    @Override
212    public String getAdvertisementName() {
213        return null;
214    }
215
216    /*
217     * Get and set the ZeroConf/mDNS service type.
218     * Default implementation does nothing.
219     */
220    @Override
221    public void setServiceType(String ServiceType) {
222    }
223
224    @Override
225    public String getServiceType() {
226        return null;
227    }
228
229    /**
230     * {@inheritDoc}
231     */
232    @Override
233    public DataInputStream getInputStream() {
234        log.trace("getInputStream() starts");
235        if (socketConn == null) {
236            log.error("getInputStream invoked with null socketConn");
237        }
238        if (!opened) {
239            log.error("getInputStream called before load(), stream not available");
240            ConnectionStatus.instance().setConnectionState(
241                    getSystemConnectionMemo(), ConnectionStatus.CONNECTION_DOWN);
242        }
243        try {
244            log.trace("getInputStream() returns normally");
245            return new DataInputStream(socketConn.getInputStream());
246        } catch (java.io.IOException ex1) {
247            log.error("Exception getting input stream:", ex1);
248            return null;
249        }
250    }
251
252    /**
253     * {@inheritDoc}
254     */
255    @Override
256    public DataOutputStream getOutputStream() {
257        if (!opened) {
258            log.error("getOutputStream called before load(), stream not available");
259        }
260        try {
261            return new DataOutputStream(socketConn.getOutputStream());
262        } catch (java.io.IOException e) {
263            log.error("getOutputStream exception:", e);
264            ConnectionStatus.instance().setConnectionState(
265                    getSystemConnectionMemo(), ConnectionStatus.CONNECTION_DOWN);
266        }
267        return null;
268    }
269
270    /**
271     * {@inheritDoc}
272     */
273    @Override
274    protected void closeConnection(){
275        try {
276            socketConn.close();
277        } catch (IOException e) {
278            log.trace("Unable to close socket", e);
279        }
280        opened=false;
281    }
282
283    /**
284     * Customizable method to deal with resetting a system connection after a
285     * successful recovery of a connection.
286     */
287    @Override
288    protected void resetupConnection() {
289    }
290
291    /**
292     * {@inheritDoc}
293     */
294    @Override
295    protected void reconnectFromLoop(int retryNum){
296        try {
297            // if the device allows autoConfiguration,
298            // we need to run the autoConfigure() call
299            // before we try to reconnect.
300            if (getMdnsConfigure()) {
301                autoConfigure();
302            }
303            connect();
304        } catch (IOException ex) {
305            log.trace("restart failed", ex); // main warning to log.error done within connect();
306            // if returned on exception stops thread and connection attempts
307        }
308    }
309
310    /*
311     * Set the connection timeout to the specified value.
312     * If the socket is not null, set the SO_TIMEOUT option on the
313     * socket as well.
314     *
315     * @param t timeout value in milliseconds
316     */
317    protected void setConnectionTimeout(int t) {
318        connTimeout = t;
319        try {
320            if (socketConn != null) {
321                socketConn.setSoTimeout(getConnectionTimeout());
322            }
323        } catch (java.net.SocketException se) {
324            log.debug("Unable to set socket timeout option on socket");
325        }
326    }
327
328    /*
329     * Get the connection timeout value.
330     *
331     * @return timeout value in milliseconds
332     */
333    protected int getConnectionTimeout() {
334        return connTimeout;
335    }
336
337    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractNetworkPortController.class);
338
339}