001package jmri.server.json;
002
003import static jmri.server.json.JSON.DATA;
004import static jmri.server.json.JSON.GET;
005import static jmri.server.json.JSON.GOODBYE;
006import static jmri.server.json.JSON.HELLO;
007import static jmri.server.json.JSON.ID;
008import static jmri.server.json.JSON.LIST;
009import static jmri.server.json.JSON.LOCALE;
010import static jmri.server.json.JSON.METHOD;
011import static jmri.server.json.JSON.PING;
012import static jmri.server.json.JSON.TYPE;
013import static jmri.server.json.JSON.VERSION;
014import static jmri.server.json.JSON.VERSIONS;
015
016import com.fasterxml.jackson.core.JsonProcessingException;
017import com.fasterxml.jackson.databind.JsonNode;
018import java.io.IOException;
019import java.util.Arrays;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.Locale;
023import java.util.ServiceLoader;
024
025import javax.annotation.Nonnull;
026import javax.servlet.http.HttpServletResponse;
027import jmri.InstanceManager;
028import jmri.JmriException;
029import jmri.server.json.schema.JsonSchemaServiceCache;
030import jmri.spi.JsonServiceFactory;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034/**
035 * Handler for JSON messages from a TCP socket or WebSocket client.
036 */
037public class JsonClientHandler {
038
039    /**
040     * When used as a parameter to {@link #onMessage(java.lang.String)}, will
041     * cause a {@value jmri.server.json.JSON#HELLO} message to be sent to the
042     * client.
043     */
044    public static final String HELLO_MSG = "{\"" + TYPE + "\":\"" + HELLO + "\"}";
045    private final JsonConnection connection;
046    private final HashMap<String, HashSet<JsonSocketService<?>>> services = new HashMap<>();
047    private final JsonServerPreferences preferences = InstanceManager.getDefault(JsonServerPreferences.class);
048    private final JsonSchemaServiceCache schemas = InstanceManager.getDefault(JsonSchemaServiceCache.class);
049    private static final Logger log = LoggerFactory.getLogger(JsonClientHandler.class);
050
051    public JsonClientHandler(JsonConnection connection) {
052        this.connection = connection;
053        String version = connection.getVersion();
054        try {
055            setVersion(version, 0);
056        } catch (JsonException e) {
057            // this exception can normally be thrown by bad input
058            // from a JSON client; however at this point it can only
059            // be caused by a bad edit of JSON.java or JsonConnection.java, so
060            // throwing an IllegalArgumentException as
061            // a failure at this point can only be caused by
062            // carelessly editing either of those classes
063            log.error("Unable to create handler for version {}", version);
064            throw new IllegalArgumentException();
065        }
066    }
067
068    public void onClose() {
069        services.values().forEach(set -> set.stream().forEach(JsonSocketService::onClose));
070        services.clear();
071    }
072
073    /**
074     * Process a JSON string and handle appropriately.
075     * <p>
076     * See {@link jmri.server.json} for expected JSON objects.
077     *
078     * @param string the message
079     * @throws java.io.IOException if communications with the client is broken
080     * @see #onMessage(JsonNode)
081     */
082    public void onMessage(String string) throws IOException {
083        if (string.equals("{\"type\":\"ping\"}")) {
084            // turn down the noise when debugging
085            log.trace("Received from client: '{}'", string);
086        } else {
087            log.debug("Received from client: '{}'", string);
088        }
089        try {
090            onMessage(connection.getObjectMapper().readTree(string));
091        } catch (JsonProcessingException pe) {
092            log.warn("Exception processing \"{}\"\n{}", string, pe.getMessage());
093            sendErrorMessage(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
094                    Bundle.getMessage(connection.getLocale(), "ErrorProcessingJSON", pe.getLocalizedMessage()), 0);
095        }
096    }
097
098    /**
099     * Process a JSON node and handle appropriately.
100     * <p>
101     * See {@link jmri.server.json} for expected JSON objects.
102     *
103     * @param root the JSON node.
104     * @throws java.io.IOException if communications with the client is broken
105     * @see #onMessage(java.lang.String)
106     */
107    public void onMessage(JsonNode root) throws IOException {
108        String method = root.path(METHOD).asText(GET);
109        String type = root.path(TYPE).asText();
110        int id = root.path(ID).asInt(0);
111        JsonNode data = root.path(DATA);
112        JsonRequest request = new JsonRequest(connection.getLocale(), connection.getVersion(), method, id);
113        try {
114            if (preferences.getValidateClientMessages()) {
115                schemas.validateMessage(root, false, request);
116            }
117            if ((root.path(TYPE).isMissingNode() || type.equals(LIST)) && root.path(LIST).isValueNode()) {
118                type = root.path(LIST).asText();
119                method = LIST;
120            }
121            if (data.isMissingNode()) {
122                if ((type.equals(HELLO) || type.equals(PING) || type.equals(GOODBYE)) ||
123                        (method.equals(LIST) || method.equals(GET))) {
124                    // these messages are not required to have a data payload,
125                    // so create one if the message did not contain one to avoid
126                    // special casing later
127                    data = connection.getObjectMapper().createObjectNode();
128                } else {
129                    sendErrorMessage(HttpServletResponse.SC_BAD_REQUEST,
130                            Bundle.getMessage(connection.getLocale(), "ErrorMissingData"), id);
131                    return;
132                }
133            }
134            // method not explicitly set in root, but set in data
135            if (root.path(METHOD).isMissingNode() && data.path(METHOD).isValueNode()) {
136                // at one point, we used method within data, so check there also
137                method = data.path(METHOD).asText(JSON.GET);
138            }
139            if (type.equals(PING)) { // turn down the noise a bit
140                log.trace("Processing '{}' with '{}'", type, data);
141            } else {
142                log.debug("Processing '{}' with '{}'", type, data);
143            }
144            if (method.equals(LIST)) {
145                if (services.get(type) != null) {
146                    for (JsonSocketService<?> service : services.get(type)) {
147                        service.onList(type, data, request);
148                    }
149                } else {
150                    log.warn("Requested list type '{}' unknown.", type);
151                    sendErrorMessage(HttpServletResponse.SC_NOT_FOUND,
152                            Bundle.getMessage(connection.getLocale(), JsonException.ERROR_UNKNOWN_TYPE, type), id);
153                }
154                return;
155            } else {
156                if (type.equals(HELLO) || type.equals(LOCALE) && !data.path(LOCALE).isMissingNode()) {
157                    connection.setLocale(
158                            Locale.forLanguageTag(data.path(LOCALE).asText(connection.getLocale().getLanguage())));
159                    setVersion(data.path(VERSION).asText(connection.getVersion()), id);
160                    // since locale or version may have changed, ensure any
161                    // response is using new version and locale
162                    request = new JsonRequest(connection.getLocale(), connection.getVersion(), method, id);
163                }
164                if (services.get(type) != null) {
165                    for (JsonSocketService<?> service : services.get(type)) {
166                        service.onMessage(type, data, request);
167                    }
168                } else {
169                    log.warn("Requested type '{}' unknown.", type);
170                    sendErrorMessage(HttpServletResponse.SC_NOT_FOUND,
171                            Bundle.getMessage(connection.getLocale(), JsonException.ERROR_UNKNOWN_TYPE, type), id);
172                }
173            }
174            if (type.equals(GOODBYE)) {
175                // close the connection if GOODBYE is received.
176                connection.close();
177            }
178        } catch (JmriException je) {
179            log.warn("Unsupported operation attempted {}", root);
180            sendErrorMessage(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, Bundle.getMessage(
181                    connection.getLocale(), "ErrorUnsupportedOperation", je.getLocalizedMessage()), id);
182        } catch (JsonException je) {
183            sendErrorMessage(je);
184        }
185    }
186
187    private void sendErrorMessage(int code, String message, int id) throws IOException {
188        JsonException ex = new JsonException(code, message, id);
189        sendErrorMessage(ex);
190    }
191
192    private void sendErrorMessage(JsonException ex) throws IOException {
193        connection.sendMessage(ex.getJsonMessage(), ex.getId());
194    }
195
196    private void setVersion(@Nonnull String version, int id) throws JsonException {
197        if (VERSIONS.stream().noneMatch(v -> v.equals(version))) {
198            throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
199                    Bundle.getMessage(connection.getLocale(), "ErrorUnknownType", version), id);
200        }
201        connection.setVersion(version);
202        onClose(); // dispose of any existing objects
203        ServiceLoader.load(JsonServiceFactory.class)
204                .forEach(factory -> {
205                    JsonSocketService<?> service = factory.getSocketService(connection, version);
206                    Arrays.stream(factory.getTypes(version)).forEach(type -> {
207                        HashSet<JsonSocketService<?>> set = services.get(type);
208                        if (set == null) {
209                            services.put(type, new HashSet<>());
210                            set = services.get(type);
211                        }
212                        set.add(service);
213                    });
214                    Arrays.stream(factory.getReceivedTypes(version)).forEach(type -> {
215                        HashSet<JsonSocketService<?>> set = services.get(type);
216                        if (set == null) {
217                            services.put(type, new HashSet<>());
218                            set = services.get(type);
219                        }
220                        set.add(service);
221                    });
222                });
223    }
224
225    protected HashMap<String, HashSet<JsonSocketService<?>>> getServices() {
226        return new HashMap<>(services);
227    }
228}