REST-JSON integration between AppGyver and SAP ERP or S/4 HANA


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.
SAP is actually releasing a BTP integration with AppGyver (https://blogs.sap.com/2022/07/01/using-btp-authentication-and-destinations-with-sap-appgyver/) which looks very interesting.

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

General%20Architecture

General Architecture

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. Refer to this blog in order to see how.

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.

Get%20Request%20exchange

Get Request exchange

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.

Option%20request

Options request

-‘PUT’ to carry out the update. In this case response code must be a ‘200’ with the body in JSON format.

PUT%20Request

PUT Request

.

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.

Follow this blog to configure the cloud connector

And follow this video in order to define the BTP approuter component

The xs-app.json file that you have to configure should look like this.

xs-app.json

xs-app.json

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.

Until the release of the AppGyver in which the BTP integration will be available on mobile, this kind of integration can be used to make communicate AppGyver with an SAP ERP or S/4 HANA on mobile app supporting the update operation.

This solution does not need SAP Gateway, therefore it is interesting as well for landscapes that does not have this component yet.

This integration can be considered as well as a useful exercise to understand the CORS protocol and how AppGyver requests works.

If you are interested on more blogs con AppGyver you can follow this link

I hope you found this blog useful. If you have any questions or, let me know in the comment.