Introduction
I am actually involved in a project in which I have to integrate AppGyver with a SAP ERP. The integration must work in mobile apps and includes update operations.
Unfortunately at the date in which I am writing this blog, the solution is not working yet on mobile apps, therefore I’ve decided to take an alternative path and integrate AppGyver and SAP through REST JSON rather than OData.
The solution involves very few components works pretty well, is simple, easy to program (for the ABAP old guys), gives you full control, resolves all the CORS issues on GET and…. very important on PUT and POST operations.
Works on web and mobile App without any problem.
Solution Overview
The development environment that I’ve used is the following
SAP backend exposes a JSON interface via the Internet Communication Framework (ICF)=.
Approuter BTP component publish the ABAP JSON service to AppGyver.
As alternative to approuter for development purpose you can use also NGROK (https://ngrok.com/)
The use case that I’ve implemented consists in listing of assets of the ERP and eventually modify one of their fields (e.g inventory number). You can adapt easily to any other use case.
Defining the JSON Service
I’ve create a simple Rest service in ICF.
Handling class that implements service is called ZASSET_HTTP
I’ve defined three additional methods apart the standard HANDLE_REQUEST coming from interface IF_HTTP_REQUEST
HANDLE_REQUEST method code is the following:
METHOD IF_HTTP_EXTENSION~HANDLE_REQUEST.
DATA: it_header_fields TYPE tihttpnvp.
DATA: it_form_fields TYPE tihttpnvp.
CALL METHOD server->request->if_http_entity~get_header_fields
CHANGING
fields = it_header_fields.
CALL METHOD server->request->if_http_entity~get_form_fields
CHANGING
fields = it_form_fields.
DATA: wa_method LIKE LINE OF it_header_fields.
DATA: wa_action LIKE LINE OF it_header_fields.
CREATE OBJECT ASSET_BACKEND.
READ TABLE it_header_fields INTO wa_method WITH KEY name = '~request_method'.
READ TABLE it_form_fields INTO wa_action WITH KEY name = 'action'.
IF ( wa_method-value = 'GET' and wa_action-value = 'getlist' ).
CALL METHOD getlist
EXPORTING
server = server
.
elseIF ( wa_method-value = 'GET' and wa_action-value = 'getdetail' ).
CALL METHOD getdetail
EXPORTING
server = server
.
ELSEIF ( wa_method-value = 'PUT' ) or ( wa_method-value = 'OPTIONS' ).
CALL METHOD update
EXPORTING
server = server.
ENDIF.
ENDMETHOD.
GETLIST method code:
method GETLIST.
DATA: it_header_fields TYPE tihttpnvp.
DATA: it_form_fields TYPE tihttpnvp.
FIELD-SYMBOLS: <form> TYPE ihttpnvp.
DATA: wa_origin TYPE ihttpnvp.
DATA: lv_result TYPE string.
DATA: cdata TYPE string.
DATA: lv_bukrs TYPE bukrs.
CALL METHOD server->request->if_http_entity~get_form_fields
CHANGING
fields = it_form_fields.
CALL METHOD server->request->if_http_entity~get_header_fields
CHANGING
fields = it_header_fields.
READ TABLE it_header_fields INTO wa_origin WITH KEY name = 'origin'.
READ TABLE it_form_fields ASSIGNING <form> WITH KEY name = 'bukrs'.
IF ( sy-subrc = 0 ).
lv_bukrs = <form>-value.
endif.
CALL METHOD ASSET_BACKEND->GETLIST
EXPORTING
i_bukrs = lv_bukrs
IMPORTING
result = lv_result.
server->response->set_header_field(
name = 'Content-Type' "#EC NOTEXT
value = 'application/json' ).
if ( wa_origin-value = 'https://appgyver-ompmyzem-platform.appgyver.black' or
wa_origin-value = 'https://appgyver-ompmyzem.preview-btp.appgyver.black' or
wa_origin-value = 'https://appgyver-ompmyzem.preview.appgyver.black' or
wa_origin-value = 'https://appgyver-ompmyzem.appgyver.black' ).
******** enable CORS Access
server->response->set_header_field(
name = 'Access-Control-Allow-Origin'
value = wa_origin-value ).
endif.
server->response->set_cdata( data = lv_result ).
server->response->set_status( code = 200 reason = 'OK' ).
endmethod.
If you want to transform an ABAP list/structure to JSON use the class /ui2/cl_json. If you don’t have this class on your system look in the SAP Community. There are plenty of alternative solutions.
Notice the code at the end to handle CORS requests.
If the origin of the request comes from Appgyver platform I set the ‘Access-Control-Allow-Origin’ header value to the value of the origin header.
In order to understand CORS I’ve used this article (https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
I won’t show the GETDETAIL code that is quite similar conceptually to the GETLIST
The UPDATE code is the following:
METHOD update.
DATA: it_header_fields TYPE tihttpnvp.
DATA: wa_method TYPE ihttpnvp.
DATA: body TYPE string.
DATA: wa_asset TYPE zinm_activo_ext.
DATA: it_form_fields TYPE tihttpnvp.
FIELD-SYMBOLS: <form> TYPE ihttpnvp.
FIELD-SYMBOLS: <header> TYPE ihttpnvp.
FIELD-SYMBOLS <origin> TYPE ihttpnvp.
DATA: lv_response TYPE bapiret2.
DATA: ret_json TYPE string.
DATA: cdata TYPE string.
DATA: wa_invnr TYPE zinm_activo_ext.
DATA: wa_anln1 TYPE anln1.
DATA: wa_anln2 TYPE anln2.
DATA: wa_bukrs TYPE bukrs.
CALL METHOD server->request->if_http_entity~get_header_fields
CHANGING
fields = it_header_fields.
CALL METHOD server->request->if_http_entity~get_form_fields
CHANGING
fields = it_form_fields.
READ TABLE it_header_fields INTO wa_method WITH KEY name = '~request_method'.
IF wa_method-value = 'OPTIONS'.
READ TABLE it_header_fields ASSIGNING <origin> WITH KEY name = 'origin'.
IF sy-subrc = 0.
IF ( <origin>-value = 'https://appgyver-ompmyzem-platform.appgyver.black' OR
<origin>-value = 'https://appgyver-ompmyzem.preview-btp.appgyver.black' OR
<origin>-value = 'https://appgyver-ompmyzem.preview.appgyver.black' OR
<origin>-value = 'https://appgyver-ompmyzem.appgyver.black' ).
******** enable CORS Access
server->response->set_header_field(
name = 'Access-Control-Allow-Origin'
value = <origin>-value ).
ENDIF.
server->response->set_header_field(
name = 'Access-control-allow-methods'
value = 'PUT, POST, GET, OPTIONS' ).
server->response->set_header_field(
name = 'Access-Control-Allow-Headers'
value = 'X-PINGOTHER, Content-type' ).
server->response->set_header_field(
name = 'Access-Control-Max-Age'
value = '86400' ).
ret_json = /ui2/cl_json=>serialize( data = lv_response compress = abap_true pretty_name = /ui2/cl_json=>pretty_mode-camel_case ).
server->response->set_cdata( data = ret_json ).
server->response->set_status( code = 204 reason = 'No Content' ).
RETURN.
ENDIF.
ELSEIF wa_method-value = 'PUT'.
CALL METHOD server->request->if_http_entity~get_cdata
RECEIVING
data = body.
CALL METHOD /ui2/cl_json=>deserialize
EXPORTING
json = body
CHANGING
data = wa_asset.
READ TABLE it_form_fields ASSIGNING <form> WITH KEY name = 'anln1'.
IF ( sy-subrc = 0 ).
wa_anln1 = <form>-value.
ENDIF.
READ TABLE it_form_fields ASSIGNING <form> WITH KEY name = 'anln2'.
IF ( sy-subrc = 0 ).
wa_anln2 = <form>-value.
ENDIF.
READ TABLE it_form_fields ASSIGNING <form> WITH KEY name = 'bukrs'.
IF ( sy-subrc = 0 ).
wa_bukrs = <form>-value.
ENDIF.
READ TABLE it_header_fields ASSIGNING <origin> WITH KEY name = 'origin'.
IF sy-subrc = 0.
IF ( <origin>-value = 'https://appgyver-ompmyzem-platform.appgyver.black' OR
<origin>-value = 'https://appgyver-ompmyzem.preview-btp.appgyver.black' OR
<origin>-value = 'https://appgyver-ompmyzem.preview.appgyver.black' OR
<origin>-value = 'https://appgyver-ompmyzem.appgyver.black' ).
******** enable CORS Access
server->response->set_header_field(
name = 'Access-Control-Allow-Origin'
value = <origin>-value ).
ENDIF.
ENDIF.
CALL METHOD asset_backend->update
EXPORTING
i_bukrs = wa_bukrs
i_anln1 = wa_anln1
i_anln2 = wa_anln2
i_invnr = wa_asset-invnr
i_anexo = wa_asset-anexo
i_extension = wa_asset-extension
IMPORTING
e_result = lv_response.
IF ( <origin> IS ASSIGNED ).
server->response->set_header_field(
name = 'access-control-allow-origin'
value = <origin>-value ).
ENDIF.
server->response->set_header_field(
name = 'content-type'
value = 'application/json' ).
ret_json = /ui2/cl_json=>serialize( data = lv_response compress = abap_true pretty_name = /ui2/cl_json=>pretty_mode-camel_case ).
server->response->set_cdata( data = ret_json ).
server->response->set_status( code = 200 reason = 'OK' ).
ENDIF.
ENDMETHOD.
In this case AppGyver sends two kind of requests
- ‘OPTIONS’ to know if can perform a PUT operation: In this case, the response code must be ‘204’ without any body.
- ‘PUT’ to carry out the update. In this case response code must be a ‘200’ with the body in JSON format.
Code can be modularized much better but to make it simpler I’ve left everything in one method.
Defining the approuter
Through the approuter it willl be possible to publish our service to BTP to make it available to AppGyver.
The xs-app.json file that you have to configure should look like this.
Where you have to put your BTP destination.
authenticationMethod is set to “none” just to simpifly the scenario.
In production scenario you should configure an appropiate authentication method
Testing the approuter
Once you deployed the MTA you should get from the BAS terminal the URL published
Test the URL published adding the arguments for the service call. You should get the JSON answer.
Testing from AppGyver.
From the AppGyver click the data Icon and choose from AppGyver classic Data Entities ⇒ Create Data Entity ⇒ Rest API Direct Integration
Define the service using the public URL given by approuter.
In the getCollection I’ve added the action parameter set to the static value “getlist” and the BUKRS parameter
If I test I’ve got the result.
And finally update case
If I test it….
works pretty well.