HTTP REST API

When a model has been declared, HTTP REST access to your modeled resources can be enabled in just one simple step. This document shows how to enable the HTTP REST API for your declared resource models and shows how the Model API and queries are performed in HTTP.

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.

The URL syntax uses equal signs (=) to set the filter parameters and ampersands (&) to combine the filter conditions.

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
Collection paging uses zero-based indexing. For example, the first member in the collection is indexed at 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.

Specifying ordering in the following section changes the members in specified page ranges.

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.

Version 1.1.26825