SAP AppGyver, SAP Business Technology Platform, SAP S/4HANA

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.

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 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.

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 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.
Options request
  • ‘PUT’ to carry out the update. In this case response code must be a ‘200’ with the body in JSON format.
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.

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

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.