001package jmri.server.json;
002
003import static jmri.server.json.JSON.GOODBYE;
004import static jmri.server.json.JSON.JSON;
005import static jmri.server.json.JSON.JSON_PROTOCOL_VERSION;
006import static jmri.server.json.JSON.TYPE;
007import static jmri.server.json.JSON.ZEROCONF_SERVICE_TYPE;
008
009import com.fasterxml.jackson.core.JsonParser.Feature;
010import com.fasterxml.jackson.databind.ObjectMapper;
011import com.fasterxml.jackson.databind.ObjectReader;
012import java.io.DataInputStream;
013import java.io.DataOutputStream;
014import java.io.IOException;
015import java.io.InputStream;
016import java.util.HashMap;
017import java.util.NoSuchElementException;
018import java.util.concurrent.TimeUnit;
019import jmri.InstanceManager;
020import jmri.InstanceManagerAutoDefault;
021import jmri.implementation.AbstractShutDownTask;
022import jmri.jmris.JmriServer;
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025
026/**
027 * This is an implementation of a JSON server for JMRI. See
028 * {@link jmri.server.json} for more details.
029 *
030 * @author Paul Bender Copyright (C) 2010
031 * @author Randall Wood Copyright (C) 2016
032 */
033public class JsonServer extends JmriServer implements InstanceManagerAutoDefault {
034
035    private static final Logger log = LoggerFactory.getLogger(JsonServer.class);
036    private ObjectMapper mapper;
037
038    /**
039     * Create a new server using the default port.
040     */
041    public JsonServer() {
042        this(InstanceManager.getDefault(JsonServerPreferences.class).getPort(), InstanceManager.getDefault(JsonServerPreferences.class).getHeartbeatInterval());
043    }
044
045    /**
046     * Create a new server.
047     *
048     * @param port    the port to listen on
049     * @param timeout the timeout before closing unresponsive connections
050     */
051    public JsonServer(int port, int timeout) {
052        super(port, timeout);
053        this.mapper = new ObjectMapper().configure(Feature.AUTO_CLOSE_SOURCE, false);
054        shutDownTask = new AbstractShutDownTask("Stop JSON Server") { // NOI18N
055            @Override
056            public void run() {
057                try {
058                    JsonServer.this.stop();
059                } catch (Exception ex) {
060                    log.warn("Exception shutting down JSON Server", ex);
061                }
062            }
063        };
064    }
065
066    @Override
067    public void start() {
068        log.info("Starting JSON Server on port {}", this.portNo);
069        super.start();
070    }
071
072    @Override
073    public void stop() {
074        log.info("Stopping JSON Server.");
075        super.stop();
076    }
077
078    @Override
079    protected void advertise() {
080        HashMap<String, String> properties = new HashMap<>();
081        properties.put(JSON, JSON_PROTOCOL_VERSION);
082        this.advertise(ZEROCONF_SERVICE_TYPE, properties);
083    }
084
085    // Handle communication to a client through inStream and outStream
086    @Override
087    public void handleClient(DataInputStream inStream, DataOutputStream outStream) throws IOException {
088        ObjectReader reader = this.mapper.reader();
089        JsonClientHandler handler = new JsonClientHandler(new JsonConnection(outStream));
090
091        // Start by sending a welcome message
092        handler.onMessage(JsonClientHandler.HELLO_MSG);
093
094        boolean handling = true;
095        while (handling) {
096            try {
097                handler.onMessage(reader.readTree((InputStream) inStream));
098                // Read the command from the client
099            } catch (IOException e) {
100                // attempt to close the connection and throw the exception
101                handler.onClose();
102                throw e;
103            } catch (NoSuchElementException nse) {
104                // we get an NSE when we are finished with this client
105                // so break out of the loop
106                handling = false;
107            }
108        }
109        handler.onClose();
110    }
111
112    @Override
113    public void stopClient(DataInputStream inStream, DataOutputStream outStream) throws IOException {
114        outStream.writeBytes(this.mapper.writeValueAsString(this.mapper.createObjectNode().put(TYPE, GOODBYE)));
115        try {
116            // without this delay, the output stream could be closed before the
117            // preparing to disconnect message is sent
118            TimeUnit.MILLISECONDS.sleep(100);
119        } catch (InterruptedException ex) {
120            // log for debugging only, since we are most likely shutting down the
121            // server or the program entirely at this point, so it doesn't matter
122            log.debug("Wait to send clean shutdown message interrupted.");
123            Thread.currentThread().interrupt();
124        }
125    }
126}