Jump to: navigation, search

EclipseLink/DesignDocs/JPARS2.0

JPA RS 2.0

ER448484

Document History

Date Author Version Description & Notes
22.09.2014 Dmitry Kornilov Initial revision

Project overview

The main task of this project id implementing the following additional features to the existing JPARS application:

  • Change schema for singular resources (entities), collection resources (query results) and resources metadata.
  • Pagination is the process of dividing the content into multiple “pages”. In the context of REST, it applies to collection resource only. Instead of returning all the singular resources with one request, each request only returns a chunk of the singular resources.
  • Support filtering what fields should be returned. For example, ?fields=dname,loc only returns dname and loc fields. Comma “,” is used to separate multiple field names.
  • Each returned resource has a set of links. These links should include a self link and a canonical link. The self link is how the resource is accessed currently, and the canonical link is the desired link for a resource.
  • Resource metadata is information about the resource that is not specific to a particular representation. A resource metadata is provided at version/persistent-unit/metadata-catalog/resourceName
  • Metadata catalog is a collection resource and its items are the metadata of all available resources. It is available at v2.0/persistent-unit/metadata-catalog

Details

Test Data

All use cases and samples used below to describe the service functionality use the same test data described below.

Imagine that we have two related entities: Basket and BasketItem.

   @Entity
   @Table(name = "JPARS_BASKET")
   public class Basket {	
       @Id
       @Column(name = "BASKET_ID")
       private Integer id;

       @Column(name = "BASKET_NAME")
       private String name;

       @OneToMany(mappedBy = "basket")
       @RestPageable(limit = 2)
       private List<BasketItem> basketItems = new ArrayList<BasketItem>();

       // Getters and setters are skipped	
   }

   @Entity
   @Table(name = "JPARS_BASKET_ITEM")
   @NamedQueries({
       @NamedQuery(
           name = "BasketItem.findAll",
           query = "SELECT bi FROM BasketItem bi ORDER BY bi.id"),
       @NamedQuery(
           name = "BasketItem.findAllPageable",
           query = "SELECT bi FROM BasketItem bi ORDER BY bi.id"),
       })
       @RestPageableQueries({
           @RestPageableQuery(queryName = "BasketItem.findAllPageable", limit = 20)
       })
       public class BasketItem {	
           @Id
           @Column(name = "ITEM_ID")
           private Integer id;

           @ManyToOne
           @JoinColumn(name = "BASKET_ID")
           private Basket basket;
           
           @Column(name = "ITEM_NAME")
           private String name;
           
           // Getters and setters are skipped
       }
   }

The Basket and BasketItem tables have the following data:

JPARS_BASKET

BASKET_ID BASKET_NAME
1 Basket1


JPARS_BASKET_ITEM

ITEM_ID BASKET_ID ITEM_NAME
1 1 BasketItem1
2 1 BasketItem2
3 1 BasketItem3
4 1 BasketItem4
5 1 BasketItem5


All URLs start from http://localhost:8080/eclipselink.jpars.test/persistence/v2.0/jpars_basket-static. I'll mention it as <root> just for the reason of readability.


Resource schema

Singular resource schema:

   "singularResource": {
       "title": "Singular Resource",
       "description": "Oracle base singular resource schema definition",
       "type": "object",
       "properties": {
           "links": {
               "type": "array",
               "items": {
                   "$ref": "#/definitions/instanceLink"
               }
           }
       },
        "links": [
           {
              "href": "#",
              "rel": "describedby"
           }
       ]
   }


Collection resource schema:

   "collectionBaseResource": {
       "allOf": [
           {
               "$ref": "#/definitions/singularResource"
           }
       ],
       "title": "Base Collection Resource",
       "description": "Oracle base collection resource schema definition. ",
       "properties": {
           "items": {
               "type": "array",
               "items": {
                   "type": "object"
               }
           }
       }
   }


This change makes the protocol semantic of JPARS 2.0 different from older versions. To make the service backwards compatible the version number is included in the URL.

The URL template looks like this:

