Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
rdiger_plantiko2
Active Contributor

In a previous blog, I described a very general XML format for Ajax. In this blog, I show how this format can be used to build a web client against a REST API. For simplicity, I developed an API which allows to maintain a single database table. Although simple, it shows the essential ideas and can be thought as a building block for more complex applications.

The web client consists of an HTML page, JavaScript code, and two XSLT transformations which are executed in the browser. Basically, these objects could be hosted on any web server, it doesn't have to be the SAP Web AS. But when working with the SAP Web AS, Business Server Pages are the right framework for this: We need to roll our own application-specific JavaScript and HTML code, and we want the full control over every single byte going out to the client.

Why REST?

When entering an URL in the browser, it displays the web content that corresponds to this URL. This is a functional correspondence: The detailed content is a function of that URL alone (at least for static web pages, which do not change in time). Technically, the browser performs a GET request for that URL, with empty HTTP body.

This "functional" view - considering the complete ressource as a function of the URL, "ressource = f( URL )" -  extends in a natural manner to remote objects. An URL can be seen as a short form of an object. The complete data describing the object can be called using an HTTP GET request. Frequently, the ID or number of an object will then be part of the URL, so that the URL path ends with a readable expression, like ".../order/4711".

In order to remotely change, create, or delete objects, further HTTP methods apart from GET have to be considered: Natural choices are PUT for create and change operations, DELETE for deletion, and POST for actions which change state on server side, but cannot be seen as an operation on a single object. For all these operations, it makes sense to use the HTTP request body to carry further information: In the PUT case, for example, the body may contain the representation of the object that is to be created or changed.

