Resource (REST) programming model and conventions
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>]]
/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 |
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.