001package jmri.jmrix.loconet.loconetovertcp;
002
003import java.beans.PropertyChangeEvent;
004import java.io.IOException;
005import java.net.ServerSocket;
006import java.net.Socket;
007import java.util.ArrayList;
008import java.util.LinkedList;
009import java.util.List;
010import jmri.InstanceManager;
011import jmri.ShutDownManager;
012import jmri.jmrix.loconet.LnTrafficController;
013import jmri.jmrix.loconet.LocoNetSystemConnectionMemo;
014import jmri.util.zeroconf.ZeroConfService;
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017
018import javax.annotation.Nonnull;
019
020/**
021 * Implementation of the LocoNetOverTcp LbServer Server Protocol.
022 *
023 * @author Alex Shepherd Copyright (C) 2006
024 */
025public class LnTcpServer {
026
027    private final List<ClientRxHandler> clients = new LinkedList<>();
028    private Thread socketListener;
029    private ServerSocket serverSocket;
030    private final List<LnTcpServerListener> stateListeners = new ArrayList<>();
031    private boolean settingsChanged = false;
032    private final Runnable shutDownTask = this::disable;
033    private ZeroConfService service = null;
034
035    private int portNumber;
036    private final LnTrafficController tc;
037
038    private LnTcpServer(@Nonnull LocoNetSystemConnectionMemo memo) {
039        tc = memo.getLnTrafficController(); // store tc in order to know where to send messages
040        LnTcpPreferences pm = LnTcpPreferences.getDefault();
041        portNumber = pm.getPort();
042        pm.addPropertyChangeListener((PropertyChangeEvent evt) -> {
043            // ignore uninteresting property changes
044            if (LnTcpPreferences.PORT.equals(evt.getPropertyName())) {// only change the port if stopped
045                if (!isEnabled()) {
046                    portNumber = pm.getPort();
047                }
048            }
049        });
050    }
051
052    /**
053     * Get the default server instance, creating it if necessary.
054     *
055     * @return the default LnTcpServer instance
056     */
057    public static synchronized LnTcpServer getDefault() {
058        return InstanceManager.getOptionalDefault(LnTcpServer.class).orElseGet(() -> {
059            LnTcpServer server = new LnTcpServer(jmri.InstanceManager.getDefault(LocoNetSystemConnectionMemo.class));
060            return InstanceManager.setDefault(LnTcpServer.class, server);
061        });
062    }
063
064    public boolean isEnabled() {
065        return (socketListener != null) && (socketListener.isAlive());
066    }
067
068    public boolean isSettingChanged() {
069        return settingsChanged;
070    }
071
072    public void enable() {
073        if (socketListener == null) {
074            socketListener = new Thread(new ClientListener());
075            socketListener.setDaemon(true);
076            socketListener.setName("LocoNetOverTcpServer");
077            log.info("Starting new LocoNetOverTcpServer listener on port {}", portNumber);
078            socketListener.start();
079            updateServerStateListeners();
080            // advertise over Zeroconf/Bonjour
081            if (this.service == null) {
082                this.service = ZeroConfService.create("_loconetovertcpserver._tcp.local.", portNumber);
083            }
084            log.info("Starting ZeroConfService _loconetovertcpserver._tcp.local for LocoNetOverTCP Server");
085            this.service.publish();
086            InstanceManager.getDefault(jmri.ShutDownManager.class).register(this.shutDownTask);
087        }
088    }
089
090    public void disable() {
091        if (socketListener != null) {
092            socketListener.interrupt();
093            socketListener = null;
094            try {
095                if (serverSocket != null) {
096                    serverSocket.close();
097                }
098            } catch (IOException ignore) {
099            }
100
101            updateServerStateListeners();
102
103            // Now close all the client connections
104            Object[] clientsArray;
105
106            synchronized (clients) {
107                clientsArray = clients.toArray();
108            }
109            for (Object o : clientsArray) {
110                ((ClientRxHandler) o).close();
111            }
112        }
113        if (this.service != null) {
114            this.service.stop();
115        }
116        InstanceManager.getDefault(ShutDownManager.class).deregister(this.shutDownTask);
117    }
118
119    private void updateServerStateListeners() {
120        synchronized (this) {
121            this.stateListeners.stream().filter((l) -> (l != null)).forEachOrdered((l) -> {
122                l.notifyServerStateChanged(this);
123            });
124        }
125    }
126
127    private void updateClientStateListeners() {
128        synchronized (this) {
129            this.stateListeners.stream().filter((l) -> (l != null)).forEachOrdered((l) -> {
130                l.notifyClientStateChanged(this);
131            });
132        }
133    }
134
135    public void addStateListener(LnTcpServerListener l) {
136        this.stateListeners.add(l);
137    }
138
139    public boolean removeStateListener(LnTcpServerListener l) {
140        return this.stateListeners.remove(l);
141    }
142
143    /**
144     * Get the port this server is using.
145     *
146     * @return the port
147     */
148    public int getPort() {
149        return this.portNumber;
150    }
151
152    class ClientListener implements Runnable {
153
154        @Override
155        public void run() {
156            Socket newClientConnection;
157            String remoteAddress;
158            try {
159                serverSocket = new ServerSocket(portNumber);
160                serverSocket.setReuseAddress(true);
161                while (!socketListener.isInterrupted()) {
162                    newClientConnection = serverSocket.accept();
163                    remoteAddress = newClientConnection.getRemoteSocketAddress().toString();
164                    log.info("Server: Connection from: {}", remoteAddress);
165                    addClient(new ClientRxHandler(remoteAddress, newClientConnection, tc));
166                }
167                serverSocket.close();
168            } catch (IOException ex) {
169                if (!ex.toString().toLowerCase().contains("socket closed")) {
170                    log.error("Server: IO Exception: ", ex);
171                }
172            }
173            serverSocket = null;
174        }
175    }
176
177    protected void addClient(ClientRxHandler handler) {
178        synchronized (clients) {
179            clients.add(handler);
180        }
181        updateClientStateListeners();
182    }
183
184    protected void removeClient(ClientRxHandler handler) {
185        synchronized (clients) {
186            clients.remove(handler);
187        }
188        updateClientStateListeners();
189    }
190
191    public int getClientCount() {
192        synchronized (clients) {
193            return clients.size();
194        }
195    }
196
197    private final static Logger log = LoggerFactory.getLogger(LnTcpServer.class);
198
199}