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