REST builds on HTTP as transfer protocol and exploits it in more detail than alternatives like SOAP. While in SOAP all the context of the operation is carried as "envelope", as part of the HTTP request body, REST uses HTTP itself to convey this context: HTTP status codes, HTTP methods, HTTP header fields like "Accept" are used for the negotiations between client and server. REST can be seen as a natural application of HTTP for remote object access. It is tied to HTTP, re-using the HTTP features  (which can be seen as a disadvantage as well: It's not possible to use REST with another protocol than HTTP).

A Sample REST Service

Every complex application decomposes into simple building blocks. If a relational database system is used for modelling, such a simple building block could be  responsible for the maintenance of the data contained in one particular database table. It is more than what people usually call a DAO (Data Access Object), because it not only takes care about the database updates, but also incorporates the rules connected with these data: consistency checks, logic, mapping, and dependencies with other entities of the system.

For test purposes, I have written a little HTTP service for handling an imaginary table of  a "Batch Job Management" system. Imagine an application for planning all the batch jobs running in the system.

Well, we have SM36 and SM37, you might say...

But we may want some additional information beyond the SM37 data: For each job, we want not only to know the variant and report name to be started, but also a contact person, a text for short notes, a priority (how important is it that this job runs?),  and the info whether this job can simply be restarted after a failure.

These job attributes are kept in database table ZJOBS, with a 4-digit ID as key. The service under the URL

http://bsp.mits.ch/buch/job/attributes/

accepts GET, PUT and DELETE requests (and a special POST request). As response, it sends XML data back to the client, in the Ajax Response format described in my earlier blog.

A single entity - a row in the database table - is described by these XML data:

<job id="0001">
  <descr>Output all sales order confirmations</descr>
  <repid>RSNAST00</repid>
  <varid>UXPD_KUBE_KV</varid>
  <prio>2</prio>
  <restart>X</restart>
  <contact>Rainer Zufall</contact>
</job>

The ID of the job in question may be appended to the URL path, if the operation requires an identified object. So, for example, the HTTP request

DELETE attributes/0001

denotes the request to delete the job with the ID 0001.

Additionally, this test service offers a special POST request,

POST attributes/reset

which restores the test data.

The service is written as an ABAP request handler. In a forthcoming blog I will give some details on writing REST services in ABAP.

The focus in the present blog is on how to write web applications based on such services.

The Application

I have written a demo BSP application, accessible under the URL http://bsp.mits.ch/buch/zz_jobs/jobs.htm. Here is how it looks (the button and table design is shamelessly stolen from the SAP Retail Store design):

You see that the application is composed of four parts:

  • A message area, reserved for the display of single message lines,
  • An input area, consisting of input text fields, listboxes, checkboxes, for changing the attributes of a single job
  • A button area, presenting the different choices of user commands,
  • A table area, displaying the actual content of the job table.

The HTML code and the JavaScript code should reflect this visible structure. Before going into more details, I want to talk about one of the most important parts of any development:

Testing

When the application is in being, it's the best time to start with tests. Used at an early stage, tests help to discover bugs at the root - immediately when they are produced. This early, they are easy to analyze and to fix. During development, I can add scenario after scenario to an automated test suite, ensuring all the processes are performed properly.

In a very popular browser comparison, Firefox is accused for its numerous plugins. Also, Firefox clearly doesn't win the browser speed contest. There may be some truth in these critiques, but there are two plugins that I find particularly useful and that make Firefoy my favorite browser for developing web applications: The Selenium IDE, and Firebug. With these plugins, it becomes very easy to detect bugs and to keep the functionality stable during developing. Here is a screenshot of some tests in action (click to enlarge the image):

The JavaScript

Ordered from most abstract to most specific, I use the following JavaScript ressources (each of them small in size):

  • A minimalistic JavaScript library minlib.js (which is part of the global.js of my BSP framework), the code requiring about 7 KB. This is not an essential ingredient of the application. It only helps writing shorter JavaScript, by providing shortcuts and browser-independent functions for the most common JavaScript calls. For example, the function byId( ... ) simply is a shortcut for document.getElementById( ). Also, the function doRequest() is a cross-browser abstraction of the XMLHttpRequest object.
  • Sarissa.js which helps handling XSLT transformations and XML Documents in the browser. Needs 12 KB.
  • globalAjax.js - a set of objects for connecting XML data with web objects like tables or input fields. Needs 3.5 KB.
  • Application-specific JavaScript code. This is the specific, non-reusable part of the code for serving this particular application. It requires 10 KB. I left it in the source code of the HTML page for easy inspection. Usually, this code will be an own file belonging to the MIME objects of an application.

On top of all the code are the user actions, triggering the followup actions. For example, there is a Save button, defined in the BSP layout with the help of a BSP extension element:

            <z:button  fcode     = "SAVE"
                              onClick   = "saveJob();"
                               text      = "<%=otr(Z_MVC_EXAMPLE/SICHERN)%>"/>

This renders the button

and attaches the function saveJob() as click handler (purists may argue that a DOM level 2 registration would be better here - and they are right. But this is a detail.) When the user hits the button, the function will be called. It has to

  • take action only if there is at least a report name specified in the input area,
  • take the data from the input area and put them into a "job" XML structure,
  • call the PUT method of the JobAttributes API, with that XML document in the HTTP body,
  • display a potential message, and update the row in the job table, as soon as the API comes back.

This is the function which affords all this:

       function saveJob() {
         if (notEmpty("repid")) {
           callJobAttributes("PUT",function() {
             jobTable.updateRows( this.responseXML );
             });
           }
         }

Similarly, the deleteJob() function looks like this:

       function deleteJob() {
         if (notEmpty("id")) {
           callJobAttributes("DELETE",function() {
             jobTable.deleteRows(this.responseXML);
             resetInput();
             });
           }
         }

Working With the Response XML Data

The common function callJobAttributes() is responsible for calling the Job Attributes API. It collects the input area into an XML document, attaches it to the HTTP body of a request, sends this request as "Ajax request", registering a handler for callback when the response comes in.

// Call the "job attributes" API
// "action" = HTTP action: One of PUT, POST, GET, DELETE, OPTIONS
       function callJobAttributes(action, handler, suffix) {
         var id = getText("id");
         var url = ajaxBaseURL +
                   "/attributes/" +
                   (suffix === undefined ? id : suffix );
         var jobDOM = xmldocFromInputFields("repid,varid,prio,descr,restart,contact","job");
         byTagName("job",jobDOM)[0].setAttribute("id",id);
         sendAjaxRequest( url, handler, jobDOM, action);
         }

At this point, we are in the globalAjax.js layer: The functions xmldocFromInputFields() and sendAjaxRequest() are general in nature - not specific to this application. Therefore, they are implemented in globalAjax.js.

// Send an Ajax request, handle message element in response
// Do followup action only if there was no error
    function sendAjaxRequest( url, handler, data, action ) {
        doRequest( url, function() {
            if (!this.responseXML) {
                throw "No response to HTTP request " + url;
                }
            checkMessage( this.responseXML );
            if (handler && !errorOccurred()) handler.call(this);
            }, data, action,
            {"Content-Type":"text/xml; charset=ISO-8859-1"});  // still...
      }


Here, doRequest() is the blank XMLHttpRequest function itself (located in the most general layer, in the JavaScript framework minlib.js).

If the Content-Type of the response ist text/xml, the data browser will parse the data of the response into an XML document, accessible as attribute responseXML of the XMLHttpRequest object. It can be evaluated with DOM functions. For example, the method checkMessage() looks for the existence of a <message> element and, if so, renders the message with the given text and messsage type. If there are <field> child elements, then those will be rendered as error fields:

// Checks whether the passed DOM object contains a <message> element
// If so, the message text is placed into the application's message area
// and given the style according to its type
// Also, erroneous input <field>s are marked
var msgClass = {   E:"error",   W:"warning",  I:"info",  S:"success",   A:"error",  X:"error"   };
function checkMessage(xmlNode,theMsgId) {     
  var msgId = theMsgId || "msg" ;     
  resetMessage(msgId); 
  byId(msgId).className="";
  byTagName("input").each( resetError );
  byTagName("message",xmlNode).each( function(msg) {
    var msgType = msg.getAttribute("type");     
    setMessage(msgId,msg.getAttribute("text"),msgClass[msgType]);
    if (msgType.match(/[EAX]/)) {     
      byTagName("field",msg).each(  function( field ) {     
        setErrorField(field.getAttribute("name"));     
        });
      }
    throw $break; // There is only one message element   
    });     
  }  

The Field Abstraction

When filling control elements like input fields, checkboxes, listboxes or text areas with data from the server, it is of advantage to have a unified interface for setting and getting data from it. Sometimes, only the value attribute has to be filled - like for input fields. For checkboxes, the ABAP flag value "X" should correspond to setting the checked attribute. For listboxes, a preprocessing should be performed to adjust the set of available options. All these functions should be addressed by an identical interface.

This can be performed with the Field abstraction. The call of Field([id]) retrieves or creates an object instance with a set(value) and a get( ) function.

Consider, for example, the statement

Field("restart").get();

This should query the state of the checkbox "restart". If it is checked, the call should return an "X" - if not, " ".  Similarly, the following statements sets the checked attribute:

Field("restart").set("X");

Due to the unified interface, the checkbox can be treated like any other input field when data are passed to or retrieved from the server.

Another example:

Field("varid").set("test");

This should not only set the selected option of the "varid" drop-down field to "test": Before this happens, it has to verify that the current set of variants still corresponds to the current report id. If not, the new variants have to be retrieved from the server, and only after that, the value has to be set.

Let's start with an instance management, attaching objects to  id's:

function Field(idOrNode) {
  var field = getElement(idOrNode);
  var f = Field.functions[field.id];
  if (!f) {
    f = Field.setNew(field);
    }
  return f;
  } Field.functions = {};

By default, there is some standard behaviour, depending on the element name and its "type" attribute:

Field.setNew = function(field) {
  var type = (field.nodeName == "INPUT") ? field.type : field.nodeName;
  var f;
  switch (type) {
    case "checkbox":
      f = new CheckboxField(field);
      break;
    case "SELECT":
    case "TEXTAREA":
    case "text":
      f = new InputField(field);
      break;
    default:
      f = new SpanTextField(field);
    }
  return (Field.functions[field.id] = f);
  }

Now, the standard InputField object type is defined quite straightforward:

function InputField(field) {
  this.field = field;
  }
InputField.prototype.set = function(value) { this.field.value = value; };
InputField.prototype.get =   function() { return this.field.value; } ;

For checkboxes, there is a little bit more logic:

function CheckboxField(field) {
  this.field = field;
  }
setAttributes(CheckboxField.prototype, {
  set: function(value) { this.field.checked = (value == "X") ; },
  get: function() { return this.field.checked ? "X" : " "; }
  });

(Here, the minlib framework function setAttributes(left,right) copies all members of the right object to the left object.)

The Self-Actualizing Drop-Down Box

As the final Field() example, here is the logic for a listbox with automatic actualizion of its option set - the most complex Field instance type.

The drop-down box for variants obviously depends on the content of the input field containing the program.  

The options are to be re-selected whenever the program name is changed. With a GET request, the new options can be retrieved from the server:

As in more complex cases, a little stylesheet to be executed on the client will transform these data into HTML elements. The resulting <option> elements will then be the new content of the drop-down field.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:template match="report">
    <select>
      <xsl:apply-templates/>
    </select>
  </xsl:template>
  <xsl:template match="variant">
    <option value="{@name}">
      <xsl:value-of select="@text"/>
    </option>
  </xsl:template>
</xsl:stylesheet>


The drop-down box has to be initialized once at page loading, by calling new Select( ). Once this is done, the Field("varid").set() method works as expected: On change of repid, the new variants will be reselected before the value is set.

        variantsSelect =
            new Select("varid",
                function(){
                    return ajaxBaseURL + "/variants?repid="+getText("repid")
                  },
                "xslt/variants.xsl");


Here is the implementation of those dropdown Fields. The constructor needs


  • the ID of the select box
  • a function getOptionsURL() returning the URL which can be used to retrieve the current set of listbox values
  • a string transformOptionsURL pointing to an XSLT, transforming the XML response of the getOptions call into a list of HTML <option> elements.

Observe that the implementation is generic. It contains nothing special to our sample application. Select objects for self-actualizing drop-down boxes can be reused in any other web application, and there can be multiple instances, i.e. multiple drop-down boxes per page handled in this manner.

// An abstraction for dropdown fields which are actualized by
// XML documents from the server
function Select(selectElement,getOptionsURL,transformOptionsURL) {
  this.selectElement       = getElement(selectElement);
  this.getOptionsURL       = getOptionsURL;
  this.transformOptionsURL = transformOptionsURL;
  this.transformer         = null;
  this.lastURL             = "";
  var selectObject         = this; // For the closure...
  if (this.selectElement.id) {
// Special rule for putting a value into the listbox
// Determine the proper selections before setting the value
    Field(this.selectElement.id).set = function(value) {
      selectObject.actualizeAndSetValue(value);
      };
    }     
  }
setAttributes( Select.prototype, {
// A synonym, for better readability of the client code 
  actualizeAndSetValue:function(setValue) {
    this.actualize(setValue);
    },
// Use this method without argument if you only want to update the listbox   
  actualize:function(setValue) {
    var select = this;
    var newURL = this.getOptionsURL();
    if (this.lastURL != newURL) {
      if (this.transformer) {
        doRequest( newURL, function() {
          checkMessage(this.responseXML);
          Sarissa.clearChildNodes(select.selectElement);
          var dom = select.transformer.transformToDocument( this.responseXML );
          byTagName("option",dom).each(function(opt,i) {
            var option = new Option(
              opt.firstChild.data,
              opt.getAttribute("value")
              );
            select.selectElement.options[i] = option;         
            });
          if (typeof setValue== "string") select.selectElement.value = setValue;
          select.lastURL = newURL;
          });
        }
      else {
        doRequest( this.transformOptionsURL, function() {
          select.transformer = new XSLTProcessor();
          select.transformer.importStylesheet(this.responseXML);
          select.actualize(setValue);
          });
        }
      }
    else {
      if (typeof setValue== "string") select.selectElement.value = setValue;
      }
    }
  });

The Table Abstraction

Let's come back to the example of the "save" button: When the user hits it, the following function is triggered:

 function saveJob() {
         if (notEmpty("repid")) {
           callJobAttributes("PUT",function() {
             jobTable.updateRows( this.responseXML );
             });
           }
         }

The update of the visible data table from the responseXML data coming back from the server, is performed with the method updateRows() of the jobTable object. It transforms the result into a collection of <TR> elements and copies them into the page. This is an instance of another abstraction - the Table abstraction.

When the page is loaded, the table object has to be created and provided with

  • a container element of the page, designed as carrier of the table, and
  • the URL of the post-processing XSLT transformation which maps the Ajax result into HTML element data:

jobTable = new Table("jobTableArea","xslt/tableTransformer.xsl");

Once the object is created, methods like updateRows(xmldoc) and deleteRows(xmldoc) can be called in the Ajax call-back, using the xmlResponse document as argument. Only requirement for this to work is that the HTML table rows (the <TR> elements) should have an ID. This is necessary since the table performs a per-row update and delete, making it possible to transfer only subsets of the rows.

// An abstraction for dynamic tables, which are actualized by
// XML documents, to be transformed into HTML with a "row transformer"
function Table(tableArea,rowTransformerURL) {
  this.tableArea      = getElement(tableArea);
  this.table          = null; // will be filled later
  this.body           = null; // will be filled later
  this.rowTransformer = null; // will be loaded when required
  this.rowTransformerURL = rowTransformerURL;
  }
setAttributes( Table.prototype, {
  buildInitial: function( xmldoc ) {
  var tableArea = this.tableArea;
  var table = this;
  Sarissa.clearChildNodes(tableArea);
  this.transform(xmldoc, function() {
    Sarissa.moveChildNodes(this,tableArea);
    table.table = byTagName("table",table.tableArea)[0];
    table.body  = byTagName("tbody",table.tableArea)[0];
    }, true);
  },
  transform: function( xmldoc, callback, with_title_row ) {
    var table = this;
    if (this.rowTransformer) {
      this.rowTransformer.setParameter(null,"with_title_row",!!with_title_row);
      callback.call( table.rowTransformer.transformToDocument( xmldoc ) );
      }
    else {
      // Load XSLT transformation for table rows
      doRequest(this.rowTransformerURL, function() {
        table.rowTransformer = new XSLTProcessor();
        table.rowTransformer.importStylesheet(this.responseXML);
        table.transform( xmldoc, callback, with_title_row );
        });
      }
    },
  updateRows:function(xmldoc) {
    var table = this;
    this.transform( xmldoc, function() {
      var result = this;
      var rows = byTagName("tr", result);
      rows.each( function(row) {
          var id = row.getAttribute("id");
          if (!byId(id)) {
// Row does not exist yet - create
            table.table.insertRow(-1).id = id;
            table.actualizeZebraStripes();
            }
// Row exists - fill with data
          table.updateCellData(row);
        });
      }); 
    },
  deleteRows:function(xmldoc) {
    var table = this;
    this.transform( xmldoc, function() {
      byTagName("tr",this).each( function(row) {
        table.body.removeChild(byId(row.getAttribute("id")));
        });
      table.actualizeZebraStripes();
      });
    },
  updateCellData: function( xmlrow ) {
    if (!xmlrow) return;
    var target = byId( xmlrow.getAttribute("id") );
    Sarissa.copyChildNodes(xmlrow, target);
    },
  actualizeZebraStripes: function(xmldoc) {
    var rows = byTagName("tr",this.body);
    rows.each( function(row,i) {
      row.className = "zebra" + (i%2+1);
      });
    },
  getCellsForRow: function(id){
    var cells = byId("row_"+id).cells;
// Make it a "real" array, with all array methods accessible:   
    return Array.prototype.slice.call(cells,0);
    }
  });

Observe again that Table(), like Field(), is a real abstraction. It doesn't contain any application-specific code and is therefore located in globalAjax.js. The time to implement it is spent only once. In a particular application, there is only a small XSLT transformation necessary to adapt the data from the XML response format to HTML. From there, they can be imported into the HTML document.

Summary

In this blog, I have shown how to implement a web application as client for a REST API, using XML as transfer language (which once was the intended format for Ajax). For test purposes, I exposed a sample REST service http://bsp.mits.ch/buch/job/attributes/  and an application http://bsp.mits.ch/buch/zz_jobs/jobs.htm working against this service.

I discussed the different areas of the user interface - the message area, the input area, the button area and the table area. I showed how to connect them with each other, and how to let them communicate with the server. The basic idea is to use an XSLT enabled JavaScript framework like Sarissa for postprocessing the response XML documents into HTML fragments. Those fragments can then be spliced into the current HTML page using methods like Sarissa.copyChildNodes(). By separating the concrete applicational code from "abstractions" - like the Field abstraction or the Table abstraction, the latter can be reused in similar tasks.

Although the web application has been written as BSP, there is no line of ABAP code involved. The BSP only serves as container for HTML, CSS, JavaScript and XSLT ressources. This is no "must", of course. There may be presentation logic which could be comfortably implemented with the help of ABAP. But the separation between "presentation logic" and "business logic" is the REST API.