Project

General

Profile

Actions

Read / Write REST Services

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.

Specification

NOUNS

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.

VERBS

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.

Services

If the verb is packagename and the unique identifier of the resource is uuid then the services exposed should be

  • GET /packagename Retrieve a list of entities

  • GET /packagename/uuid Find an entity

  • POST /packagename Create an resource (that will be subsequently found at /packagename/uuid )

  • POST /packagename/uuid Update the resource at that location

  • DELETE /packagename/uuid Delete the resource at that location

Representation

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.

<?xml version="1.0" encoding="UTF-8"?>
<InvalidObjectException xmlns="http://etaxonomy.eu/cdm/1.0">
  <Status>400</Status>
  <Code>Bad Request</Code>
  <Message>The Person object is not valid. The 'titleCache' field may not be null or empty.</Message>
</InvalidObjectException>

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.

Implementation of Controller Layer

Resource: /packagename

Represents the collection of objects of the type represented by packagename.

Method: GET

Get some or all of the objects in that collection

Optional parameters:
  • class: takes the fully qualified class name of a subclass of the type supported. Restricts objects returned to objects of that type. Optional.

  • pageNumber: The offset in pageSize chunks from the beginning of the recordset. Optional.

  • pageSize: The maximum number of objects returned. Optional.

  • 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

Request Headers
  • 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

  • Accept-Language: Won't affect the representation per-se, but will affect the rendering of error messages

Status Code
  • @200 OK@: on success

  • @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

  • @415 UNSUPPORTED MEDIA FORMAT@: if the client asks for an unsupported representation in the Accept header

Response

From the controller layer, a ModelAndView with a single object of type Pager in it. This can be rendered using the same elements used within the DataSet object in cdmlib-io thus:

<?xml version="1.0" encoding="UTF-8"?>
<Taxa xmlns="http://etaxonomy.eu/cdm/model/1.0"
           xmlns:taxon="http://etaxonomy.eu/cdm/model/taxon/1.0"
           pageSize="..." pageNumber="..." maxResults="...">
  <taxon:Taxon . . . />
  . . .
</cdm:Taxa>

These elements have had three additional attributes; pageSize, pageNumber, and maxResults added to them to allow for paging of the result set

Can be implemented thus:

@RequestMapping(value = "/packagename", method = RequestMethod.GET)
public ModelAndView get(@RequestParam(value = "class", required = false) Class<? extends T> clazz,
                        @RequestParam(value = "pageNumber", required = false, defaultValue = DEFAULT_PAGE_NUMBER) Integer pageNumber,
                        @RequestParam(value = "pageSize", required = false, defaultValue = DEFAULT_PAGESIZE) Integer pageSize,
                        @RequestParam(value = "sort", required = false, defaultValue = DEFAULT_SORT) List<OrderHint> sort) {
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.addObject(service.list(clazz, pageSize,                     
                                        pageNumber,sort,
                                        DEFAULT_INIT_STRATEGY));
    return modelAndView;
}

Method: POST

Add a new object to that collection

Optional parameters:

None

Request Headers
  • 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

  • Accept-Language: Won't affect the representation per-se, but will affect the rendering of error messages

Request Body

An XML document conforming to the CDM Schema that maps to one of the object types that this resource supports.

Status Code
  • @201 CREATED@: on success

  • @400 BAD REQUEST@: if the XML document in the request body is ill formed, or if the object, when marshalled is invalid

  • @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.

  • @409 CONFLICT@: If there is already a resource with the same UUID

  • @415 UNSUPPORTED MEDIA FORMAT@: if the client asks for an unsupported representation in the Accept header

Response Headers
  • Location: containing the location of the newly created resource
Response

An xml document representing the newly created resource (see below for GET /packagename/uuid).

Implementation
@ResponseStatus(value = HttpStatus.CREATED)
@RequestMapping(method = RequestMethod.POST)
public ModelAndView post(@RequestBody T t1) {
    T t2 = fieldMapper.map(t1,t1.getClass());
    collectionMapper.map(t1,t2);
    List<ConstraintViolation<T>> constraintViolations = validator.validate(t, Level2.class,Level3.class);
    if(!constraintViolations.isEmpty()) {
        throw new InvalidObjectException(t,constraintViolations);
    }
    service.saveOrUpdate(t2);
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.addObject(t);
    return modelAndView;
}

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.

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.

Resource /packagename/uuid

This resource represents a single IdentifiableEntity

Method: GET

Retrieves the resource

Optional parameters:

None

Request Headers
  • 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

  • Accept-Language: Won't affect the representation per-se, but will affect the rendering of error messages

Status Code
  • @200 OK@: on success

  • @400 BAD REQUEST@: If the UUID is not well formed

  • @404 NOT FOUND@: If a resource does not exist with that UUID

  • @410 GONE@: If a resource did exist, but has been deleted (optional)

  • @415 UNSUPPORTED MEDIA FORMAT@: if the client asks for an unsupported representation in the Accept header

Response

The response is rendered as XML using the standard JAXB mappings present in on the model classes:

