Use Dojo's JsonRestStore with your REST services

Learn a simple, generic service implementation

Dojo's JsonRestStore is one of the more advanced options for connecting your REST services to Dojo's data API. If your service does not conform to JsonRestStore's expectations for data structure, the wiring between JsonRestStore and your REST service can become difficult. In this article, learn about a simple, generic service implementation for wiring up nonstandard REST services to JsonRestStore. Learn how to use and extend the implementation for your own services by exploring several examples.

Share:

Nick Maynard, Senior Software Engineer, Industry and Business Solutions Team, IBM

Author photoNick Maynard works for the IBM Software Solutions Transformation team in Hursley, UK. He specializes in Web programming, Linux, Web services, and business integration technologies. You can contact Nick at nick.maynard@uk.ibm.com.



14 December 2010

Also available in Chinese Japanese

Introduction

Dojo's data abstraction layer, dojo.data, provides a standard API for Dojo applications to access a variety of back-end services. JsonRestStore, located in the Dojo extensions library DojoX, lets you quickly wire your applications to back-end Representational State Transfer (REST) services.

JsonRestStore is a good solution for linking Dojo to REST services. By default, though, JsonRestStore has expectations for the format of the information passed to, and returned by, the service. The Service Mapping Description (SMD) facility for customizing these interactions is, at best, difficult to understand.

JsonRestStore is a data store that provides full read, write, and notification capabilities through standards based on HTTP/REST interaction with the server using GET, PUT, POST, and DELETE commands. It lets you communicate with server-side database/persistent data storage using the Dojo Data API with JavaScript, and efficiently handles create, read, update, and delete (CRUD) operations.

When Dojo application authors need to communicate with a preexisting back-end REST store, when the store's communications cannot be changed, they use a custom dojox.rpc.Service implementation.

In this article, learn a simple, generic service implementation for wiring up nonstandard REST services to JsonRestStore. Explore how to use and extend it for your own services by walking through a practical example.

To download the source code for this article, see the Download section.


Example service implementation

Develop skills on this topic

This content is part of a progressive knowledge path for advancing your skills. See Get started with Dojo development

EasyRestService provides implementations for the four REST operations: POST (create), GET (read), PUT (update), and DELETE (delete). For each activity, it provides the ability to hook in before and after invocation of the service method. The invocation life cycle is:

  1. Construct a standard Dojo XHR invocation structure containing the URL, arguments, and data format metadata.
  2. Call the arguments transformer for the method. This method can alter the invocation structure (including the path), and can be used to transform the invocation structure to conform with the REST service's expectations.
  3. Populate the putData/postData/deleteData/getData node of the invocation structure by translating the content node to a JSON representation.
  4. Call the XHR method with the altered XHR invocation structure.
  5. Add a results transformer to the XHR callback. This method can alter the data structure of the results. Use it to transform the results into the structure expected by JsonRestStore.

Listing 1 shows the example code.

Listing 1. EasyRestService source code source
dojo.provide("com.ibm.developerworks.EasyRestService");