http(s)://server:port/app/persistence/version/persistent-unit/...

Where:

server:port - server IP address (DNS name) + port app - your application name. persistence - JPARS application name. Constant. version - JPARS version. Currently only values "v1.0" and "v2.0" only supported. persistent-unit - Your persistent unit name. Defined in persistece.xml file.

To make the URL format compatible with the previous version of the service it is possible to omit version parameter from the URL. In this case "v1.0" is used.

It is possible to use the 'latest' keyword instead of the version number. In this case the latest protocol semantic version will be used. It is "v2.0" at the moment.

Examples:

This request gets a Basket entity with id = 1 using jpars_basket-static persistent unit from localhost. It returns data in JPARS 1.0 format.

http(s)://localhost:8080/eclipselink.jpars.test/persistence/jpars_basket-static/entity/Basket/1 

it's the same as:

http(s)://localhost:8080/eclipselink.jpars.test/persistence/v1.0/jpars_basket-static/entity/Basket/1

The response of the previous version of JPARS is out of scope here, so I skip it.

This request does the same but uses JPARS 2.0 protocol semantic:

http(s)://localhost:8080/eclipselink.jpars.test/persistence/v2.0/jpars_basket-static/entity/Basket/1

This request does absolutely the same as the previous one:

http(s)://localhost:8080/eclipselink.jpars.test/persistence/latest/jpars_basket-static/entity/Basket/1 

The response is:

   {
       "id": 1,
       "name": "Basket1",
       "basketItems": {
           "links": [
               {
                   "rel": "self",
                   "href": "<root>/entity/Basket/1/basketItems"
               },
               {
                   "rel": "canonical",
                   "href": "<root>/entity/Basket/1/basketItems"
               }
           ]
       },
       "links": [
           {
               "rel": "self",
               "href": "<root>/entity/Basket/1"
           },
           {
               "rel": "canonical",
               "href": "<root>/entity/Basket/1"
           }
       ]
   }


The data returned is compatible with single resource schema. It contains a required 'links' section.

This is a collection resource request.

GET <root>/query/BasketItem.findAll

Response:

   {
       "items": [
           {
               "type": "basketItem",
               "basket": {
                   "links": [
                       {
                           "rel": "self",
                           "href": "<root>/entity/BasketItem/5/basket"
                       },
                       {
                           "rel": "canonical",
                           "href": "<root>/entity/Basket/1"
                       }
                   ]
               },
               "id": 1,
               "name": "BasketItem1",
               "links": [
                   {
                       "rel": "self",
                       "href": "<root>/entity/BasketItem/1"
                   },
                   {
                       "rel": "canonical",
                       "href": "<root>/entity/BasketItem/1"
                   }
               ]
           },
           ...
       ],
       "links": [
           {
               "rel": "self",
               "href": "<root>/query/BasketItem.findAll"
           }
       ]
   }

Pagination

Pagination is not available for all collection resources by default. It's configured on the server side using annotations. If pagination request comes to the resource which doesn't support it exception is thrown.

Resources supporting pagination:

  • Named query resources (/persistence/v2.0/persistent-unit/query/query-name)
  • Single attributes of collection type (/persistence/v2.0/persistent-unit/entity/entity-name/primary-key/attribute-name)

Server Side Configuration

The server side configuration is done by annotating your entity with special paging annotations:

@RestPageableQueries

This annotation defines a list of all pageable queries defined by @RestPageableQuery anotations. It's placed on entity class. It works the similar way as @NamedQueries annotation.

@RestPageableQuery(queryName=<named_query_name>, limit=<items_per_page>)

This annotation must be placed inside @RestPageableQueries annotation. It says that named query with name *named_query_name* is pageable and the default number of items per page is *limit*. If limit is not defined the default value of 100 used. Named query with *named_query_name* has to exist.

Sample:

   @Entity
   @Table(name = "JPARS_BASKET_ITEM")
   @NamedQueries({
       @NamedQuery(
           name = "BasketItem.findAllPageable",
           query = "SELECT bi FROM BasketItem bi ORDER BY bi.id"),
   })
   @RestPageableQueries({
       @RestPageableQuery(queryName = "BasketItem.findAllPageable", limit = 20)
   })
   public class BasketItem { ... }


