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 two recent blogs, I demonstrated how to write web clients of REST APIs - with XML (demo application here) or JSON (demo application here) as data transfer format. In this blog, I will focus on the server side: How to implement a REST API as ABAP request handler. You can inspect all the code I am discussing here on the MIGROS BSP website: It's all in Class ZCL_JOB_DATA .

The ICF Tree

Request handlers are classes implementing the interface IF_HTTP_EXTENSION, which consists of one single method HANDLE_REQUEST. A request handler class can be attached to a path in transaction SICF. An incoming HTTP request will be analyzed by the Internet Communication Framework, trying to match the request path against the SICF path. The match process is stopped as soon as a node with an attached request handler is found. If this is the case, an instance of the handler class will be created, and the method HANDLE_REQUEST will be called.

Our example service is attached to the path /job/attributes. The class ZCL_JOB_DATA is declared to be responsible for all incoming requests where the request path starts with /job/attributes :

First Strategy: HTTP Request Method

The implementation of the interface method if_http_extension~handle_request() forms the uppermost level of processing. Therefore, the implementation only gives the rough processing skeleton: An instance for database operations, as well as an instance for the processing of the REST operation are created, the request handling is delegated to that instance, and there is a catch block for error processing in case that no instance could be determined for processing the request. Such a situation should result in an HTTP response with status code '400 - Bad Request'.

At this place, we are using the Strategy design pattern: Depending on the HTTP method (GET, PUT, POST, DELETE, OPTIONS), a specific instance is created. Each possible instance corresponds to a specific strategy.


method if_http_extension~handle_request .

   data: lo_db             type ref to lif_db,
         lo_rest           type ref to lif_rest,
         lo_invalid_method type ref to zcx_error,
         lv_reason         type string.

   try.

* Object for database operations
       lo_db   ?= get_db( io_server = server ).

* Get the correct rest handler instance, depending on the verb (GET, PUT, POST, OPTIONS, DELETE)
       lo_rest ?= get_rest( io_server    = server
                                    io_db        = lo_db   ).

* Do the operation
       lo_rest->handle_request( ).

     catch zcx_not_found into lo_invalid_method.

        lv_reason = lo_invalid_method->get_text( ).
        server->response->set_status( code = 400   " Bad Request
                                      reason = lv_reason ).


   endtry.

endmethod.

We are using a naming convention for the instance determination: The class LCL_REST_GET will be associated with HTTP verb GET, LCL_REST_PUT with PUT, and so on. All these classes implement the interface LIF_REST. This way, we can use dynamic instance creation. Alternatively, we could have written a large CASE ... statement with many WHEN's. The advantage of the CASE would be that the create object statement could be statically checked for syntactical correctness. I have chosen the dynamical variant since I find it clearer and more readable than a bunch of WHEN branches.

Observe that the HTTP request method (GET, PUT, POST, ...) is available as pseudo header field with the name '~request_method':

method get_rest.
   data: lv_classname type seoclsname,
         lv_method    type string,
         lv_message   type text255.
   lv_method = io_server->request->get_header_field( '~request_method' ).
   concatenate 'LCL_REST_' lv_method into lv_classname.
   try.
       create object eo_rest
         type (lv_classname)
         exporting
           io_request   = io_server->request
           io_response  = io_server->response
           io_db        = io_db.
     catch cx_sy_create_object_error.
       lv_message = 'Method ''&'' not supported'(001).
       replace '&' in lv_message with lv_method.
       _raise_with_text zcx_not_found lv_message.
   endtry.
endmethod.

Second Strategy: Data Transfer Format

Now we have different handler classes for the different HTTP request methods. But for all these handlers, there are some common tasks. One of these common tasks is: to determine the current data transfer format, and to convert the input - if available - into ABAP data, and vice versa: to convert the ABAP result data into the output with the desired data transfer format (XML or JSON).

Now, some request methods like GET do not require any  request content. So the conversion of incoming data is performed by those method handlers that know they require content data. On the other hand, there will always be a result of the following data type:

