Resource (REST) programming model and conventions

IBM® WebSphere® sMash simplifies the task of creating applications using the Representational State Transfer (REST) architectural style.

RESTful design

RESTful designs often make use of collections. A collection is a simple model for manipulating a set of resources. Collections have member items that you can add, remove, update and delete. You can also get a list of members in the collection. For example:
HTTP Method    URI                   Description
-----------    ---                   ----------------
GET            /people               list members
POST           /people               create member
GET            /people/1             retrieve member
PUT            /people/1             update member
DELETE         /people/1             delete member

Simple convention for resources

WebSphere sMash supports the collection model natively within the <apphome>/app/resources virtualized directory. Each script within the resources directory represents a resource handler, which implements the collection and member operations. Resource handlers are accessed via a simple URL convention, based upon the following pattern:
/resources/<collection name>[/<member identifier>[/<path info>]]
For example, a request to /resources/people/1 has the collection name "people" and member identifier "1". The collection name identifies the resource handler (here, <apphome>/app/resources/people.groovy) and the value of member identifier is provided as a request parameter with name "<collection name>Id" (here, zget("/request/params/peopleId") == 1). The resource handler is a WebSphere sMash event handler, designed to handle resource CRUD events. HTTP methods are mapped into collection and member events according to the following:
Resource GET PUT POST DELETE
Collection list putCollection create deleteCollection
Member retrieve update postMember delete
Following is a more detailed example of request patterns and corresponding events and parsed data for handlers written in Groovy. The mappings for PHP handlers are detailed here.

Groovy Resource Handler Mappings

HTTP Method URI ...invokes method in app/resources/people.groovy ...with event data
GET /resources/people onList()  
POST /resources/people onCreate()  
GET /resources/people/1/accounts onRetrieve() zget("/request/params/peopleId")==1
zget("/event/pathInfo")==/accounts
PUT /resources/people/1 onUpdate() zget("/request/params/peopleId")==1
DELETE /resources/people/1 onDelete() zget("/request/params/peopleId")==1

Note: Many HTTP servers redirect URIs that do not contain a trailing slash (i.e. "/resources/people" from the example above) to the URI with a trailing slash appended. WebSphere sMash does not redirect RESTful resources that lack a trailing slash in order to maintain a consistent URI space and for readability. Addressing a RESTful resource with a trailing slash will result in a 404 error.


Sample collection handler

Showing a Groovy implementation: <apphome>/app/resources/people.groovy
def onList() {
    // Get configured DataManager for data access
    def data = zero.data.groovy.Manager.create('peopleDB')

    def result = data.queryArray('SELECT * FROM people')

    // Serialize list to JavaScript Object Notation format
    request.view = 'JSON'
    request.json.output = result
      
    render()
}

def onCreate() {
    // Convert entity to JSON object
    def emp = zero.json.Json.decode(request.input[])

    def data = zero.data.groovy.Manager.create('peopleDB')

    def memberId = data.insert(
                      """INSERT INTO people (firstname, lastname)
                         VALUES ($emp.firstname, $emp.lastname)""", ['id'])

    // Set a Location header with URI to the new record
    locationUri = getRequestedUri(false) + '/' + memberId
    request.headers.out.Location = locationUri
    request.status = HttpURLConnection.HTTP_NO_CONTENT
}

def onRetrieve() {
    // Member id is parsed from the path
    String id = request.params.peopleId[]

    def data = zero.data.groovy.Manager.create('peopleDB')
    def person = data.queryFirst("SELECT * FROM people WHERE id=$id")

    if(person!= null) {
        // Use ViewEngine JSON rendering
        request.view='JSON'
        request.json.output = person
     
        render()
    
    } else {
        // Error handling
        request.status = HttpURLConnection.HTTP_NOT_FOUND
        request.error.message = "username $username not found."
        request.view = 'error'
        render()
    }
}
 
def onUpdate() {
    def emp = zero.json.Json.decode(request.input[])

    def data = zero.data.groovy.Manager.create('personDB')

    data.update("UPDATE employees "
        + "SET firstname=$emp.firstname, lastname=$emp.lastname "
        + "WHERE id=$emp.id")

    request.status = HttpURLConnection.HTTP_NO_CONTENT
}
 
def onDelete() {
    def id = request.params.peopleId[]
    def data = zero.data.groovy.Manager.create('personDB')

    data.update("DELETE FROM people WHERE id=$id");

    request.status = HttpURLConnection.HTTP_NO_CONTENT
}

PHP Resource Handler Mappings

PHP allows a resource handler to scripted as a regular script or through a set of functions defined in a class.

Simple Script dispatch

Using this approach the script is executed for a resource request and it is up to the developer to code for the different methods or events. Since the script is executed for both collection and member resources, distinguishing a GET method for a collection (List) from a GET method for an item (Retrieve) can be done using the value of /event/_name key from the global context.

Values of <resource>Id and pathInfo are added to the request parameters and event zone respectively as shown in the following table. Also, the script people.php is executed for each of the URI in the table.

