CdmServerReadWriteRest » History » Version 1
Ben Clark, 02/10/2010 09:32 AM
1 | 1 | Ben Clark | |
---|---|---|---|
2 | # Read / Write REST Services |
||
3 | |||
4 | |||
5 | This page provides some details about a _in progress_ implementation of read / write REST services for the CDM Server and briefly sketches a technical implementation of those services, particularly focussing on the gaps in the current implementation. |
||
6 | |||
7 | |||
8 | |||
9 | ## Specification |
||
10 | |||
11 | |||
12 | ### NOUNS |
||
13 | |||
14 | The nouns (objects) exposed in the first instance should be the major objects in the CDM. All objects that extend IdentifiableEntity will be exposed as a noun in the first instance. Many such objects are abstract and have subclasses, but these will not have separate nouns. Following the current CDM REST Services V2, the names of the nouns shall be the package names in the main. |
||
15 | |||
16 | |||
17 | |||
18 | ### VERBS |
||
19 | |||
20 | The GET verb is already exposed in the current CDM REST Services V2. To achieve full Read / Write functionality we will need to implement POST (Create), POST (Update) and DELETE. Some tweaking of GET is also neccessary. |
||
21 | |||
22 | |||
23 | Services |
||
24 | |||
25 | If the verb is _packagename_ and the unique identifier of the resource is _uuid_ then the services exposed should be |
||
26 | |||
27 | |||
28 | * `GET` /_packagename_ Retrieve a list of entities |
||
29 | |||
30 | * `GET` /_packagename_/_uuid_ Find an entity |
||
31 | |||
32 | * `POST` /_packagename_ Create an resource (that will be subsequently found at /_packagename_/_uuid_ ) |
||
33 | |||
34 | * `POST` /_packagename_/_uuid_ Update the resource at that location |
||
35 | |||
36 | * `DELETE` /_packagename_/_uuid_ Delete the resource at that location |
||
37 | |||
38 | |||
39 | |||
40 | ## Representation |
||
41 | |||
42 | |||
43 | The services shall read and write XML following the schema defined in the cdmlib-io package. Reading other representations is currently supported in e.g. spring via the jackson library but beyond the scope of this document. In order to succesfully represent error messages, we propose the creation of a CdmError base class, that can be extended for specific error types and marked up using JAXB annotations e.g. |
||
44 | |||
45 | |||
46 | ~~~ |
||
47 | <?xml version=â€1.0†encoding=â€UTF-8â€?> |
||
48 | <InvalidObjectException xmlns=â€http://etaxonomy.eu/cdm/1.0â€> |
||
49 | <Status>400</Status> |
||
50 | <Code>Bad Request</Code> |
||
51 | <Message>The Person object is not valid. The 'titleCache' field may not be null or empty.</Message> |
||
52 | </InvalidObjectException> |
||
53 | ~~~ |
||
54 | |||
55 | It is reccommended that such errors objects are placed within the general cdm namespace and the base CdmError resides in the cdmlib-model package to allow specialist errors to be subclassed within the cdmlib and thrown by cdmlib components (e.g. in the service layer) and caught and handled transparently in cdmserver. |
||
56 | |||
57 | |||
58 | |||
59 | ## Implementation – Controller Layer |
||
60 | |||
61 | |||
62 | |||
63 | ### Resource: /_packagename_ |
||
64 | |||
65 | |||
66 | Represents the collection of objects of the type represented by _packagename_. |
||
67 | |||
68 | |||
69 | |||
70 | #### Method: `GET` |
||
71 | |||
72 | |||
73 | Get some or all of the objects in that collection |
||
74 | |||
75 | |||
76 | |||
77 | ##### Optional parameters: |
||
78 | |||
79 | |||
80 | * class: takes the fully qualified class name of a subclass of the type supported. Restricts objects returned to objects of that type. Optional. |
||
81 | |||
82 | * pageNumber: The offset in pageSize chunks from the beginning of the recordset. Optional. |
||
83 | |||
84 | * pageSize: The maximum number of objects returned. Optional. |
||
85 | |||
86 | * sort: A property of the object to sort by, in the form propertyName_(asc|desc) e.g. titleCache_asc, created_desc, name.combinationAuthor.firstName_asc |
||
87 | |||
88 | |||
89 | |||
90 | ##### Request Headers |
||
91 | |||
92 | |||
93 | * Accept: determined what format the representation should be rendered in: application/xml is supported, plus others such as text/json, text/html or application/xhtml might also be supported |
||
94 | |||
95 | * Accept-Language: Won't affect the representation per-se, but will affect the rendering of error messages |
||
96 | |||
97 | |||
98 | |||
99 | ##### Status Code |
||
100 | |||
101 | |||
102 | * @200 OK@: on success |
||
103 | |||
104 | * @400 BAD REQUEST@: if one of the arguments is ill formed e.g. if the class parameter does not map to the correct class or if pageSize is negative |
||
105 | |||
106 | * @415 UNSUPPORTED MEDIA FORMAT@: if the client asks for an unsupported representation in the Accept header |
||
107 | |||
108 | |||
109 | |||
110 | ##### Response |
||
111 | |||
112 | |||
113 | From the controller layer, a ModelAndView with a single object of type Pager<T> in it. This can be rendered using the same elements used within the DataSet object in cdmlib-io thus: |
||
114 | |||
115 | ~~~ |
||
116 | <?xml version=â€1.0†encoding=â€UTF-8â€?> |
||
117 | <Taxa xmlns=â€http://etaxonomy.eu/cdm/model/1.0†|
||
118 | xmlns:taxon=â€http://etaxonomy.eu/cdm/model/taxon/1.0†|
||
119 | pageSize=â€...†pageNumber=â€...†maxResults=â€...â€> |
||
120 | <taxon:Taxon . . . /> |
||
121 | . . . |
||
122 | </cdm:Taxa> |
||
123 | ~~~ |
||
124 | |||
125 | These elements have had three additional attributes; pageSize, pageNumber, and maxResults added to them to allow for paging of the result set |
||
126 | |||
127 | |||
128 | Can be implemented thus: |
||
129 | |||
130 | |||
131 | ~~~ |
||
132 | @RequestMapping(value = “/packagenameâ€, method = RequestMethod.GET) |
||
133 | public ModelAndView get(@RequestParam(value = "class", required = false) Class<? extends T> clazz, |
||
134 | @RequestParam(value = "pageNumber", required = false, defaultValue = DEFAULT_PAGE_NUMBER) Integer pageNumber, |
||
135 | @RequestParam(value = "pageSize", required = false, defaultValue = DEFAULT_PAGESIZE) Integer pageSize, |
||
136 | @RequestParam(value = "sort", required = false, defaultValue = DEFAULT_SORT) List<OrderHint> sort) { |
||
137 | ModelAndView modelAndView = new ModelAndView(); |
||
138 | modelAndView.addObject(service.list(clazz, pageSize, |
||
139 | pageNumber,sort, |
||
140 | DEFAULT_INIT_STRATEGY)); |
||
141 | return modelAndView; |
||
142 | } |
||
143 | ~~~ |
||
144 | |||
145 | |||
146 | #### Method: `POST` |
||
147 | |||
148 | |||
149 | Add a new object to that collection |
||
150 | |||
151 | |||
152 | |||
153 | ##### Optional parameters: |
||
154 | |||
155 | None |
||
156 | |||
157 | |||
158 | |||
159 | ##### Request Headers |
||
160 | |||
161 | |||
162 | * Accept: determined what format the representation should be rendered in: application/xml is supported, plus others such as text/json, text/html or application/xhtml might also be supported |
||
163 | |||
164 | * Accept-Language: Won't affect the representation per-se, but will affect the rendering of error messages |
||
165 | |||
166 | |||
167 | |||
168 | ##### Request Body |
||
169 | |||
170 | An XML document conforming to the CDM Schema that maps to one of the object types that this resource supports. |
||
171 | |||
172 | |||
173 | |||
174 | ##### Status Code |
||
175 | |||
176 | |||
177 | * @201 CREATED@: on success |
||
178 | |||
179 | * @400 BAD REQUEST@: if the XML document in the request body is ill formed, or if the object, when marshalled is invalid |
||
180 | |||
181 | * @401 UNAUTHORIZED@: It is expected that this resource will be protected by an authorization mechanism (e.g. HTTP Basic / HTTP Digest / TLS or other) that might be application specific and beyond the scope of this document. If the request is not authorized, this error code is returned. |
||
182 | |||
183 | * @409 CONFLICT@: If there is already a resource with the same UUID |
||
184 | |||
185 | * @415 UNSUPPORTED MEDIA FORMAT@: if the client asks for an unsupported representation in the Accept header |
||
186 | |||
187 | |||
188 | |||
189 | ##### Response Headers |
||
190 | |||
191 | |||
192 | * Location: containing the location of the newly created resource |
||
193 | |||
194 | |||
195 | |||
196 | ##### Response |
||
197 | |||
198 | |||
199 | An xml document representing the newly created resource (see below for GET /_packagename_/_uuid_). |
||
200 | |||
201 | |||
202 | |||
203 | ##### Implementation |
||
204 | |||
205 | |||
206 | ~~~ |
||
207 | @ResponseStatus(value = HttpStatus.CREATED) |
||
208 | @RequestMapping(method = RequestMethod.POST) |
||
209 | public ModelAndView post(@RequestBody T t1) { |
||
210 | T t2 = fieldMapper.map(t1,t1.getClass()); |
||
211 | collectionMapper.map(t1,t2); |
||
212 | List<ConstraintViolation<T>> constraintViolations = validator.validate(t, Level2.class,Level3.class); |
||
213 | if(!constraintViolations.isEmpty()) { |
||
214 | throw new InvalidObjectException(t,constraintViolations); |
||
215 | } |
||
216 | service.saveOrUpdate(t2); |
||
217 | ModelAndView modelAndView = new ModelAndView(); |
||
218 | modelAndView.addObject(t); |
||
219 | return modelAndView; |
||
220 | } |
||
221 | |||
222 | ~~~ |
||
223 | |||
224 | The unmarshalling is handled by the @RequestBody annotation in conjunction with a MarshallingHttpMessageConverter. A custom IDResolver implementation is required to resolve entities that are referenced in the XML fragment – entities already found on the server will be found by this component and substituted for the relevant UUIDs. The fieldMapper.map(...) call maps the object onto a fresh object, filtering out fields that should be generated or set automatically – id, uuid, created, createdBy, updated, updatedBy. collectionMapper.map(...) filters the *-to-many properties of the object. |
||
225 | |||
226 | |||
227 | The object is then validated and, if it is valid, saved (the saveOrUpdate call updates the related objects rather than trying to create a new version of them). The response status is set to 201 CREATED (courtesy of the @ResponseStatus annotation). The Location header is set to the location of the newly created resource in the view. |
||
228 | |||
229 | |||
230 | |||
231 | ### Resource /_packagename_/_uuid_ |
||
232 | |||
233 | |||
234 | This resource represents a single IdentifiableEntity |
||
235 | |||
236 | |||
237 | Method: GET |
||
238 | |||
239 | Retrieves the resource |
||
240 | |||
241 | Optional parameters: |
||
242 | |||
243 | None |
||
244 | |||
245 | Request Headers |
||
246 | |||
247 | Accept: determined what format the representation should be rendered in: application/xml is supported, plus others such as text/json, text/html or application/xhtml might also be supported |
||
248 | |||
249 | Accept-Language: Won't affect the representation per-se, but will affect the rendering of error messages |
||
250 | |||
251 | Status Code |
||
252 | |||
253 | 200 OK: on success |
||
254 | |||
255 | 400 BAD REQUEST: If the UUID is not well formed |
||
256 | |||
257 | 404 NOT FOUND: If a resource does not exist with that UUID |
||
258 | |||
259 | 410 GONE: If a resource did exist, but has been deleted (optional) |
||
260 | |||
261 | 415 UNSUPPORTED MEDIA FORMAT: if the client asks for an unsupported representation in the Accept header |
||
262 | |||
263 | Response |
||
264 | |||
265 | The response is rendered as XML using the standard JAXB mappings present in on the model classes: |
||
266 | |||
267 | <?xml version=â€1.0†encoding=â€UTF-8â€?> |
||
268 | |||
269 | <taxon:Taxon |
||
270 | |||
271 | uuid="urn-uuid-49361efb-aad6-448b-a316-f09005cee160" |
||
272 | |||
273 | isDoubtful="false" |
||
274 | |||
275 | xmlns=â€http://etaxonomy.eu/cdm/model/1.0†|
||
276 | |||
277 | xmlns:common=â€http://etaxonomy.eu/cdm/model/common/1.0†|
||
278 | |||
279 | xmlns:taxon=â€http://etaxonomy.eu/cdm/model/taxon/1.0†|
||
280 | |||
281 | > |
||
282 | |||
283 | <common:TitleCache>Acherontia Laspeyres, 1809 sec cate-project.org</common:TitleCache> |
||
284 | |||
285 | <common:ProtectedTitleCache>false</common:ProtectedTitleCache> |
||
286 | |||
287 | <taxon:Name>urn-uuid-a8ab9567-7179-430d-a9fc-e3f22fe269f1</taxon:Name> |
||
288 | |||
289 | <taxon:Sec>urn-uuid-b8cd17ba-32ea-4201-85f1-63d31526ccdb</taxon:Sec> |
||
290 | |||
291 | <taxon:TaxonomicChildrenCount>1</taxon:TaxonomicChildrenCount> |
||
292 | |||
293 | <taxon:RelationsFromThisTaxon> |
||
294 | |||
295 | <taxon:FromThisTaxonRelationship uuid="urn-uuid-24f94660-c4e0-490c-8fb3-5cac20a426a4"> |
||
296 | |||
297 | <taxon:RelatedFrom>urn-uuid-49361efb-aad6-448b-a316-f09005cee160</taxon:RelatedFrom> |
||
298 | |||
299 | <taxon:RelatedTo>urn-uuid-cc55da11-3bd7-4ca3-b1e2-6c5503eefea6</taxon:RelatedTo> |
||
300 | |||
301 | <taxon:Type>urn-uuid-5799a150-1386-4bae-952e-e040d06f7485</taxon:Type> |
||
302 | |||
303 | </taxon:FromThisTaxonRelationship> |
||
304 | |||
305 | </taxon:RelationsFromThisTaxon> |
||
306 | |||
307 | <taxon:Descriptions xsd:nil=â€trueâ€/> |
||
308 | |||
309 | </taxon:Taxon> |
||
310 | |||
311 | One issues is the incomplete initialization of model entities resulting in LazyInitializationExceptions' when the entities are serialized. This can be overcome by setting a custom JAXB accessor that returns null if an object is uninitialized (much like the current JSON processors). To distinguish between empty collections and uninitialized collections, collections of objects are declared to be “nillable†- an uninitialized collection looks like this: |
||
312 | |||
313 | <taxon:Descriptions xsd:nil=â€trueâ€/> |
||
314 | |||
315 | Wheras an empty collection looks like this |
||
316 | |||
317 | <taxon:Descriptions/> |
||
318 | |||
319 | Non-collections cannot be distinguished in this way, so all *-To-One relationships should be initialized by default (i.e. the initialization strategy should be “$â€). Using nil elements to distinguish between empty and uninitialized collections has benefits on the POST side too, as is allows incomplete representations of objects to be updated. |
||
320 | |||
321 | Can be implemented thus |
||
322 | |||
323 | @RequestMapping(method = RequestMethod.GET) |
||
324 | |||
325 | public ModelAndView get(@PathVariable(value = "uuid") UUID uuid) { |
||
326 | |||
327 | T t = service.load(uuid,DEFAULT_INIT_STRATEGY); |
||
328 | |||
329 | if(t == null) { |
||
330 | |||
331 | throw new ObjectNotFoundException(type,uuid); |
||
332 | |||
333 | } |
||
334 | |||
335 | ModelAndView modelAndView = new ModelAndView(); |
||
336 | |||
337 | modelAndView.addObject(t); |
||
338 | |||
339 | return modelAndView; |
||
340 | |||
341 | } |
||
342 | |||
343 | Method: POST |
||
344 | |||
345 | Updates the resource |
||
346 | |||
347 | Optional parameters: |
||
348 | |||
349 | None |
||
350 | |||
351 | Request Headers |
||
352 | |||
353 | Accept: determined what format the representation should be rendered in: application/xml is supported, plus others such as text/json, text/html or application/xhtml might also be supported |
||
354 | |||
355 | Accept-Language: Won't affect the representation per-se, but will affect the rendering of error messages |
||
356 | |||
357 | Status Code |
||
358 | |||
359 | 200 OK: on success |
||
360 | |||
361 | 400 BAD REQUEST: If the UUID is not well formed or if the updated object is not valid |
||
362 | |||
363 | 404 NOT FOUND: If a resource does not exist with that UUID |
||
364 | |||
365 | 401 UNAUTHORIZED: It is expected that this resource will be protected by an authorization mechanism (e.g. HTTP Basic / HTTP Digest / TLS or other) that might be application specific and beyond the scope of this document. If the request is not authorized, this error code is returned. |
||
366 | |||
367 | 410 GONE: If a resource did exist, but has been deleted (optional) |
||
368 | |||
369 | 415 UNSUPPORTED MEDIA FORMAT: if the client asks for an unsupported representation in the Accept header |
||
370 | |||
371 | Response |
||
372 | |||
373 | See the response for GET |
||
374 | |||
375 | Implementation |
||
376 | |||
377 | @RequestMapping(method = RequestMethod.POST) |
||
378 | |||
379 | public ModelAndView post(@PathVariable(value = "uuid") UUID uuid, @RequestBody T t2) { |
||
380 | |||
381 | T t1 = service.load(uuid,DEFAULT_INIT_STRATEGY); |
||
382 | |||
383 | if(t1 == null) { |
||
384 | |||
385 | throw new ObjectNotFoundException(type,uuid); |
||
386 | |||
387 | } |
||
388 | |||
389 | mapper.map(t1,t2); |
||
390 | |||
391 | List<ConstraintViolation<T>> constraintViolations = validator.validate(t1, Level2.class,Level3.class); |
||
392 | |||
393 | if(!constraintViolations.isEmpty()) { |
||
394 | |||
395 | throw new InvalidObjectException(t,constraintViolations); |
||
396 | |||
397 | } |
||
398 | |||
399 | service.saveOrUpdate(t1); |
||
400 | |||
401 | ModelAndView modelAndView = new ModelAndView(); |
||
402 | |||
403 | modelAndView.addObject(t1); |
||
404 | |||
405 | return modelAndView; |
||
406 | |||
407 | } |
||
408 | |||
409 | This method combines the approach used to GET /{packagename}/{uuid} and to POST /{packagename}. However, in this case the mapping is from the new version to transient object. |
||
410 | |||
411 | Method: DELETE |
||
412 | |||
413 | Deletes the resource |
||
414 | |||
415 | Optional parameters: |
||
416 | |||
417 | None |
||
418 | |||
419 | Request Headers |
||
420 | |||
421 | Accept: determined what format the representation should be rendered in: application/xml is supported, plus others such as text/json, text/html or application/xhtml might also be supported |
||
422 | |||
423 | Accept-Language: Won't affect the representation per-se, but will affect the rendering of error messages |
||
424 | |||
425 | Status Code |
||
426 | |||
427 | 200 OK: on success |
||
428 | |||
429 | 400 BAD REQUEST: If the UUID is not well formed |
||
430 | |||
431 | 404 NOT FOUND: If a resource does not exist with that UUID |
||
432 | |||
433 | 401 UNAUTHORIZED: It is expected that this resource will be protected by an authorization mechanism (e.g. HTTP Basic / HTTP Digest / TLS or other) that might be application specific and beyond the scope of this document. If the request is not authorized, this error code is returned. |
||
434 | |||
435 | 410 GONE: If a resource did exist, but has already been deleted (optional) |
||
436 | |||
437 | 415 UNSUPPORTED MEDIA FORMAT: if the client asks for an unsupported representation in the Accept header |
||
438 | |||
439 | Response |
||
440 | |||
441 | Although Clients may not expect any content in the response body, it is recommended to redirect to the parent resource (/{packagename}). |
||
442 | |||
443 | Implementation |
||
444 | |||
445 | @RequestMapping(method = RequestMethod.DELETE) |
||
446 | |||
447 | public ModelAndView post(@PathVariable(value = "uuid") UUID uuid) { |
||
448 | |||
449 | T t = service.load(uuid,DEFAULT_INIT_STRATEGY); |
||
450 | |||
451 | if(t1 == null) { |
||
452 | |||
453 | throw new ObjectNotFoundException(type,uuid); |
||
454 | |||
455 | } |
||
456 | |||
457 | service.delete(t); |
||
458 | |||
459 | ModelAndView modelAndView = new ModelAndView(“redirect:/{packagename}â€); |
||
460 | |||
461 | return modelAndView; |
||
462 | |||
463 | } |
||
464 | |||
465 | Changes |
||
466 | |||
467 | cdmlib-model |
||
468 | |||
469 | 1. Creation of a custom AccessorFactory implementation in eu.etaxonomy.cdm.jaxb |
||
470 | |||
471 | 2. Creation of a Hibernate safe Accessor implentation in eu.etaxonomy.cdm.jaxb |
||
472 | |||
473 | 3. Addition of @XmlAccessorFactory annotations in all package-info.java files in cdmlib-model |
||
474 | |||
475 | 4. Addition of “nillable = true†to most @XmlElementWrapper annotations |
||
476 | |||
477 | 5. Creation of a CdmError base class in cdmlib-model |
||
478 | |||
479 | cdmlib-io |
||
480 | |||
481 | 1. Addition of “nillable = true†to equivalent xml elements |
||
482 | |||
483 | 2. Creation of a CdmIDResolver implentation in eu.etaxonomy.cdm.io.jaxb |
||
484 | |||
485 | 3. Modification of eu.etaxonomy.cdm.io.jaxb.DataSet and creation of extra elements to incorporate pageSize, pageNumber and maxResults attributes |
||
486 | |||
487 | cdmlib-remote |
||
488 | |||
489 | 1. Creation of an XmlView class to render cdm entities as CDM XML |
||
490 | |||
491 | 2. Creation of dozer mapper custom converters to merge collections if they are not null. |
||
492 | |||
493 | 3. Creation of dozer mapping files to map cdm objects. |
||
494 | |||
495 | All of the following changes are low-risk because they do not change the behaviour of existing code. The custom accessor factory must be explicitly enabled for to have an effect. |