types:
   begin of ty_result,
     msgtype type symsgty,
     message type c length 255,
     jobs type zjobs_tab,
   end of ty_result.

There may not always be entries in the job table. But not every component of this structure will be initial. If there is no job table, then usually there will be a message. So the conversion of the result can always be performed.

It makes sense to work with an abstract converter class, the specific subclasses containing the conversion algorithms per content-type. This is the second application of the Strategy pattern.


class lcl_converter definition abstract.
   public section.
     class-methods get_instance
       importing
         iv_accept type string
       returning value(eo_instance) type ref to lcl_converter.
     methods content_type abstract
       returning value(ev_content_type) type string.
     methods get_entered_data abstract
       importing
         iv_cdata type string
       exporting
         es_job type zjobs
       raising zcx_parse_error.
     methods result_to_cdata abstract
       importing
         is_result type ty_result
       exporting
         ev_cdata type string.
endclass.                    "lcl_converter DEFINITION

The static method LCL_CONVERTER=>GET_INSTANCE( ) makes the distinction, depending on the Accept header field of the HTTP request:

class lcl_converter implementation.
   method get_instance.
     if iv_accept cs 'application/json'.
       create object eo_instance type lcl_json_converter.
     else.
       create object eo_instance type lcl_xml_converter.
     endif.
   endmethod.                    "get_instance
endclass.                    "lcl_converter IMPLEMENTATION

The Common Plot for All Requests

We can extract common tasks into a superclass lcl_rest of all specific method handlers, implementing the interface lif_rest~handle_request( ) once for all subclasses.

The common code in the superclasse needs to be mixed with specific code, implemented in the subclass and defining the specific behaviour of that subclass. To achieve this, we call at the desired point of time in lif_rest~handle_request(), an abstract method do( ), which has to be redefined in the subclasses. This do() method will contain the specific action.

Now, the common implementation lif_rest~handle( ) in the superclass only defines the flow of the processing, leaving the concrete actions to the subclasses or to delegates like go_converter:

  1. Execute the specific action by calling do(),
  2. Error handling, with HTTP error code 400 "Bad Request" in case of conversion error (wrong incoming data), or setting response data for an error message in case of an application error,
  3. The result structure is mapped to the response data structure (XML or JSON), using the corresponding converter instance,
  4. Finally, the response data is placed into the body of the HTTP response, and also the appropriate response type is set: application/json, or text/xml.

This is the general sketch - the response processing that is valid for all HTTP request methods and for all content types (XML as well as JSON). The details are contained in the called methods.

method lif_rest~handle_request.

     data: lo_ex        type ref to cx_root,
           lv_cdata     type string,
           ls_result    type ty_result.
 
     try.

* Execute the specific operation
         do( importing es_result = ls_result ).

       catch zcx_parse_error into lo_ex.
         go_response->set_status( code     = 400  " Bad request
                                  reason   = lo_ex->get_text( ) ).
         set_response_parameters( ).
         return.
       catch zcx_error into lo_ex.
         ls_result-message = lo_ex->get_text( ).
         ls_result-msgtype    = 'E'.

     endtry.

* Convert result structure into JSON or XML, respectively
     call method go_converter->result_to_cdata
       exporting
         is_result = ls_result
       importing
         ev_cdata  = lv_cdata.

* Place the result in the response body
     call method set_response
       exporting
         iv_content_type = go_converter->content_type( )
         iv_cdata        = lv_cdata.


   endmethod.                    "handle_request

A Specific Task - the PUT Request

Let's look at a specific task for illustration: The PUT request - which always is a task to update or insert job attributes for a given ID on the database. As follows from the design, there is an own local class LCL_REST_PUT handling PUT requests. Actually, for this request handler, there was only the do method itself to implement (which is the absolute minimum for a specific task class to implement: do() is abstract in the parent class. Without an implementation, no instances could be built.):