<?xml version=”1.0” encoding=”UTF-8”?>
<taxon:Taxon 
    uuid="urn-uuid-49361efb-aad6-448b-a316-f09005cee160"     
    isDoubtful="false"
    xmlns=”http://etaxonomy.eu/cdm/model/1.0” 
    xmlns:common=”http://etaxonomy.eu/cdm/model/common/1.0”
    xmlns:taxon=”http://etaxonomy.eu/cdm/model/taxon/1.0”
 >
  <common:TitleCache>Acherontia Laspeyres, 1809 sec cate-project.org</common:TitleCache>
  <common:ProtectedTitleCache>false</common:ProtectedTitleCache>
  <taxon:Name>urn-uuid-a8ab9567-7179-430d-a9fc-e3f22fe269f1</taxon:Name>
  <taxon:Sec>urn-uuid-b8cd17ba-32ea-4201-85f1-63d31526ccdb</taxon:Sec>
  <taxon:TaxonomicChildrenCount>1</taxon:TaxonomicChildrenCount>
  <taxon:RelationsFromThisTaxon>
    <taxon:FromThisTaxonRelationship uuid="urn-uuid-24f94660-c4e0-490c-8fb3-5cac20a426a4">
      <taxon:RelatedFrom>urn-uuid-49361efb-aad6-448b-a316-f09005cee160</taxon:RelatedFrom>
      <taxon:RelatedTo>urn-uuid-cc55da11-3bd7-4ca3-b1e2-6c5503eefea6</taxon:RelatedTo>
      <taxon:Type>urn-uuid-5799a150-1386-4bae-952e-e040d06f7485</taxon:Type>
    </taxon:FromThisTaxonRelationship>
  </taxon:RelationsFromThisTaxon>
  <taxon:Descriptions xsd:nil=”true”/>
</taxon:Taxon>

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:

Wheras an empty collection looks like this

taxon:Descriptions/

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.

Can be implemented thus

@RequestMapping(method = RequestMethod.GET)
public ModelAndView get(@PathVariable(value = "uuid") UUID uuid) {
    T t = service.load(uuid,DEFAULT_INIT_STRATEGY);
    if(t == null) {
        throw new ObjectNotFoundException(type,uuid);
    }
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.addObject(t);
    return modelAndView;
}

Method: POST

Updates the resource

Optional parameters:

None

Request Headers
  • 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

  • Accept-Language: Won't affect the representation per-se, but will affect the rendering of error messages

Status Code
  • @200 OK@: on success

  • @400 BAD REQUEST@: If the UUID is not well formed or if the updated object is not valid

  • @404 NOT FOUND@: If a resource does not exist with that UUID

  • @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.

  • @410 GONE@: If a resource did exist, but has been deleted (optional)

  • @415 UNSUPPORTED MEDIA FORMAT@: if the client asks for an unsupported representation in the Accept header

Response

See the response for GET

Implementation
@RequestMapping(method = RequestMethod.POST)
public ModelAndView post(@PathVariable(value = "uuid") UUID uuid, @RequestBody T t2) {
    T t1 = service.load(uuid,DEFAULT_INIT_STRATEGY);
    if(t1 == null) {
        throw new ObjectNotFoundException(type,uuid);
    }
    fieldMapper.map(t1,t2);
    collectionsMapper.map(t1,t2);
    List<ConstraintViolation<T>> constraintViolations = validator.validate(t1, Level2.class,Level3.class);
    if(!constraintViolations.isEmpty()) {
        throw new InvalidObjectException(t,constraintViolations);
    }
    service.saveOrUpdate(t1);
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.addObject(t1);
    return modelAndView;
}

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.

Method: DELETE

Deletes the resource

Optional parameters:

None

Request Headers
  • 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

  • Accept-Language: Won't affect the representation per-se, but will affect the rendering of error messages

Status Code
  • @200 OK@: on success

  • @400 BAD REQUEST@: If the UUID is not well formed

  • @404 NOT FOUND@: If a resource does not exist with that UUID

  • @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.

  • @410 GONE@: If a resource did exist, but has already been deleted (optional)

  • @415 UNSUPPORTED MEDIA FORMAT@: if the client asks for an unsupported representation in the Accept header

Response

Although Clients may not expect any content in the response body, it is recommended to redirect to the parent resource (/packagename) with a message indicating that a resource has been deleted.

Implementation
@RequestMapping(method = RequestMethod.DELETE)
public ModelAndView post(@PathVariable(value = "uuid") UUID uuid) {
    T t = service.load(uuid,DEFAULT_INIT_STRATEGY);
    if(t1 == null) {
        throw new ObjectNotFoundException(type,uuid);
    }
    service.delete(t);
    ModelAndView modelAndView = new ModelAndView(“redirect:/{packagename}”);
    return modelAndView;
}

Changes

cdmlib-model

  1. Creation of a custom AccessorFactory implementation in eu.etaxonomy.cdm.jaxb

  2. Creation of a Hibernate safe Accessor implentation in eu.etaxonomy.cdm.jaxb

  3. Addition of @XmlAccessorFactory annotations in all package-info.java files in cdmlib-model

  4. Addition of “nillable = true” to most @XmlElementWrapper annotations

  5. Creation of a CdmError base class in cdmlib-model

cdmlib-io

  1. Addition of “nillable = true” to equivalent xml elements

  2. Creation of a CdmIDResolver implentation in eu.etaxonomy.cdm.io.jaxb

  3. Modification of eu.etaxonomy.cdm.io.jaxb.DataSet and creation of extra elements to incorporate pageSize, pageNumber and maxResults attributes

cdmlib-remote

  1. Creation of an XmlView class to render cdm entities as CDM XML

  2. Creation of dozer mapper custom converters to merge collections if they are not null.

  3. Creation of dozer mapping files to map cdm objects.

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.

Updated by Katja Luther almost 2 years ago · 8 revisions