@RestPageable(limit=items_per_page)

This annotation defines a pageable collection attribute. It must be placed on the collection field. The number of items per page is defined by the optional limit parameter. Default limit value is 100.

Sample:

   @OneToMany(mappedBy = "basket")
   @RestPageable(limit = 2)
   private List<BasketItem> basketItems = new ArrayList<BasketItem>();


Query Parameters

If pagination is supported for a resource the following two optional parameters can be used to control it.

limit: control how many rows to return. For example /persistence/version/persistent-unit/query/query-name?limit=10 returns the first 10 items. The server uses a default value defined by the corresponding server side annotation if this query parameter is not specified or greater than the defaut value. The actual used limit is returned by limit response property.

offset: control from which row the response should contain. The first offset MUST be 0. e.g. /persistence/version/persistence-unit/query/query-name?limit=10&offset=10 returns the 11th-20th items. The default value is 0. If offset is greater than the actual number of records empty list is returned.


Links

For pagination request, it is important to contain links to related pages in the response.

next - where paging is enabled and there is a next page, next is used as the rel.

prev - where paging is enabled and there is a previous page, prev MUST be used as the rel.

The pagination links placed in the “links” section of the collection resource (see examples below), along with the other links associated with the resource.


Items

For collection resource, “items” should contain the following properties that are used in paging:

limit: the actual paging size used by the server.

offset: the actual index from which the records are returned.

count: the total number of records in the current response.

hasMore: this is a Boolean property to indicate whether there are more pages that satisfy the request. Always returned.


Use Cases

Use Case 1
GET <root>/query/BasketItem.findAllPageable

BasketItem.findAllPageable is a pageable query. Limit parameter is not specified, so the default limit specified in @RestPageableQuery annotation is used. The default limit is 20, so all 5 items are returned. Because all items are returned there are no "prev" and "next" links.

Response:

   {
       "items": [
           {
               "id": 1,
               "name": "BasketItem1",
               ...
           },
           ...
       ],
       "hasMore": false,
       "limit": 20,
       "offset": 0,
       "count": 5,
       "links": [
           {
               "rel": "self",
               "href": "<root>/query/BasketItem.findAllPageable"
           }
       ]
   }

Please note that basket items 2-5 were omitted from the response above.


Use Case 2
GET <root>/query/BasketItem.findAllPageable?limit=2 

In this case limit=2 is requested and no offset specified. Requested limit is less than default limit, so it's accepted. Offset is set to default value of 0. The first page of 3 is returned, so there is a "next" link and no "prev" link.

{
   "items": [
       {
           "id": 1,
           "name": "BasketItem1",
           ...
       },
       {
           "id": 2,
           "name": "BasketItem2",
           ...
       }
    ],
   "hasMore": true,
   "limit": 2,
   "offset": 0,
   "count": 2,
   "links": [
       {
           "rel": "next",
           "href": "<root>/query/BasketItem.findAllPageable?offset=2&limit=2"
       },
       {
           "rel": "self",
           "href": "<root>/query/BasketItem.findAllPageable?limit=2"
       }
   ]
}


Use Case 3
GET <root>/query/BasketItem.findAllPageable?limit=2&offset=2 

The next page from use case 2 is requested. Basket items 3 and 4 are returned. Both "prev" and "next" links are present.

{
   "items": [
       {
           "id": 3,
           "name": "BasketItem3",
           ...
       },
       {
           "id": 4,
           "name": "BasketItem4",
           ...
       }
   ],
   "hasMore": true,
   "limit": 2,
   "offset": 2,
   "count": 2,
   "links": [
       {
           "rel": "next",
           "href": "<root>/query/BasketItem.findAllPageable?offset=4&limit=2"
       },
       {
           "rel": "prev",
           "href": "<root>/query/BasketItem.findAllPageable?offset=0&limit=2"
       },
       {
           "rel": "self",
           "href": "<root>/query/BasketItem.findAllPageable?limit=2&offset=2"
       }
   ]
}