class lcl_rest_put definition inheriting from lcl_rest.
   protected section.
     methods do redefinition.
endclass.                    "lcl_rest_put DEFINITION

The implementation goes as follows:

  • The job with the specified ID is read from the database (if an ID was specified - for new jobs, this is not the case),
  • The entered data will be parsed into an ls_job structure, using the appropriate go_converter instance,
  • And finally, the save() method is called. It is implemented in the superclass, since other request methods use it, too.


class lcl_rest_put implementation.
   method do.
     data: ls_job type zjobs,
           lv_id  type zjobs-id.
     try.
         get_job_by_id( importing es_job     = ls_job ).
         lv_id = ls_job-id.
       catch zcx_not_found.
     endtry.

     clear ls_job.
     call method go_converter->get_entered_data
       exporting
         iv_cdata = go_request->get_cdata( )
       importing
         es_job   = ls_job.

     if ls_job is not initial.
       if lv_id is not initial.
         ls_job-id = lv_id.
       endif.
       save( changing cs_job = ls_job ).
       es_result-message = 'Job & has been saved'(002).
       replace '&' in es_result-message with ls_job-id.
       es_result-msgtype    = 'S'.  " success message
       insert ls_job into table es_result-jobs.
     endif.


   endmethod.                    "do
endclass.                    "lcl_rest_put IMPLEMENTATION

Note that the implementation of this task doesn't care about the HTTP data structure, the format actually in use, nor about the details of the transfer data format. It simply works with ABAP data structures ls_job for the input and es_result for the output.

Session, Identity and Locking

In the test applications (neither in the JSON app nor in the XML app), there is neither login nor enqueue of the data. Since the applications are open for everybody, this works only since I don't really operate on a database table ZJOBS. Actually, each client who calls the application is working with his own session data, so he doesn't conflict with other users' operations, and is himself not disturbed by other users. The session data are preserved for him as server-side cookies, surviving the single dialogue step (for example reloading the page would reproduce the current state of the data).

When a web-client is written as BSP, there is a session-ID available in the attribute runtime->server_id. This session ID identifies the particular browser instance that made the request. On the client-side, this session-ID is always contained in a cookie called sap-appcontext. If an application has state which has to be preserved with a session ID, the ID has to be extracted from the sap-appcontext cookie and has to be passed as a query parameter with all the Ajax requests. Here is the function which extracts the sap-appcontext from the cookie:


function get_appcontext() {
  var lAppcontextCookie = document.cookie.match(/sap-appcontext=(.*?)(?:;|$)/);
  return lAppcontextCookie &&
         ( lAppcontextCookie.length >= 2) &&
         unescape( lAppcontextCookie[1] ) || "";
  }

The appcontext returned from this function, can be passed as query parameter with every Ajax request. On the server side, the session ID can be extracted from that parameter:

method get_session_id.

   data: lv_app_context   type string,
         lv_app_context64 type string.

* Read the form field, provided by the Ajax request
   lv_app_context64 = io_server->request->get_form_field( 'sap_appcontext' ).

   if lv_app_context64 is not initial.

* Base64 decode
     lv_app_context   = cl_http_utility=>decode_base64( lv_app_context64 ).

* Extract the Session-ID
     find regex 'sap-sessionid=([^;]+)(?:;|$)'
          in lv_app_context
          submatches ev_session_id.

   endif.

   if ev_session_id is initial.
     ev_session_id = io_server->session_id.
   endif.

endmethod.

As a fallback, in line 22, the server->session_id is used. However, there will be a new server->session_id for each request, which results in fresh session data with each dialogue step. If you really need session management, it is essential that the session id is passed to the server.

It is a good idea to combine the session id with the login procedure: If the user authenticates, his browser receives a session-id with a limited validity. That session-ID has to be passed with each successive REST operation. In ABAP, it can be used to store and retrieve session-specific data in the database table SSCOOKIE, via its database access class CL_BSP_SERVER_SIDE_COOKIE.