Method URI ...with GC data
GET /resources/people zget('/event/_name') = 'list'
POST /resources/people zget('/event/_name') = 'create'
PUT /resources/people zget('/event/_name') = 'putCollection'
DELETE /resources/people zget('/event/_name') = 'deleteCollection'
GET /resources/people/1/acc zget('/event/_name') = 'retrieve'
zget('/request/params/peopleId') = 1
zget('/event/pathInfo') = /acc
PUT /resources/people/1 zget('/event/_name') = 'update'
zget('/request/params/peopleId') = 1
POST /resources/people/1 zget('/event/_name') = 'postMember'
zget('/request/params/peopleId') = 1
DELETE /resources/people/1 zget('/event/_name') = 'delete'
zget('/request/params/peopleId') = 1

The code sample below shows how this can be combined with the /request/method key from the global context.

<appRoot>/app/resources/people.php
<?php

switch (zget('/request/method')) {
        case 'GET':
        if (zget('/event/_name') == 'list') {
                // Perform a list specific operation on the collection
        } else {
                // /event/_name = "retrieve"
                // Retrieve the resource
        }
        break;
        case 'POST':
        // do POST specific handling
        break;
        case 'DELETE':
        // do DELETE specific handling
        break;
        case 'PUT':
        // do PUT specific handling
        break;
}

?>

Method dispatch

The script developer can delegate the method-specific handling to WebSphere sMash by defining a class with the with the resource name and specifying the operations on the resource by defining the required functions in the class based on the table below. Values of <resource>Id and pathInfo are added to the request parameters and event zone respectively as shown in the following table.

Method URI ...invokes handler in app/resources/people/ ...with event data
GET /resources/people people.php
People:onList()
 
POST /resources/people people.php
People::onCreate()
 
PUT /resources/people people.php
People::onPutCollection()
 
DELETE /resources/people people.php
People::onDeleteCollection()
 
GET /resources/people/1/acc people.php
People::onRetrieve()
zget('/request/params/peopleId') = 1
zget('/event/pathInfo') = /acc
PUT /resources/people/1 people.php
People::onUpdate()
zget('/request/params/peopleId') = 1
POST /resources/people/1 people.php
People::onPostMember()
zget('/request/params/peopleId') = 1
DELETE /resources/people/1 people.php People::onDelete() zget('/request/params/peopleId') = 1
<appRoot>/app/resources/people.php
<?php
// Initialization common to all operations
// Get configured (as 'theDB' in zero.config) DataManager for data access
$dataManager = data_manager('theDB');
class Employees {
        function onList() {
                global $dataManager;
                // Retrieve employee records via WebSphere sMash
                $employeeRecords = dataExec($dataManager, "SELECT * FROM employees");
                // Use the zero global context to render the data as JSON
                zput('/request/view', 'JSON');
                zput('/request/json/output', $employeeRecords);
                render_view();
        }
        function onCreate() {
                global $dataManager;
                // Convert the raw JSON stream in to a PHP array
                $er = json_decode($HTTP_RAW_POST_DATA);
                $result = dataExec($dataManager, "INSERT INTO employees (username, , location, phonenumber) ". 
                "VALUES (?, ?, ?, ?, ?)", array($er['username'], $er['location'], $er['phonenumber']));
                // Set a Location header with URI to the new record
                $locationUri = get('/request/path') . "/" . $er["username"];
                zput('/request/headers/out/Location', $locationUri);
                zput('/request/status', 204);
        }
        function onRetrieve() {
                // Get configured DataManager for data access
                global $dataManager;
                // WebSphere sMash puts 'itemId' when accessing restful resources 
                $username = zget("/request/params/employeesId");
                // Retrieve employee record via WebSphere sMash
                $employeeRecord = dataQueryFirst($dataManager, "SELECT * FROM employees WHERE username=?", array($username));
                if(isset($employeeRecord)) {
                        // JSON encode employee record
                        zput('/request/headers/out/Content-Type', 'text/json');  			
                        echo json_encode($employeeRecord);
                } else {
                        // Error handling; return a custom error page
                        zput("/request/status", 404);
                        echo "username ". $username . " not found.";
                }
        }
        function onUpdate() {
                // Get configured DataManager for data access
                global $dataManager;
                // By convention, WebSphere sMash places posted text/json into the GC as a JSON object
                $er = json_decode($HTTP_RAW_POST_DATA);
                $result = dataExec($dataManager, "UPDATE employees location=?, phonenumber=? WHERE username=?",
                array($er['firstname'], $er['phonenumber'], $er['username']));
                zput("/request/status", 204);
        }
        function onDelete() {
                // Get configured DataManager for data access
                global $dataManager;
                $username = zget("/request/params/employeesId");
                // Delete employee record via WebSphere sMash
                $result = dataExec($dataManager, "DELETE FROM employees WHERE username=?", array($username));
                zput("/request/status", 204);
        }
}
?>

NOTE: The script is always invoked prior to the method dispatch. This means that any code outside the class definition (including that in any includes) will be executed for all operations. It is recommended that only common code applicable to all operations be placed outside the class definition.

NOTE: As WebSphere sMash does not buffer output, any PHP code that generates output would effectively commit that response and headers to the client. Subsequent header or status manipulation would be ignored.

Version 1.1.0.0.21442