Use Case 4
GET <root>/query/BasketItem.findAllPageable?limit=2&offset=4 

The next page from use case 3 is requested. This is the last page. Basket item 5 is returned. "hasMore" is false and count=1. Only "prev" link is present.

{
   "items": [
       {
           "id": 5,
           "name": "BasketItem5",
           ...
       }
   ],
   "hasMore": false,
   "limit": 2,
   "offset": 4,
   "count": 1,
   "links": [
       {
           "rel": "prev",
           "href": "<root>/query/BasketItem.findAllPageable?offset=2&limit=2"
       },
       {
           "rel": "self",
           "href": "<root>/query/BasketItem.findAllPageable?limit=2&offset=4"
       }
   ]
}


Use Case 5
GET <root>/entity/Basket/1/basketItems?limit=2

Getting the first 2 items of pageable field Basket.basketItems.

{
   "items": [
       {
           "id": 1,
           "name": "BasketItem1",
           ...
       },
       {
           "id": 2,
           "name": "BasketItem2",
           ...
       }
   ],
   "hasMore": true,
   "limit": 2,
   "offset": 0,
   "count": 2,
   "links": [
       {
           "rel": "next",
           "href": "<root>/entity/Basket/1/basketItems?offset=2&limit=2"
       },
       {
           "rel": "self",
           "href": "<root>/entity/Basket/1/basketItems?limit=2"
       }
   ]
}

The response is the same as in Use Case 2 but there is a little difference in 'next' and 'previous' links.

Fields Filtering

This feature allows user to select fields should be returned. It's done with help of two query parameters *fields* and *excludeFields*:

fields parameter defines a list of returned fields. For example, *?fields=name,description* only returns *name* and *description* fields. Comma “,” is used to separate multiple field names.

excludeFields parameter defines a list of excluded fields. For example, *?excludeFields=image,text* returns all fields except *image* and *text*. Comma “,” is used to separate multiple field names.

fields and excludeFields parameters are mutual exclusive. If both of them are present in the request the error is returned.

. Use Cases

Use Case 1
GET <root>/entity/BasketItem/1?fields=id,name

Getting 'id' and 'name' fields of BasketItem entity with id = 1.

{
   "id": 1,
   "name": "BasketItem1"
 }	
   
Use Case 2
GET <root>/entity/BasketItem/1?excludeFields=name

Getting all fields except 'name' of BasketItem entity with id = 1.

{
   "id": 1,
   "basket": {
       "links": [
           {
               "rel": "self",
               "href": "<root>/entity/BasketItem/1/basket"
           },
           {
               "rel": "canonical",
               "href": "<root>/entity/Basket/1"
           }
       ]
   },
   "links": [
       {
           "rel": "self",
           "href": "<root>/entity/BasketItem/1"
       },
       {
           "rel": "canonical",
           "href": "<root>/entity/BasketItem/1"
       }
   ]
}

Links (self/canonical)

The 'links' section of singular and collection resources should include a self link and a canonical link. The self link is how the resource is accessed currently, and the canonical link is the desired link for a resource. Links should be absolute path to save client’s effort of computing the absolute path.

For example, this is a single entity request:

<root>/entity/Basket/1

The response contains self and canonical links for both singular and collection resources:

{
   "id": 1,
   "name": "Basket1",
   "basketItems": {
       "links": [
           {
               "rel": "self",
               "href": "<root>/entity/Basket/1/basketItems"
           },
           {
               "rel": "canonical",
               "href": "<root>/entity/Basket/1/basketItems"
           }
       ]
   },
   "links": [
       {
           "rel": "self",
           "href": "<root>/entity/Basket/1"
       },
       {
           "rel": "canonical",
           "href": "<root>/entity/Basket/1"
       }
   ]
}

In the example above self and canonical links are the same.

Metadata for resources

Resource metadata is information about the resource that is not specific to a particular representation. It contains a link to a resource schema, canonical link to resource and other data.

