001package jmri.server.json; 002 003import static jmri.server.json.JSON.FORCE_DELETE; 004import static jmri.server.json.JSON.CONFLICT; 005 006import com.fasterxml.jackson.databind.JsonNode; 007import com.fasterxml.jackson.databind.ObjectMapper; 008import com.fasterxml.jackson.databind.SerializationFeature; 009import com.fasterxml.jackson.databind.node.ArrayNode; 010import com.fasterxml.jackson.databind.node.ObjectNode; 011 012import java.io.IOException; 013import java.util.Locale; 014import javax.annotation.Nonnull; 015import javax.annotation.CheckForNull; 016import javax.servlet.http.HttpServletResponse; 017 018/** 019 * Provide HTTP method handlers for JSON RESTful messages 020 * <p> 021 * It is recommended that this class be as lightweight as possible, by relying 022 * either on a helper stored in the InstanceManager, or a helper with static 023 * methods. 024 * <h2>Message ID Handling</h2> 025 * <p> 026 * A message ID from a client is a positive integer greater than zero, to be 027 * passed back unchanged to the client so the client can track direct responses 028 * to requests (this is not needed in the RESTful API, but is available in the 029 * RESTful API). The Message ID (or zero if none) is passed into most public 030 * methods of JsonHttpService as the {@code id} parameter. When creating an 031 * object that is to be embedded in another object as a property, it is 032 * permissable to pass the additive inverse of the ID to ensure the ID is not 033 * included in the embedded object, but allow any error messages to be thrown 034 * with the correct message ID. 035 * <p> 036 * Note that to ensure this works, only create a complete object with 037 * {@link #message(String, JsonNode, String, int)} or one of its variants. 038 * 039 * @author Randall Wood 040 */ 041public abstract class JsonHttpService { 042 043 protected final ObjectMapper mapper; 044 045 /** 046 * Create an HTTP handler for a JSON service. 047 * 048 * @param mapper the ObjectMapper to create new JSON nodes 049 */ 050 protected JsonHttpService(@Nonnull ObjectMapper mapper) { 051 this.mapper = mapper; 052 this.mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); 053 } 054 055 /** 056 * Respond to an HTTP GET request for the requested name. 057 * <p> 058 * If name is null, return a list of all objects for the given type, if 059 * appropriate. 060 * <p> 061 * This method should throw a 500 Internal Server Error if type is not 062 * recognized. 063 * 064 * @param type the type of the requested object 065 * @param name the system name of the requested object 066 * @param data JSON data set of attributes of the requested object 067 * @param request the JSON request 068 * @return a JSON description of the requested object 069 * @throws JsonException if the named object does not exist or other error 070 * occurs 071 */ 072 @Nonnull 073 public abstract JsonNode doGet(@Nonnull String type, @Nonnull String name, @Nonnull JsonNode data, 074 @Nonnull JsonRequest request) 075 throws JsonException; 076 077 /** 078 * Respond to an HTTP POST request for the requested name. 079 * <p> 080 * This method should throw a 400 Invalid Request error if the named object 081 * does not exist. 082 * 083 * @param type the type of the requested object 084 * @param name the system name of the requested object 085 * @param data JSON data set of attributes of the requested object to be 086 * updated 087 * @param request the JSON request 088 * @return a JSON description of the requested object after updates have 089 * been applied 090 * @throws JsonException if the named object does not exist or other error 091 * occurs 092 */ 093 @Nonnull 094 public abstract JsonNode doPost(@Nonnull String type, @Nonnull String name, @Nonnull JsonNode data, 095 @Nonnull JsonRequest request) throws JsonException; 096 097 /** 098 * Respond to an HTTP PUT request for the requested name. 099 * <p> 100 * Throw an HTTP 405 Method Not Allowed exception if new objects of the type 101 * are not intended to be addable. 102 * 103 * @param type the type of the requested object 104 * @param name the system name of the requested object 105 * @param data JSON data set of attributes of the requested object to be 106 * created or updated 107 * @param request the JSON request 108 * @return a JSON description of the requested object 109 * @throws JsonException if the method is not allowed or other error occurs 110 */ 111 @Nonnull 112 public JsonNode doPut(@Nonnull String type, @Nonnull String name, @Nonnull JsonNode data, 113 @Nonnull JsonRequest request) 114 throws JsonException { 115 throw new JsonException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, 116 Bundle.getMessage(request.locale, "PutNotAllowed", type), request.id); 117 } 118 119 /** 120 * Respond to an HTTP DELETE request for the requested name. 121 * <p> 122 * Throw an HTTP 405 Method Not Allowed exception if the object is not 123 * intended to be removable. 124 * <p> 125 * Do not throw an error if the requested object does not exist. 126 * 127 * @param type the type of the deleted object 128 * @param name the system name of the deleted object 129 * @param data additional data 130 * @param request the JSON request 131 * @throws JsonException if this method is not allowed or other error occurs 132 */ 133 public void doDelete(@Nonnull String type, @Nonnull String name, @Nonnull JsonNode data, 134 @Nonnull JsonRequest request) 135 throws JsonException { 136 throw new JsonException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, 137 Bundle.getMessage(request.locale, "DeleteNotAllowed", type), request.id); 138 } 139 140 /** 141 * Respond to an HTTP GET request for a list of items of type. 142 * <p> 143 * This is called by the {@link jmri.web.servlet.json.JsonServlet} to handle 144 * get requests for a type, but no name. Services that do not have named 145 * objects, such as the {@link jmri.server.json.time.JsonTimeHttpService} 146 * should respond to this with a list containing a single JSON object. 147 * Services that can't return a list may throw a 400 Bad Request 148 * JsonException in this case. 149 * 150 * @param type the type of the requested list 151 * @param data JSON data set of attributes of the requested objects 152 * @param request the JSON request 153 * @return a JSON list or message containing type {@value JSON#LIST}, the 154 * list as data, and the passed in id 155 * @throws JsonException may be thrown by concrete implementations 156 */ 157 @Nonnull 158 public abstract JsonNode doGetList(@Nonnull String type, @Nonnull JsonNode data, @Nonnull JsonRequest request) 159 throws JsonException; 160 161 /** 162 * Get the JSON Schema for the {@code data} property of the requested type 163 * of JSON object. It is a invalid for implementations to not return a valid 164 * schema that clients can use to validate a request to or response from the 165 * JSON services. 166 * <p> 167 * Note that a schema must be contained in a standard object as: 168 * <p> 169 * {@code {"type":"schema", "data":{"schema":<em>schema</em>, 170 * "server":boolean}} } 171 * <p> 172 * If using {@link #doSchema(String, boolean, String, String, int)}, an 173 * implementation can be as simple as: {@code 174 * return doSchema(type, server, "path/to/client/schema.json", "path/to/server/schema.json", id); 175 * } 176 * 177 * @param type the type for which a schema is requested 178 * @param server true if the schema is for a message from the server; false 179 * if the schema is for a message from the client 180 * @param request the JSON request 181 * @return a JSON Schema valid for the type 182 * @throws JsonException if an error occurs preparing schema; if type is is 183 * not a type handled by this service, this must be 184 * thrown with an error code of 500 and the localized 185 * message ERROR_UNKNOWN_TYPE 186 */ 187 @Nonnull 188 public abstract JsonNode doSchema(@Nonnull String type, boolean server, @Nonnull JsonRequest request) 189 throws JsonException; 190 191 /** 192 * Helper to make implementing 193 * {@link #doSchema(String, boolean, JsonRequest)} easier. Throws a 194 * JsonException based on an IOException or NullPointerException if unable 195 * to read the schemas as resources. 196 * 197 * @param type the type for which a schema is requested 198 * @param server true if the schema is for a message from the server; 199 * false if the schema is for a message from the client 200 * @param serverSchema the path to the schema for a response object of type 201 * @param clientSchema the path to the schema for a request object of type 202 * @param id the message id set by the client 203 * @return a JSON Schema valid for the type 204 * @throws JsonException if an error occurs preparing schema 205 */ 206 @Nonnull 207 protected final JsonNode doSchema(@Nonnull String type, boolean server, @Nonnull String serverSchema, 208 @Nonnull String clientSchema, int id) throws JsonException { 209 JsonNode schema; 210 try { 211 if (server) { 212 schema = this.mapper.readTree(this.getClass().getClassLoader().getResource(serverSchema)); 213 } else { 214 schema = this.mapper.readTree(this.getClass().getClassLoader().getResource(clientSchema)); 215 } 216 } catch ( 217 IOException | 218 IllegalArgumentException ex) { 219 throw new JsonException(500, ex, id); 220 } 221 return this.doSchema(type, server, schema, id); 222 } 223 224 /** 225 * Helper to make implementing 226 * {@link #doSchema(String, boolean, JsonRequest)} easier. 227 * 228 * @param type the type for which a schema is requested 229 * @param server true if the schema is for a message from the server; false 230 * if the schema is for a message from the client 231 * @param schema the schema for a response object of type 232 * @param id the message id set by the client 233 * @return a JSON Schema valid for the type 234 */ 235 @Nonnull 236 protected final JsonNode doSchema(@Nonnull String type, boolean server, @Nonnull JsonNode schema, int id) { 237 ObjectNode data = mapper.createObjectNode(); 238 data.put(JSON.NAME, type); 239 data.put(JSON.SERVER, server); 240 data.set(JSON.SCHEMA, schema); 241 return message(JSON.SCHEMA, data, id); 242 } 243 244 /** 245 * Get the in-use ObjectMapper for this service. 246 * 247 * @return the object mapper 248 */ 249 @Nonnull 250 public final ObjectMapper getObjectMapper() { 251 return this.mapper; 252 } 253 254 /** 255 * Verify a deletion token. If the token is not valid any pending deletion 256 * tokens for the type and name are also deleted. 257 * 258 * @param type the type of object pending deletion 259 * @param name the name of object pending deletion 260 * @param token the token previously provided to client 261 * @return true if token was provided to client and no other delete attempt 262 * was made by client with a different or missing token since token 263 * was issued to client; false otherwise 264 */ 265 public final boolean acceptForceDeleteToken(@Nonnull String type, @Nonnull String name, 266 @CheckForNull String token) { 267 return JsonDeleteTokenManager.getDefault().acceptToken(type, name, token); 268 } 269 270 /** 271 * Throw an HTTP CONFLICT (409) exception when an object is requested to be 272 * deleted and it is in use. This exception will include a token that can be 273 * used to force deletion by the client and may include a JSON list of the 274 * objects using the object for which deletion was requested. 275 * 276 * @param type the type of object in conflicting state 277 * @param name the name of the object in conflicting state 278 * @param conflicts the using objects of this object; may be empty 279 * @param request the JSON request 280 * @throws JsonException the exception 281 */ 282 public final void throwDeleteConflictException(@Nonnull String type, @Nonnull String name, 283 @Nonnull ArrayNode conflicts, 284 @Nonnull JsonRequest request) throws JsonException { 285 ObjectNode data = mapper.createObjectNode(); 286 data.put(FORCE_DELETE, JsonDeleteTokenManager.getDefault().getToken(type, name)); 287 if (conflicts.size() != 0) { 288 data.set(CONFLICT, conflicts); 289 } 290 throw new JsonException(HttpServletResponse.SC_CONFLICT, 291 Bundle.getMessage(request.locale, "ErrorDeleteConflict", type, name), data, request.id); 292 } 293 294 /** 295 * Create a message node from an array. 296 * 297 * @param data the array 298 * @param id the message id provided by the client or its additive inverse 299 * @return if id is a positive, non-zero integer, return a message of type 300 * {@value JSON#LIST} with data as the data and id set; otherwise 301 * return data without modification 302 * @see #message(String, JsonNode, String, int) 303 * @see #message(String, JsonNode, int) 304 * @see #message(ObjectMapper, ArrayNode, String, int) 305 * @see #message(ObjectMapper, String, JsonNode, String, int) 306 */ 307 public final JsonNode message(@Nonnull ArrayNode data, int id) { 308 return message(mapper, data, null, id); 309 } 310 311 /** 312 * Create a message node without an explicit method. 313 * 314 * @param type the message type 315 * @param data the message data 316 * @param id the message id provided by the client or its additive inverse 317 * @return a message node without a method property; an id property is only 318 * present if id is greater than zero 319 * @see #message(ArrayNode, int) 320 * @see #message(String, JsonNode, String, int) 321 * @see #message(ObjectMapper, ArrayNode, String, int) 322 * @see #message(ObjectMapper, String, JsonNode, String, int) 323 */ 324 public final ObjectNode message(@Nonnull String type, @Nonnull JsonNode data, int id) { 325 return message(type, data, null, id); 326 } 327 328 /** 329 * Create a message node. 330 * 331 * @param type the message type 332 * @param data the message data 333 * @param method the message method 334 * @param id the message id provided by the client or its additive 335 * inverse 336 * @return a message node; an id proper 337 * @see #message(ArrayNode, int) 338 * @see #message(String, JsonNode, int) 339 * @see #message(ObjectMapper, ArrayNode, String, int) 340 * @see #message(ObjectMapper, String, JsonNode, String, int) 341 */ 342 public final ObjectNode message(@Nonnull String type, @Nonnull JsonNode data, @CheckForNull String method, int id) { 343 return message(mapper, type, data, method, id); 344 } 345 346 /** 347 * Create a message node from an array. 348 * 349 * @param mapper the ObjectMapper to use to construct the message 350 * @param data the array 351 * @param method the message method 352 * @param id the message id provided by the client or its additive 353 * inverse 354 * @return if id is a positive, non-zero integer, return a message of type 355 * {@value JSON#LIST} with data as the data and id set; otherwise 356 * just return data without modification 357 * @see #message(ArrayNode, int) 358 * @see #message(String, JsonNode, String, int) 359 * @see #message(String, JsonNode, int) 360 * @see #message(ObjectMapper, String, JsonNode, String, int) 361 */ 362 public static final JsonNode message(@Nonnull ObjectMapper mapper, @Nonnull ArrayNode data, 363 @CheckForNull String method, int id) { 364 return (id > 0) ? message(mapper, JSON.LIST, data, method, id) : data; 365 } 366 367 /** 368 * Create a message node. 369 * 370 * @param mapper the ObjectMapper to use to construct the message 371 * @param type the message type 372 * @param data the message data 373 * @param method the message method or null 374 * @param id the message id provided by the client or its additive 375 * inverse 376 * @return a message node; if method is null, no method property is 377 * included; if id is not greater than zero, no id property is 378 * included 379 * @see #message(ArrayNode, int) 380 * @see #message(String, JsonNode, String, int) 381 * @see #message(String, JsonNode, int) 382 * @see #message(ObjectMapper, ArrayNode, String, int) 383 */ 384 public static final ObjectNode message(@Nonnull ObjectMapper mapper, @Nonnull String type, @Nonnull JsonNode data, 385 @CheckForNull String method, int id) { 386 ObjectNode root = mapper.createObjectNode(); 387 root.put(JSON.TYPE, type); 388 root.set(JSON.DATA, data); 389 if (method != null) { 390 root.put(JSON.METHOD, method); 391 } 392 if (id > 0) { 393 root.put(JSON.ID, id); 394 } 395 return root; 396 } 397}