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: 
UweFetzer_se38
Active Contributor
0 Kudos

Abstract


In VCD #16 you have seen, amongst others, \ the REST bot. This short blog wants to explain how he/it works.


The blog is devided into the ABAP part and \ the Google wave part. You may read both or just one of these parts, according \ your preferences.


Motivation


In the last couple of weeks you surely have \ followed the new trending topics on twitter and SDN: REST and Googlewave.


My timeline in these exciting weeks

What else motivation an ABAP enthusiast needs to jump into a new adventure?


REST


As you can see in Daniels blog, SOAP isn't the easiest way of communication between Googlewave and SAP. Therefore my first question was: how did DJ do this? But I didn't ask him, I did ask myself.


Last time I've developed a HTTP handler in the SAP ICF was some years ago, so I had to experiment a little bit. But the first handler works quite fast, so my next question was: can be build an universal handler that is able to handle all kind of REST requests? The answer is: yes we can :wink:


The result you can find in the ABAP part of \ this blog.


REST extreme


Brian McKellar 06/27/2004: "I like your idea of addressable data. It will not take long until someone has the handler for /name_of_table/row_key!" (as a comment on DJ's above mentioned Blog)


He was quite right...


Googlewave


If you are not familiar with Googlewave yet, you may have a look on Daniels Blog "Starting on Google Wave"


About Robots

Wave robots are tiny programs written in Java or Python. They interact with the wave nearly as they were human participants. The code is stored as an Google App Engine application on the google server (appspot.com).


Advantages:

  • robots have the full access to the wave (all information, all participants, all change possibilities).
  • They can add information as plain text, form fields and images (but no file attachments yet. Maybe later, the Wave API is still under construction)

Disadvantages:

  • see "Advantage": They ONLY can add information as plain text, form fields and images.
  • Robots cannot interact with gadgets
About Gadgets

Gadgets are XML files which you can embedded into a blib of a wave.


Advantages:

  • within the XML file you can include (nearly) all HTML-Tags and Scripts (Javascript, Actionscript,...).
  • the XML file can be stored everywhere on the web

Disadvantage:

  • Gadgets don't have access to the wave and cannot interact with robots

The ABAP Part


The main goal for our new universal handler class is to handle REST HTTP requests regardless which object type we request and which content-type we expect as response.


HTTP request should look like http://host:port/rest/noun/key1/key2/../keyn/verb


  • Noun: Object type, i.e. “customer”
  • Key1 – Key n: the object key, i.e. the customer number
  • Verb: method, i.e. “getname”
The REST handler Class

The new handler class must implement the interface IF_HTTP_EXTENSION.


Method IF_HTTP_EXTENSION~HANDLE_REQUEST:

METHOD if_http_extension~handle_request.

   DATA: lo_request       TYPE REF TO if_http_request

       , lo_response      TYPE REF TO if_http_response

       , lv_pathinfo      TYPE string

       , lt_objects       TYPE string_table

       , lv_data          TYPE xstring

       , lv_cdata         TYPE string

       , lv_content_type  TYPE string

       , lv_rest_resolver TYPE string

       , lo_exc_ref       TYPE REF TO cx_sy_dyn_call_error

       , lv_exc_text      TYPE string

       .

   FIELD-SYMBOLS: <lv_object> TYPE string.

*--- get the REST path: ie. /customer/4711/getname ---*

   lo_request = server->request.

   lv_pathinfo = lo_request->if_http_entity~get_header_field( '~path_info' ).

   SHIFT lv_pathinfo LEFT. "delete leading '/'

   SPLIT lv_pathinfo AT '/'

     INTO TABLE lt_objects.

*--- get the object type (ie. customer) ---*

   READ TABLE lt_objects INDEX 1 ASSIGNING <lv_object>.

   IF sy-subrc <> 0.

     lo_response = server->response.

     lo_response->if_http_entity~set_cdata( 'No object type given' ).

     RETURN.

   ENDIF.

   TRANSLATE <lv_object> TO UPPER CASE.

*--- create the resolver class name (ie. /se380/cl_rest_customer) ---*

   CONCATENATE

     'ZCL_REST_'

     <lv_object>

   INTO lv_rest_resolver.

   DELETE lt_objects INDEX 1.   "delete the object type

   lv_content_type = 'text/plain'.  "preset content type

*--- call the resolver class ---*

   TRY .

       CALL METHOD (lv_rest_resolver)=>resolve

         EXPORTING

           request      = lt_objects       "(ie. 2 lines with object and method)

         IMPORTING

           data         = lv_data

           cdata        = lv_cdata

           content_type = lv_content_type.

     CATCH cx_sy_dyn_call_error INTO lo_exc_ref.

       lv_cdata = lo_exc_ref->get_text( ).

       CONCATENATE

         'Object/Method currently not supported. Error message:'

         lv_cdata

       INTO lv_cdata.

   ENDTRY.

*--- response ---*

   lo_response = server->response.

   IF lv_cdata IS NOT INITIAL.

     lo_response->if_http_entity~set_cdata( lv_cdata ).

   ENDIF.

   IF lv_data IS NOT INITIAL.

     lo_response->if_http_entity~set_data( lv_data ).

   ENDIF.

   IF lv_content_type IS NOT INITIAL.

     lo_response->if_http_entity~set_content_type( lv_content_type ).

   ENDIF.

ENDMETHOD.

In the ICF tree (transaction SICF) we only have to create a new entry “REST” under /sap/bc, enter the logon data and the new created handler class into the handler list:



(In a productive environment you better create an alias to this service)


The REST resolver classes

After implementing the handler class, you only have to create a class for each object type you want to serve, i.e. the Customer-Class with a method “resolve” and the following interface:



The import parameter “request” contains the object key(s) and the desired method (verb) as last entry.


Exporting parameters:


  • data: content as byte-stream (i.e. a PDF document or an image)
  • cdata: content as plain text
  • content_type: i.e. “text/plain” or “application/pdf”

The method “resolve” could looks like:

METHOD resolve.

  DATA: lv_kunnr TYPE kunnr.

  FIELD-SYMBOLS: <lv_object> TYPE string

               , <lv_method> TYPE string

               .

  content_type = 'text/plain'.

  READ TABLE request INDEX 1

    ASSIGNING <lv_object>.

  IF <lv_object> IS NOT ASSIGNED.

    cdata = 'No object given'.

    RETURN.

  ENDIF.

  UNPACK <lv_object> TO lv_kunnr.

  READ TABLE request INDEX 2

    ASSIGNING <lv_method>.

  IF <lv_method> IS NOT ASSIGNED.

    cdata = 'No method given'.

    RETURN.

  ENDIF.

  CASE <lv_method>.

    WHEN 'getname'.

      cdata = getname( lv_kunnr ).

    WHEN 'getaddress'.

      cdata = getaddress( lv_kunnr ).

    WHEN OTHERS.

      CONCATENATE

        'unknown method'

        <lv_method>

      INTO cdata SEPARATED BY space.

  ENDCASE.


ENDMETHOD.

If you want to serve many objects, you should create an Interface for this method (not part of this blog).


The “REST extreme” class

Just the code, no comments. There is really no use case for productive usage of this kind of stuff. Just a proof of concept and the right motivation (see above).

METHOD getvalue.

  TYPES: BEGIN OF lty_key

       ,   fieldname TYPE fieldname

       ,   value     TYPE REF TO data

       , END OF lty_key

       .

  DATA: lt_objects    TYPE string_table

      , lt_dd03l      TYPE TABLE OF dd03l

      , lv_tabname    TYPE string

      , lv_last_entry TYPE i

       , lv_fieldname  TYPE string

       , lv_rollname   TYPE rollname

       , lv_value      TYPE REF TO data

       , lt_keys       TYPE TABLE OF lty_key

       , lt_where      TYPE string_table

       , lv_search     TYPE string

       .

   FIELD-SYMBOLS: <lv_value>  TYPE ANY

                , <ls_dd03l>  TYPE dd03l

                , <ls_key>    TYPE lty_key

                , <lv_object> TYPE string

                , <lv_where>  TYPE string

                .

   lt_objects = request.

*--- get the tab name ---*

   READ TABLE lt_objects INDEX 1 INTO lv_tabname.

   DELETE lt_objects INDEX 1.

   TRANSLATE lv_tabname TO UPPER CASE.

*--- get the target field ---*

   lv_last_entry = LINES( lt_objects ).

   READ TABLE lt_objects INDEX lv_last_entry INTO lv_fieldname.

   DELETE lt_objects INDEX lv_last_entry.

   TRANSLATE lv_fieldname TO UPPER CASE.

*--- key field name/value pairs ---*

   SELECT * FROM dd03l

     INTO TABLE lt_dd03l

     WHERE tabname = lv_tabname

     AND   fieldname <> 'MANDT'

     AND   keyflag = 'X'

     ORDER BY position.

   IF sy-subrc <> 0.

     CONCATENATE

       'Tablename'

       lv_tabname

       'unknown'

     INTO cdata SEPARATED BY space.

     RETURN.

   ENDIF.

   LOOP AT lt_dd03l

     ASSIGNING <ls_dd03l>.

     INSERT INITIAL LINE INTO TABLE lt_keys ASSIGNING <ls_key>.

     <ls_key>-fieldname = <ls_dd03l>-fieldname.

     CREATE DATA <ls_key>-value TYPE (<ls_dd03l>-rollname).

     ASSIGN <ls_key>-value->* TO <lv_value>.

     READ TABLE lt_objects INDEX sy-tabix ASSIGNING <lv_object>.

     IF <lv_object> IS NOT ASSIGNED.

       CONCATENATE

         'Not all key field of table'

         lv_tabname

         'given'

       INTO cdata SEPARATED BY space.

       RETURN.

     ENDIF.

     IF <lv_object> CO ' 0123456789'.

       UNPACK <lv_object> TO <lv_value>.

     ELSE.

       <lv_value> = <lv_object>.

     ENDIF.

   ENDLOOP.

*--- create where tab ---*

   LOOP AT lt_keys

     ASSIGNING <ls_key>.

     IF sy-tabix > 1.

       INSERT INITIAL LINE INTO TABLE lt_where ASSIGNING <lv_where>.

       <lv_where> = 'AND'.

     ENDIF.

     "*--- search value ---*

     ASSIGN <ls_key>-value->* TO <lv_value>.

     TRANSLATE <lv_value> TO UPPER CASE.  "keys are always upper case

     CONCATENATE

       `'`

       <lv_value>

       `'`

     INTO lv_search.

     INSERT INITIAL LINE INTO TABLE lt_where ASSIGNING <lv_where>.

     CONCATENATE

       <ls_key>-fieldname

       '='

       lv_search

     INTO <lv_where> SEPARATED BY space.

   ENDLOOP.

*--- create target field data object ---*

   SELECT rollname

     INTO lv_rollname

     UP TO 1 ROWS

     FROM dd03l

     WHERE tabname = lv_tabname

     AND   fieldname = lv_fieldname.

   ENDSELECT.

   IF sy-subrc <> 0.

     CONCATENATE

       'Traget fieldname'

       lv_fieldname

       'unknown'

     INTO cdata SEPARATED BY space.

     RETURN.

   ENDIF.

   CREATE DATA lv_value TYPE (lv_rollname).

   ASSIGN lv_value->* TO <lv_value>.

*--- get the value ---*

   TRY .

       SELECT SINGLE (lv_fieldname)

         INTO <lv_value>

         FROM (lv_tabname)

         WHERE (lt_where).

     CATCH cx_sy_dynamic_osql_error.

       cdata = 'SQL Error'.

       RETURN.

   ENDTRY.

   cdata = <lv_value>.


ENDMETHOD.

The Wave Part


The Google wave part was (for me) the more difficult one: I didn’t want (and still don’t want) to learn Java therefore I had to learn Python. But this was a nice experience!


The Robot


Creating the robot to answer the simple questions (plain text response) was quite simple, it (or he?) contains just two sections:


  • Create an Hello message and a How to use guide
  • Concatenate the rest command to a URL and get the content

from waveapi import events

from waveapi import robot

from waveapi import document

from google.appengine.api import urlfetch

def OnRobotAdded(properties, context):

     """Invoked when the robot has been added."""

     root_wavelet = context.GetRootWavelet()

     root_wavelet.CreateBlip().GetDocument().SetText(

         "SAP Rest Demo V3.01

\n" + 

         "

\n

\nUsage of the bot:

\n" +

         "Keyword (always 'rest') Object-Type Object-Key(s) Method

\n" +

         "rest customer 481 getname

\n" +

         "rest salesorder 1033 1000 getlist

\n" +

         "rest spool 10801 getpdf

\n" +

         "and a couple of undocumented stuff ;)" 

     )

def OnBlipSubmitted(properties, context):

     """Invoked when a Blip is submitted"""

     blip = context.GetBlipById(properties['blipId'])

     text = blip.GetDocument().GetText().lower()

     if text.startswith("rest"):

         sub_blip = blip.CreateChild()

         url = "</span><a class="jive-link-external-small" href="http://hostname:port/sap/bc/" _mce_href="http://hostnameport">http://hostname:port/sap/bc/</a><span>" 

         url += text.replace(" ", "/")

         response = urlfetch.fetch(url=url)

         content = response.content

         contenttype = response.headers['content-type']

         if contenttype.startswith('text/plain'):

             sub_blip.GetDocument().SetText('

\n

\n' + content)

if __name__ == '__main__':

     myRobot = robot.Robot('se38testrobot', 

       image_url='</span><a class="jive-link-external-small" href="http://se38testrobot.appspot.com/assets/service.png" _mce_href="http://se38testrobot.appspot.com/assets/service.png">http://se38testrobot.appspot.com/assets/service.png</a><span>',

       version='3.01',

       profile_url='</span><a class="jive-link-external-small" href="http://se38testrobot.appspot.com/" _mce_href="http://se38testrobot.appspot.com/">http://se38testrobot.appspot.com/</a><span>')

     myRobot.RegisterHandler(events.BLIP_SUBMITTED, OnBlipSubmitted)

     myRobot.RegisterHandler(events.WAVELET_SELF_ADDED, OnRobotAdded)

     myRobot.Run()

The Gadget


But one issue was still open: I have created a resolving class which delivers a PDF response (rest/spool/xyz/getpdf). As I mentioned above, the robot only can handle plain-text responses. “Make a gadget”, a twitter follower advised me. Nice try, but how can I pass parameters to a gadget. I mentioned also, that the robot has no access to the gadget and the gadget cannot read the requesting blip for the parameters.


Sleepless nights.


One day, I was at my current customer, I’ve got the answer: do we really need a static XML file for the gadget? Quit work, took my motorbike and flew to my home office and tried my idea: it works :smile:


The solution:


I simply wrote a BSP (gadget.xml) which has one input parameter:

<%@page language="abap" %>

<?xml version="1.0" encoding="UTF-8" ?>

<Module>

   <ModulePrefs title="PDF Gadget" height="600">

     <Require feature="wave" />

   </ModulePrefs>

   <Content type="html">

     <![CDATA[

     <html>

       <body>

       Hello Wave, greetings from the PDF gadget!<br />

<%

     DATA: lv_url TYPE string.

     REPLACE ' ' WITH '/' INTO rest_objects.

     CONCATENATE

       '</span><a class="jive-link-external-small" href="http://hostname:port/sap/bc/" _mce_href="http://hostnameport">http://hostname:port/sap/bc/</a><span>'

       rest_objects

     INTO lv_url.

%>

       <iframe src="<%= lv_url %>" width="100%" height="600">

       </body>

     </html>

     ]]>

   </Content>

</Module>

I change my robot a bit the gadget was correctly called by the robot as you can see in the replay of VCD #16.


Added part in the robot:

        if text.endswith("getpdf") :

             """ call the PDF gadget """

             url =  "</span><a class="jive-link-external-small" href="http://hostname:port" _mce_href="http://hostnameport">http://hostname:port</a><span>"

             url += "/sap/bc/bsp/sap/zpdfgadget/gadget.xml?rest_objects="

             url += text.replace(" ", "+")

             gadget = document.Gadget(url=url)

             sub_blip.GetDocument().AppendElement(gadget)

Conclusion


These where funny weeks. Have met some new and interesting people, had some great discussions, learned a lot. What’s the next topic? 😉


More to read...


G+

8 Comments