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}