This coupling of a session id with login is - roughly - the way how the REST API for the HP Quality Center works.

Using ABAP's Built-In JSON Converter

While the XML converter instance is pretty straightforward to implement - calling an XSLT transformation for XML -> ABAP, and another one for the way back - it might come as a surprise that the JSON conversion can be handled exactly the same way: with transformations. This is possible sincehorst.keller/blog/2013/01/07/abap-and-json may send the following JSON data to the server:


{
  "ID": "0001",
  "REPID":"RSNAST00",
  "VARID": "UXPD_KUBE_KV",
  "PRIO": "2",
  "RESTART": "X",
  "DESCR": "Output all sales order confirmations",
  "CONTACT": "Rainer Zufall"
}

If a string with this content is passed as "SOURCE XML" to ABAP's CALL TRANSFORMATION statement, the JSON will be parsed into an XML representation like this one (the format is easy to understand - I think a detailled explanation is not necessary here):


<?xml version="1.0" encoding="utf-8"?>
<object>
<str name="ID">0001</str>
<str name="REPID">RSNAST00</str>
<str name="VARID">UXPD_KUBE_KV</str>
<str name="PRIO">2</str>
<str name="RESTART">X</str>
<str name="DESCR">Output all sales order confirmations</str>
<str name="CONTACT">Rainer Zufall</str>
</object>

When processing an arbitrary XSLT transformation, with the CALL TRANSFORMATION statement, and passing a JSON string as source, the XSLT will operate on this internal JSON-XML representation. It is easy to transform such a JSON-XML document into ABAP data - to be more precise: to transform it into an asXML representation of ABAP data. For example, consider the following XSLT transformation:


<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/">
    <asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
      <asx:values>
        <JOB>
          <xsl:apply-templates/>
        </JOB>
      </asx:values>
    </asx:abap>
  </xsl:template>
  <xsl:template match="str">
    <xsl:element name="{@name}">
      <xsl:value-of select="."/>
    </xsl:element>
  </xsl:template>
</xsl:transform>

When applied to the JSON string, it will produce the following result:


<?xml version="1.0" encoding="UTF-8"?>
<asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
  <asx:values>
    <JOB>
      <ID>0001</ID>
      <REPID>RSNAST00</REPID>
      <VARID>UXPD_KUBE_KV</VARID>
      <PRIO>2</PRIO>
      <RESTART>X</RESTART>
      <CONTACT>Rainer Zufall</CONTACT>
      <DESCR>Output all sales order confirmations</DESCR>
    </JOB>
  </asx:values>
</asx:abap>

This is a valid ABAP data description. If the transformation is named ZJSON2JOB, the data can simply be imported into an ABAP data structure with the components ID, REPID, and so on - as is the structure es_job in the following implementation of the JSON converter.

class lcl_json_converter implementation.
   method get_entered_data.

     data: lo_ex type ref to cx_transformation_error.

     clear es_job.
     check iv_cdata cn space.

     try.

         call transformation zjson2job
           source xml iv_cdata
           result job = es_job.

       catch cx_transformation_error into lo_ex.
         raise_parse_error( lo_ex ).

     endtry.

   endmethod.                    "get_entered_data

Many things can be done with the identity transformation ID, with no need to define an own XSLT transformation at all. If you can impose the JSON data structure to be used in the web application, it is of advantage to use such a "canonical" structure. For example, consider wrapping the JSON hash with the job attributes into another hash, making it the value for some symbolic key name like "JOB":


{
  "JOB": {
    "REPID": "RSNAST00",
    "VARID": "UXPD_KUBE_KV",
    "PRIO": "2",
    "RESTART": "X",
    "DESCR": "Output all sales order confirmations",
    "CONTACT": "Rainer Zufall",
    "ID": "0001"
  }
}

Then the data could be parsed into a structure without the need of developing a custom XSLT transformation, simple using the identity:


call transformation id
  source xml iv_cdata
  result job = es_job.