(function() {
  var pa = com.ibm.developerworks.EasyRestService = function (path, serviceImpl, schema) {
    // Enforce the dojox.rpc.Rest trailing slash functionality
    path = path.match(/\/$/) ? path : (path + '/');
    
    // A dojox.rpc.Service implementation is a function with 3 function members
    var service;
    // GET function
    service = function(id, args) {
      return _execXhr("get", id, args);
    };
    // POST function member
    service['post'] = function(id, value) {
      return _execXhr("post", id, value);
    };
    // PUT function member
    service['put'] = function(id, value) {
      return _execXhr("put", id, value);
    };
    // DELETE function member
    service['delete'] = function(id) {
      return _execXhr("delete", id);
    };
    
    // Generic XHR function for all methods
    var _execXhr = function(method, id, content) {
      // Transform the method string
      var methodCapitalised = method.substring(0,1).toUpperCase() 
        + method.substring(1).toLowerCase();
      var methodUpperCase = method.toUpperCase();
      var methodLowerCase = method.toLowerCase();
      
      // Get the transformer functions
      var argumentsTransformer = service["transform" + methodCapitalised + "Arguments"];
      var resultTransformer = service["transform" + methodCapitalised + "Results"];
      
      // Construct the standard query
      var serviceArgs = {
        url : path + (dojo.isObject(id) ? '?' + dojo.objectToQuery(id) : 
          (id == null ? "" : id)), 
        handleAs : "json",
        contentType : "application/json",
        sync : false,
        headers : { Accept : "application/json,application/javascript" }
      };
      
      // Transform the arguments
      // NOTE: argumentsTransformer has a reference to "service"
      serviceArgs = argumentsTransformer(serviceArgs, arguments);

      // Copy the content into the appropriate *Data arg
      // getData, putData, postData, deleteData
      // NOTE: If you want your arguments transformer to edit the *Data arg directly, 
      // move the arguments transformer invocation to after this call 
      serviceArgs[methodLowerCase + 'Data'] = content;
            
      // Kick off the call
      var xhrFunction = dojo['xhr' + methodCapitalised];
      var deferred = xhrFunction(serviceArgs);
      // Add our result transformer
      // NOTE: resultTransformer has a reference to "service" too
      deferred.addCallback(dojo.partial(resultTransformer, deferred));
      
      return deferred;
    };

    // Mix in the service hooks
    // Uses a "default" implementation that does nothing
    // Service hooks will have a reference to the "service" object in their context
    dojo.mixin(service, 
      new com.ibm.developerworks.EasyRestService.DefaultHooks(), 
      serviceImpl);
    
    // Now remove any default _constructor() methods
    // This is necessary as the JsonRestStore stack uses _constructor() differently
    delete service['_constructor'];
    // Remove the declaredClass member if it has been added
    delete service['declaredClass'];
    
    // Save the path away
    service.servicePath = path;
    // Save the schema
    service._schema = schema;
    
    return service;
  };
})();

dojo.declare("com.ibm.developerworks.EasyRestService.DefaultHooks", null, {
  transformGetArguments: function(serviceArgs) {
    // Alter serviceArgs to provide the information the backend
    // service requires
    return serviceArgs;
  },
  transformPutArguments: function(serviceArgs) {
    // Alter serviceArgs to provide the information the backend
    // service requires
    return serviceArgs;
  },
  transformPostArguments: function(serviceArgs) {
    // Alter serviceArgs to provide the information the backend
    // service requires
    return serviceArgs;
  },
  transformDeleteArguments: function(serviceArgs) {
    // Alter serviceArgs to provide the information the backend
    // service requires
    return serviceArgs;
  },
  transformGetResults: function(deferred, results) {
    /*
     * JsonRestStore expects the following format:
     * [
     *  { id: "1", ... },
     *  { id: "2", ... },
     *  ...
     * ] 
     */
    return results;
  },
  transformPutResults: function(deferred, results) {
    /*
     * JsonRestStore does not expect any specific content here
     */
    return results;
  },
  transformPostResults: function(deferred, results) {
    /*
     * JsonRestStore expects:
     * 1) A "Location" response header with location of the new item.
     * 		From the Dojo API:
     * 			The server’s response includes a Location header
     * 			that indicates the id of the newly created object.
     * 			This id will be used for subsequent PUT and DELETE 
     * 			requests. JsonRestStore also includes a 
     * 			Content-Location header that indicates the temporary
     * 			randomly generated id used by client, and this 
     * 			location is used for subsequent PUT/DELETEs if no 
     * 			Location header is provided by the server or if 
     * 			a modification is sent prior to receiving a response 
     * 			from the server.
     *    NB: There is no JS method for altering response headers.  
     *      You might wish to try overriding the 
     *      deferred.ioArgs.xhr.getResponseHeader() method with your
     *      own implementation.
     * 2) The new item in the following format:
     * { id: "1", ... }
     */
    return results;
  },
  transformDeleteResults: function(deferred, results) {
    /*
     * JsonRestStore does not expect any specific content here
     */
    return results;
  }
});

