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
- 08/21 Daniel Graversen published his Blog "SAP Enterprise Service and Google Wave"
- 08/25 Fortuna or the Google God (or who else) had a good day and invited me to join the developers preview of Googlewave
- 08/27 eGeeks Podcast "SOAP vs. REST"
- 09/03 DJ Adams found me on Googlewave: "I've been hacking around this morning with a connection to my SAP system" -> Demo of a wave robot based on an old rest dashboard application he wrote in 2004 (see his blog "Forget SOAP - \ build real web services with the ICF")
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
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
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...
- Gravity – Collaborative Business Process Modelling within Google Wave
- SAP Enterprise Services and Google wave
- SAP and Google Wave - Conversation Augmentation
- SAP and Google Wave - Embedding waves in SAP NetWeaver Portal