Metadata for entity resource is provided at:

/persistence/version/persistent-unit/metadata-catalog/entity/entityName

For example, metadata for Basket entity is provided at:

http(s)://localhost:7001/eclipselink.jpars.test/persistence/v2.0/jpars_basket-static/metadata-catalog/entity/Basket

Metadata for query resource is provided at:

/persistence/version/persistent-unit/metadata-catalog/query/queryName

Metadata for Basket.findAll query is provided at:

http(s)://localhost:7001/eclipselink.jpars.test/persistence/v2.0/jpars_basket-static/metadata-catalog/query/BasketItem.findAll

Another way of getting resource metadata is using OPTIONS method against the resource URL.

For example, metadata for Basket entity can be retrieved this way:

OPTIONS http://localhost:7001/eclipselink.jpars.test/persistence/v2.0/jpars_basket-static/entity/Basket

The response header contains a link to the resource metadata URL with “describedby” rel.

Link: <http://localhost:7001/eclipselink.jpars.test/persistence/v2.0/jpars_basket-static/metadata-catalog/entity/Basket>; rel="describedby",

There are two media types that can be used to describe a resource: application/json and application/schema+json. The default value is the former, which returns a very simple description of the resource, including the resource name, a link to the resource instance, and a link to the json schema of the resource.

For example if application/json is used

GET http://localhost:7001/eclipselink.jpars.test/persistence/v2.0/jpars_basket-static/metadata-catalog/entity/Basket

returns

{
   "name": "Basket",
   "links": [
       {
           "rel": "alternate",
           "href": "<root>/metadata-catalog/entity/Basket",
           "mediaType": "application/schema+json"
       },
       {
           "rel": "canonical",
           "href": "<root>/metadata-catalog/entity/Basket",
           "mediaType": "application/json"
       },
       {
           "rel": "describes",
           "href": "<root>/entity/Basket"
       }
   ]
}

If application/schema+json media type is used (header 'accepts' value 'application/schema+json') to access this resource metadata URI, then a JSON schema for the resource is returned.

{
   "$schema": "<root>/metadata-catalog/entity/Basket#",
   "allOf": [
       {
           "$ref": "rest-schemas/#/singularResource"
       }
   ],
   "title": "Basket",
   "properties": {
       "id": {
           "type": "number"
       },
       "name": {
           "type": "string"
       },
       "basketItems": {
           "type": "array",
           "items": {
               "$ref": "<root>/entity/BasketItem#"
           }
       }
   },
   "links": [
       {
           "rel": "describedby",
           "href": "<root>/entity/Basket"
       },
       {
           "rel": "find",
           "href": "<root>/entity/Basket/{primaryKey}",
           "method": "GET"
       },
       {
           "rel": "create",
           "href": "<root>/entity/Basket",
           "method": "PUT"
       },
       {
           "rel": "update",
           "href": "<root>/entity/Basket",
           "method": "POST"
       },
       {
           "rel": "delete",
           "href": "<root>/entity/Basket/{primaryKey}",
           "method": "DELETE"
       }
   ]
}

Metadata catalog

Metadata catalog is a catalog of all available entities and queries metadata. Metadata catalog is accesible at:

GET <root>/persistence/v2.0/persistent-unit/metadata-catalog

For example, a metadata catalog for jpars_basket-static persistent unit is provided here:

http(s)://localhost:7001/eclipselink.jpars.test/persistence/v2.0/jpars_basket-static/metadata-catalog

Response:

{
   "items": [
       {
           "name": "BasketItem",
           ...
       },
       {
           "name": "Basket",
           ...
           ]
       },
       {
           "name": "BasketItem.deleteAll",
           ...
       },
       {
           "name": "Basket.deleteAll",
           ...
       },
       {
           "name": "BasketItem.findAll",
           ...
       },
       {
           "name": "BasketItem.findAllPageable",
           ...
       }
   ],
   "links": [
       {
           "rel": "canonical",
           "href": "<root>/metadata-catalog"
       }
   ]
}