The code in Listing 1 replicates the default functions of dojox.rpc.Rest. It can be used with JsonRestStore, as shown in Listing 2.

Listing 2. Default use of EasyRestService with JsonRestStore
dojo.require("com.ibm.developerworks.EasyRestService");
dojo.require("dojox.data.JsonRestStore");

var store = new dojox.data.JsonRestStore({
  service: new com.ibm.developerworks.EasyRestService("https://mydomain.com/restservice"),
  idAttribute : "id"
});

By default, an EasyRestService instance does not alter the arguments and results in any way. Instead of altering DefaultHooks to perform required arguments and results transformation, EasyRestService provides a mechanism for overriding these transformers on a per-instance basis.


Customizing EasyRestService

EasyRestService provides a simple mechanism for the provision of custom transformers. The example in Listing 3 alters the behavior of EasyRestService to log the GET invocation structure before execution.

Listing 3. Customizing EasyRestService
dojo.require("com.ibm.developerworks.EasyRestService");

var transformers = { 
  transformGetArguments: function(args) { 
    console.log(args); 
    return args; 
  }
};
var service = new com.ibm.developerworks.EasyRestService(
  "https://mydomain.com/restservice", transformers);

Similarly, all transformers in DefaultHooks can be overridden on a per-instance basis.

Example

Create two instances of EasyRestService: one for a compliant service and one for a noncompliant service. The example uses them as the service providers for two instances of JsonRestStore, and issues a basic fetch against the stores.

The data stores

In Listing 4 and Listing 5, the services are mocked up using two read-only files containing JSON structures.

Listing 4. Compliant REST service, compliantService.json
[
	{ id: 1, name: "Phil" },
	{ id: 2, name: "John" }
]
Listing 5. Noncompliant REST service, noncompliantService.json
{ items : [
	{ id: 1, name: "Phil" },
	{ id: 2, name: "John" }
] }

Store and service interaction code

JavaScript will instantiate and query the stores using the code in Listing 6.

Listing 6. JavaScript code to interact with the stores
// Create a store using a service that needs no transformations
compliantStore = new dojox.data.JsonRestStore({
  service : new com.ibm.developerworks.EasyRestService(
      "./compliantService.json"),
  idAttribute : "id"
});

// Cause an async fetch from the compliant service
dojo.create("p", {
  innerHTML : "Requesting from compliant service"
}, dojo.body(), "last");
compliantStore.fetch({
  onComplete : function(items, request) {
    console.log(items);
    // Log the number of items fetched
    dojo.create("p", {
      innerHTML : "Got " + items.length + " items from compliant service."
    }, dojo.body(), "last");
  }
});

// Create a store using a service which needs transformations
// to interpret the results
noncompliantStore = new dojox.data.JsonRestStore({
  service : new com.ibm.developerworks.EasyRestService(
      "./noncompliantService.json", {
        transformGetResults : function(deferred, results) {
          // This store wraps its results in an items object
          // so return the items object
          return results.items;
        }
      }),
  idAttribute : "id"
});

// Cause an async fetch from the noncompliant service
dojo.create("p", {
  innerHTML : "Requesting from noncompliant service"
}, dojo.body(), "last");
noncompliantStore.fetch({
  onComplete : function(items, request) {
    console.log(items);
 	// Log the number of items fetched
    dojo.create("p", {
      innerHTML : "Got " + items.length
          + " items from noncompliant service."
    }, dojo.body(), "last");
  }
});

Summary

In this article, you learned how to wire up your own REST services to JsonRestStore. The examples showed a simple method of transforming your service interface to provide the signatures that JsonRestStore requires. For information on the full data structure expected by JsonRestStore, you should refer to the comments in DefaultHooks, the JsonRestStore documentation at DojoCampus, and the API documentation.

For information on the full data structure expected by JsonRestStore, see the Resources section.


Download

DescriptionNameSize
EasyRestService code sample - project archiveEasyRestServiceExample.zip6.5KB

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Web development on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development, Java technology
ArticleID=600700
ArticleTitle=Use Dojo's JsonRestStore with your REST services
publish-date=12142010