HTTP REST API
Sample resource model
The following examples that follow assumes the following persons model has
been declared in /app/models/persons.json:
{
"fields": {
"firstname": {"type": "string"},
"birthdate": {"type": "date"}
}
}
Enabling HTTP access
The HTTP API has many symmetries to the Model API and allows HTTP REST access to models collections and members with very little coding. The HTTP API is not enabled by default but requires you to provide a resource event handler in the /app/resources directory. In its simplest form, the application resource event handler delegates requests to a ZRM-provided resource event handler.
A resource event handler for the persons model located in
/app/resources/persons.groovy only needs to contain:
ZRM.delegate()
Similarly, if you are using PHP, you will first need to declare a dependency
on zero.php in your /config/ivy.xml file. Then you would create a
resource event handler for the persons model located in
/app/resources/persons.php only needs to contain:
<?php
zrm_delegate();
?>
The ZRM resource event handler provides default data access functionality
with HTTP for the data defined in the /app/model file. The remainder of
this section describes that default functionality.
Reading collections
The Model API introduced the concept of a model's collection, Member instances and collection filtering. ZRM's HTTP API maps HTTP requests to invocations on the Model API, providing access to the same collection and member through easy to learn (and construct) URIs.
ZRM follows common REST idioms. Namely, you can use the HTTP GET command for a full or filtered a model's collection and POST member instances into the collection. You can also learn more about Reading and writing model collection member instances.
The HTTP GET URI syntax for addressing model collections is shown in the following example:
<urlPrefix>/<collectionName>[?<queryParameters>]
The following examples are valid URIs for the persons model:
http://localhost/resources/persons
Unfiltered collection of members
http://localhost/resources/persons?firstname__startswith=S
Filtered collection of members where all firstname values start with 'S'
Collection filtering
You can filter collection by limiting the set of members to be returned based on one or more filter conditions. A set of filter parameters is enabled for each collection type based on the data model specified. Some examples were given earlier in this document. Here are some further examples:
http://localhost:8081/resources/persons?firstname__contains=Jo
Filtered collection where member instances' firstname field contains
'Jo'
http://localhost:8081/resources/persons?firstname__contains=Jo&birthdate__day=25,
Filtered collection where member instances' firstname field contains
'Jo' and birthdate falls on the 25th of the any month.
The valid filter conditions and corresponding parameter syntax for the URL is based on the Model API filtering syntax of the Zero Resource Model. This is defined in the Filter conditions section under the Model API topic.
Collection paging
Just as the Model API allows for specifying slice, or subset, of the overall collection, the HTTP API allows you to indicate a page range. This is done by defining the start and count parameters in the URI as follows:
http://localhost/resources/persons?start=100&count=199
Unfiltered collection of members from 101 to 200
http://localhost/resources/persons?firstname__contains=rand&start=5&count=5
Filtered collection of members that contain 'rand' and from 6 to 10
0 and the 10th at 9.
If the data slice specified does not have a complete "page", the returned
members are as expected. For example, if there are 7 members in a given
collection and the first request is for start=0&count=5 then items 1
through 5 are returned. If the second request is made for
start=5&count=5 then items 6 and 7 are returned.
You can optionally specify both start and count. One or both must exist
in order to obtain a slice of the results. The lack of both parameters
means the results is not paged. However, if count is missing, the
results start at the specified start and return up to the end of the
collection. Similarly, if start is missing, the results start at the
beginning of the collection and return the number of members specified by
count.
Collection ordering
As illustrated in the URIs in the previous example, you can order
collections results in your HTTP GET requests. By default, HTTP
collection requests for the Atom representation are sorted by members'
updated value in descending order. Conversely, JSON collection
representations are not sorted, or rather, or sorted from oldest to more
recently created.
The order_by value syntax for the URL is based on the ZRM API syntax
which is defined in the "Model API" section. Ascending order is the
default. To indicated descending order, prefix - before the field name
as show in the previous URI examples:
http://localhost/resources/persons?order_by=birthdate
Unfiltered collection of members sorted youngest to oldest
http://localhost/resources/persons?id__lt=100&order_by=-birthdate,name
Filtered collection of members with id less than 100, sorted first from
oldest to youngest and then by name from A to Z
Collection rendering
At present, collection data can be represented in either JSON or the Atom
syndication format. The desired representation can be specified in the
Accept header of the HTTP GET request by specifying the associated
MIME type. The values application/json and application/atom+xml
render JSON and Atom, respectively, to the response.
An alternative mechanism is provided for requesting a specific
representation by using the URL query parameter format_as. Valid values
for this parameter are json and atom:
http://localhost/resources/persons?format_as=atom
Collection of members with representation returned as Atom.
http://localhost/resources/persons?format_as=json
Collection of members with representation returned as JSON.
The default format is JSON if the Accept header is not specified and the URI parameter is not provided.
JSON representations
ZRM uses a JSON serialization APIs to render the JSON representation. A model collection is a JSON array of objects. Each object contains the member instance of the model in the collection. For example, the persons model when the collection contains two members rendered in JSON is:
[
{
"firstname": "Jane",
"birthdate": "1973-12-03"
},
{
"firstname": "John",
"birthdate": "1982-04-29"
}
]
Atom format
The default Atom representation of a model's collection is as you would
expect: an Atom feed containing an Atom entry for each member. The default
serialization of the model instance member data is put in the
atom:content section as key, value pairs in XOXO format. The remaining
required atom elements are set to default values as shown in the following
example of the default Atom representation of the same data in the JSON
example that was shown above:
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Title of Type persons Feed</title>
<link href="http://localhost/resources/persons?format_as=atom"/>
<updated>2003-12-13T18:30:02Z</updated>
<id>"http://localhost/resources/persons"</id>
<entry>
<title>Title of persons 2</title>
<link href="http://localhost/resources/persons/1?format_as=atom"/>
<link rel="edit" href="http://localhost/resources/persons/1"/>
<id>"http://localhost/resources/persons/1"</id>
<updated>2003-12-13T18:30:02Z</updated>
<author>Author of persons 1</author>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999.xhtml">
<ol class='xoxo'><li>persons<dl>
<dt>firstname</dt><dd>Jane</dd>
<dt>birthdate</dt><dd>1973-12-03</dd>
</dl></li></ol>
</div>
</content>
</entry>
<entry>
<title>Title of persons 2</title>
<link href="http://localhost/resources/persons/2?format_as=atom"/>
<link rel="edit" href="http://localhost/resources/persons/2"/>
<id>"http://localhost/resources/persons/2"</id>
<updated>2003-12-13T18:30:02Z</updated>
<author>Author of persons 2</author>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999.xhtml">
<ol class='xoxo'><li>persons<dl>
<dt>firstname</dt><dd>John</dd>
<dt>birthdate</dt><dd>1982-04-29</dd>
</dl></li></ol>
</div>
</content>
</entry>
</feed>
Customizing the Atom representation
You can use the model declarations in the /app/models folder to
customize the rendering of an Atom entry. You can override named templates
in each model declaration using the calculated_fields definition.
Adding the following definition to the persons model's /app/model/persons.json
file and using the same member instance data in the previous example results in
the following Atom entry:
{
"fields" : { . . . },
"calculated_fields" : {
"atom_author_name": "$member.lastname",
"atom_title": "$member.firstname",
"atom_summary": "All about ${member.firstname}."
},
"calculated_type_fields" : {
"atom_feed_title": "My Custom Feed Title"
}
}
<feed xmlns="http://www.w3.org/2005/Atom">
<title>My Custom Feed Title</title>
<link href="http://localhost/resources/persons?format_as=atom"/>
<updated>2003-12-13T18:30:02Z</updated>
<id>"http://localhost/resources/persons"</id>
<entry>
<title>Jane</title>
<link href="http://localhost/resources/persons/1?format_as=atom"/>
<link rel="edit" href="http://localhost/resources/persons/1"/>
<id>"http://localhost/resources/persons/1"</id>
<summary>All about Jane.</summary>
<updated>2003-12-13T18:30:02Z</updated>
<author>Doe</author>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999.xhtml">
<ol class='xoxo'><li>persons<dl>
<dt>firstname</dt><dd>Jane</dd>
<dt>lastname</dt><dd>Doe</dd>
</dl></li></ol>
</div>
</content>
</entry>
</feed>
Reading and writing collection members
In addition to retrieving members instances of collections as illustrated above, the HTTP API enables the manipulation of member instances.
ZRM follows common REST idioms. Namely, you can use the HTTP GET command to retrieve a single, addressable member instance. Further, you can create new member instances with POST, update a member with PUT, and remove with DELETE.
Member rendering and parsing
Incoming data used for creating and updating members must be either in JSON or
Atom representations. The Content-Type header of the HTTP request determines
how the data is parsed. The MIME types application/json and
application/atom+xml parse JSON and Atom, respectively. If neither is
specified, the default parser is JSON.
Creating a member
New collection members can be created by making an HTTP POST request to the
collection URI of the model with the member representation as shown in the
following example:
POST /resources/persons
Content-Type: application/json
{
"firstname": "Bill",
"birthdate": "1976-09-25"
}
Retrieving a member
An individual collection member can be retrieved by issuing an HTTP GET
on the collection URI with the member ID as a parameter, as shown in the
following example:
GET resources/persons/1
Updating a member
A collection member can be updated by issuing an HTTP PUT with the member
representation on the collection URI, as shown in the following example:
PUT /resources/persons/1
Content-Type: application/json
{
"firstname": "Janie",
"birthdate": "1973-12-04"
}
Deleting a member
A collection member can be deleted by issuing an HTTP DELETE with the member
ID on the collection URI, as shown in the following example:
DELETE /resources/persons/1
Reading type collection metadata
By default, the metadata of a collection can be retrieved through HTTP as a JSON representation. This is done using the following URI convention:
/resources/types[/typesId]
For example, assume that the following resource model is defined:
{
"fields" : {
"first_name" : {"type":"string", "required": true},
"birth_date":{"type":"date"},
"is_child":{"type": "boolean"}
}
}
With this model in place, the following HTTP JSON response is returned:
GET /resources/types/persons
{
"type":"person",
"fields": {
"id": {
"type":"auto",
"required":false,
"editable":true,
"primary_key":true
},
"updated": {
"type":"date-time",
"required":false,
"editable":true,
"primary_key":false
},
"birth_date": {
"type":"date",
"required":true,
"editable":true,
"primary_key":false
},
"first_name": {
"type":"string",
"required":true,
"editable":true,
"primary_key":false,
"max_length":50
},
"is_child": {
"type":"boolean",
"required":true,
"editable":true,
"primary_key":false
}
}
}
By specifying the named resource model /resources/types/persons only the JSON
object for the persons type is returned. You can obtain a list of all resource
model definitions by making an HTTP GET request to /resources/types.
Custom Resource Event Handlers
The Model API can be easily used in resource event handlers for RESTful resource access. If the black box ZRM REST enablement doesn't suit your needs, you can use the API and implement your own RESTful resource handling while still leveraging the simplicity ZRM offers.
The following Groovy example shows a file in the /app/resources/my_json_person.groovy file illustrates how to utilize the API in order to customize you resource handling needs:
import zero.resource.TypeCollection
def onList() {
def collection = TypeCollection.retrieve('person')
request.status = 200
// assign the Collection
request.json.output = collection
request.view = 'JSON'
render()
}
def onCreate() {
def collection = TypeCollection.retrieve('person')
def json = zero.json.Json.decode(request.input[])
def data = collection.type.fromJson(json)
data = collection.create(data)
locationUri = getRequestedUri(false) + '/' + data.id
request.headers.out.Location = locationUri
request.status = 201
request.json.output = data
request.view = 'JSON'
render()
}
def onRetrieve() {
def memberId = request.params.my_json_personId[]
def collection = TypeCollection.retrieve('person')
def data = collection.retrieve(id: memberId )
request.status = 200
request.json.output = data
request.view = 'JSON'
render()
}
def onUpdate() {
def memberId = request.params.my_json_personId[]
def collection = TypeCollection.retrieve('person')
def json = zero.json.Json.decode(request.input[])
def data = collection.type.fromJson(json)
data.id = memberId
data = collection.update(data)
request.status = 204
}
def onDelete() {
def memberId = request.params.my_json_personId[]
def collection = TypeCollection.retrieve('person')
collection.delete(memberId)
request.status = 204
}
The following PHP example shows a file in the /app/resources/my_json_person.php file illustrates how to utilize the API in order to customize you resource handling needs:
<?php
class My_json_person{
function onList() {
$collection = zrm_retrieve('person');
zput("/request/status", 200);
zput("/request/headers/out/Content-Type", "application/json");
print json_encode($collection);
}
function onCreate() {
$collection = zrm_retrieve('person');
$json = json_decode( $HTTP_RAW_POST_DATA );
$data = $collection->createFromArray($json);
$locationUri = get_requested_uri(false) . '/' . $data->id;
zput( "/request/headers/out/Location", $locationUri );
zput("/request/status", 201 );
//render as json
zput("/request/headers/out/Content-Type", "application/json");
print json_encode($data);
}
function onRetrieve() {
$memberId = zget("/request/params/my_json_personId");
$collection = zrm_retrieve('person');
$data = $collection->retrieve( $memberId );
zput("/request/status", 200 );
zput("/request/headers/out/Content-Type", "application/json");
print json_encode($data);
}
function onUpdate() {
$memberId = zget("/request/params/my_json_personId");
$collection = zrm_retrieve('person');
$json = json_decode( $HTTP_RAW_POST_DATA );
$data = $collection->createFromArray( $json );
$data->id = $memberId;
$data = $collection->update($data);
zput("/request/status", 204);
}
function onDelete() {
$memberId = zget("/request/params/my_json_personId");
$collection = zrm_retrieve('person');
$collection->delete($memberId);
zput("/request/status", 204);
}
}
?>
Overriding ZRM.delegate()
It is possible to delegate into the ZRM HTTP default implementation while also
providing functionality. In order to do this, first create your own Resource
Event Handler as shown in the previous example. Instead of completely overriding
all behavior, simply call ZRM.delegate() in each method. You can provide
your own custom logic before and after the ZRM.delegate() method call.
Make the Collection read-only
Because ZRM.delegate() can be called within resource even method, you can
make your Collection read-only by not implementing the methods that write. For
example, the following only implements the onList() and onRetrieve()
read-only methods:
def onList() {
ZRM.delegate()
def collection = event.resource.collection[]
}
def onRetrieve() {
ZRM.delegate()
def collection = event.resource.collection[]
def retrievedMember = event.resource.member[]
}
Similarly in PHP you can use the zrm_delegate() function and the following code
to accomplish the same results as above.
class Myresource {
function onList() {
zrm_delegate();
$collection = zget("/event/resource/collection");
}
function onRetrieve() {
zrm_delegate()
$collection = zget("/event/resource/collection");
$retrievedMember = zget("/event/resource/member");
}
}
By only implementing these methods, there are no data altering methods for the
resource event framework to call. In this example, if a client does attempt to
send an HTTP request to POST, PUT, or DELETE an HTTP status code
of 405 Method Not Allowed is returned.
Using the Collection and Member
Further, you can obtain a reference to relevant objects after making the call
into the ZRM black box implementation. Collection and Member objects
are made available in the /event/resource zone upon completion of the
ZRM.delegate() call. Consider the following example:
def onList() {
ZRM.delegate()
def collection = event.resource.collection[]
}
def onCreate() {
ZRM.delegate()
def collection = event.resource.collection[]
def createdMember = event.resource.member[]
}
def onRetrieve() {
ZRM.delegate()
def collection = event.resource.collection[]
def retrievedMember = event.resource.member[]
}
def onUpdate() {
ZRM.delegate()
def collection = event.resource.collection[]
def updatedMember = event.resource.member[]
}
def onDelete() {
ZRM.delegate()
def collection = event.resource.collection[]
}
It is important to note that once the ZRM.delegate() method is called, all
HTTP response processing has been completed. Thus, the HTTP method has been set
and any response body content has been set. Logic done before the
ZRM.delegate() call can affect the response, however.
Using the JSON Renderer
The example in the previous section illustrates how to use the TypeCollection,
Type, Collection, and Member classes in your own resource event handler.
Collection, Type, and Member classes can be assigned as JSON output and handled
by custom converters provided by the zero.resource component. In fact, the
black box HTTP API leverages the JSON rendering framework as well.