In this example, since I have written the web-client and the server-side processing, I could have chosen this more "canonical" format. But by not chosing it, I learned how to work with more flexible JSON data formats.

There are several reasons for working with "non-canonical" JSON representations of ABAP data:

  • A JSON format may be designed in favour of the web application - to optimize the readability of the client JavaScript code working on the data.
  • There may be client components requiring a particular JSON formats. For example, the jQuery datatable requires the table data to be passed as an array of arrays: http://www.datatables.net/release-datatables/examples/data_sources/ajax.html
  • JSON-based third party services may be called from the ABAP side (with a HTTP client object)
  • ABAP data may be projected to the essential data, reducing the message size to the data which are really needed.

Just to illustrate, let's have a look at the other conversion - the way out from the server to the client. Again, the format differs slightly from the "canonical" JSON format, which would simplify the ABAP-side handling considerably. As mentioned, the result data structure contains

  • a message,
  • a message type,
  • and a table of job attributes:

types:
   begin of ty_result,
     msgtype type symsgty,
     message type c length 255,
     jobs type zjobs_tab,
   end of ty_result.

The following format would be a perfect JSON pendant for this structure. It could be simply produced with the identity transformation, passing as "source result = ls_result" (where ls_result is a structure of type ty_result😞

  • All the component names match perfectly with the JSON hash key names,
  • An internal table is mapped as a JSON array of hashs, each hash representing one entry of the table,
  • And there is a top level hash with a symbolic name "RESULT" for the complete thing:


{
  "RESULT": {
    "MSGTYPE": "I",
    "MESSAGE": "Test",
    "JOBS": [
      {
        "ID": "0001",
        "REPID": "ZZTEST",
        "VARID": "VARI1",
        "PRIO": "1",
        "RESTART": "X",
        "CONTACT": "Harry Haller",
        "DESCR": "A hopeless job"
      },
      {
        "ID": "0002",
        "REPID": "ZZTEST2",
        "VARID": "VARI2",
        "PRIO": "3",
        "RESTART": "",
        "CONTACT": "Peter Pan",
        "DESCR": "A juvenile job"
      }
    ]
  }
}

But the JSON format that the REST API supports, actually differs in some details:

  • The jobs are designed not as an array, but as a hash, with the ID as hash key.
  • There is no redundant hash, wrapping the whole thing as the value for some key.
  • The component for MSGTYPE is different. It is simply called TYPE.

Here is an example instance:


{
  "JOBS": {
    "0001": {
      "REPID": "RSNAST00",
      "VARID": "UXPD_KUBE_KV",
      "PRIO": "2",
      "RESTART": "X",
      "CONTACT": "Rainer Zufall",
      "DESCR": "Output all sales order confirmations"
    },
    "0002": {
      "REPID": "RBDAPP01",
      "VARID": "UXPD_EDI_GUT02",
      "PRIO": "3",
      "RESTART": "X",
      "CONTACT": "Herbert Hurtig",
      "DESCR": "Credit Memos"
    }
  },
  "MESSAGE": "",
  "TYPE": ""
}

We proceed in a similar way as above, only in the other direction: based on the ABAP data type ty_result,  we write an XSLT transformation to obtain the internal JSON-XML format corresponding to this JSON data string.

The JSON-XML data format of the desired JSON data string looks like this:


<?xml version="1.0" encoding="utf-8"?>
<object>
<object name="JOBS">
  <object name="0001">
   <str name="REPID">RSNAST00</str>
   <str name="VARID">UXPD_KUBE_KV</str>
   <str name="PRIO">2</str>
   <str name="RESTART">X</str>
   <str name="CONTACT">Rainer Zufall</str>
   <str name="DESCR">Output all sales order confirmations</str>
  </object>
  <object name="0002">
   <str name="REPID">RBDAPP01</str>
   <str name="VARID">UXPD_EDI_GUT02</str>
   <str name="PRIO">3</str>
   <str name="RESTART">X</str>
   <str name="CONTACT">Herbert Hurtig</str>
   <str name="DESCR">Credit Memos</str>
  </object>
</object>
<str name="MESSAGE">Test</str>
<str name="TYPE">I</str>
</object>

So this is the target that has to be obtained as result of the transformation. On the other hand, the asXML format of the structure ty_result looks like this:


<?xml version="1.0" encoding="utf-8"?>
<asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
<asx:values>
  <DATA>
   <JOBS>
    <ZJOBS>
     <ID>0001</ID>
     <REPID>RSNAST00</REPID>
     <VARID>UXPD_KUBE_KV</VARID>
     <PRIO>2</PRIO>
     <RESTART>X</RESTART>
     <CONTACT>Rainer Zufall</CONTACT>
     <DESCR>Output all sales order confirmations</DESCR>
    </ZJOBS>
    <ZJOBS>
     <ID>0002</ID>
     <REPID>RBDAPP01</REPID>
     <VARID>UXPD_EDI_GUT02</VARID>
     <PRIO>3</PRIO>
     <RESTART>X</RESTART>
     <CONTACT>Herbert Hurtig</CONTACT>
     <DESCR>Credit Memos</DESCR>
    </ZJOBS>
   </JOBS>
   <MESSAGE>Test</MESSAGE>
   <MSGTYPE>I</MSGTYPE>
  </DATA>
</asx:values>
</asx:abap>

And this is the XSLT program that will perform the transformation:


<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

   <xsl:template match="DATA">
     <object>
       <xsl:apply-templates/>
     </object>
   </xsl:template>

   <xsl:template match="JOBS">
     <object name="JOBS">
       <xsl:apply-templates/>
     </object>
   </xsl:template>

   <xsl:template match="ZJOBS">
     <object name="{./ID}">
       <xsl:apply-templates select="*[name() != 'ID']"/>
     </object>
   </xsl:template>

   <xsl:template match="ZJOBS/* | MESSAGE">
     <str name="{name()}">
       <xsl:value-of select="."/>
     </str>
   </xsl:template>

   <xsl:template match="MSGTYPE">
     <str name="TYPE">
       <xsl:value-of select="."/>
     </str>
   </xsl:template>

</xsl:transform>

We see that, basically, for each deviation from the "canonical" JSON representation of ABAP data, there is a template in the XSLT transformation handling this deviation. For example, the different name TYPE instead of MSGTYPE in the target is handled with the template


   <xsl:template match="MSGTYPE">
     <str name="TYPE">
       <xsl:value-of select="."/>
     </str>
   </xsl:template>

The ID has to be rearranged: From being a simple attribute of the ZJOBS data structure, it has to be raised one level higher to become the key of a hash. All the other attributes, except ID, are copied as string nodes into the result. For this, these two templates are necessary:

   <xsl:template match="ZJOBS">
     <object name="{./ID}">
       <xsl:apply-templates select="*[name() != 'ID']"/>
     </object>
   </xsl:template>

   <xsl:template match="ZJOBS/* | MESSAGE">
     <str name="{name()}">
       <xsl:value-of select="."/>
     </str>
   </xsl:template>

Mapping the ty_result data object into a JSON string of the expected format, is now performed in ABAP with the following code:

method result_to_cdata.

     data: lo_writer type ref to cl_sxml_string_writer.

     lo_writer = cl_sxml_string_writer=>create( type = if_sxml=>co_xt_json ).
     call transformation zjobs2json
       source data = is_result
       result xml lo_writer.
     ev_cdata = cl_abap_codepage=>convert_from(  lo_writer->get_output( ) ).

   endmethod.                    "result_to_cdata

That's all: ev_cdata will then contain the JSON data string, to be placed in the HTTP response body.

Summary

I outlined some typical topics concerning the implementation of REST APIs in ABAP. It is possible to keep separate concerns in separate (local or global) classes by applying patterns like strategy. This is how the class ZCL_JOB_DATA, serving my demo REST API, is organized (the basic ideas have been discussed in this blog):

48 Comments