Skip to Content
Technical Articles
Author's profile photo Sebastian Gesiarz

How to post SAC API output into BW/4HANA using ABAP

1. Introduction

This simple tutorial should give you an idea of how to consume SAC APIs in ABAP and post the result to BW ADSO.

SAP exposes a few data-export APIs. API documentation can be found in the following links;

2. Use-case

  • Store changelog for SAC metadata
  • Enhance existing BW models with SAC metadata
  • Consume SAC APIs through ABAP using OAuth 2.0

3. Implementation

3.1 SAC

Create an OAuth 2.0 client in SAC as described in https://help.sap.com/docs/…Manage OAuth Clients. Make sure to tick the Interactive Usage and API Access.

This client will provide an authorization token for BW based on the Agent secret.

3.2 ABAP / OAuth

Follow the tutorial in the following blog to configure the OAuth2.0 Client Profile: https://blogs.sap.com/2020/12/18/configuring-oauth-2.0-and-creating-an-abap-program-that-uses-oauth-2.0-client-api/. You will find all the details that you need to provide in the SAC Client you created in the previous part of this Blog.

The Postman part is optional but recommended to test your API before consuming it from ABAP.

Remarks:

  • Leave the Scope empty.
  • Do not add https/ where it’s already provided.
  • If SAML 2.0 Audience is greyed out, change Grant Type from Client Credentials to Current user related. Fill in the OA2C_CONFIG and change it back to Client Credentials.
  • Fill in the proxy Host and Port. If you do not know, ask your BASIS administrator.

Official documentation: https://help.sap.com/docs/SAP_NETWEAVER_750/…Configuring OAuth 2.0 for AS ABAP.

Make sure you have the following authorizations assigned: Configuring the Role of the Resource Owner for OAuth 2.0. Troubleshoot in SU53 if necessary.

3.3 SAML

Enter the STRUST transaction and upload the certificates of your SAC tenant. Note that ABAP HTTP client defaults SSL_ID to ANONYM. It cannot be changed. It is a hardcoded value in the CREATE_HTTP_CLIENT method.

You need to upload the certificates into the folder: SSL client SSL Client (Anonymous). If you do not know how to download missing certificates;

  1. Go to the SAC page
  2. Click the locker icon at the beginning of the search bar
  3. Click Connection is secure
  4. Click Certificate is valid
  5. Click Details
  6. Click copy to file.
  7. Do the same for the API URL page.
  8. Upload.

If some certificate will be still missing, you will find it out while debugging the first ABAP part.

3.4 ABAP

Create a new class. Add a method to set the SAC URL (me->lv_url) based on the BW system.
Add a method to set the OAuth. Change the ##INPUT to your settings. Add error handling class, the code below does not include it. (…) are the placeholders for the comments.

    " (...)
    cl_http_client=>create_by_url(
        EXPORTING
            url = me->lv_url
            ssl_id = 'DFAULT'
            proxy_host = '##INPUT'
            proxy_service = '##INPUT'
        IMPORTING
            client = lo_http_client  ).


    lo_http_client->propertytype_logon_popup = 0.
    lo_http_client->request->set_method( EXPORTING method = 'GET' ).


    " (...)
    lo_http_client->request->set_header_field( name = 'x-sap-sac-custom-auth' value = 'true' ).
    lo_http_client->request->set_header_field( name = 'x-csrf-token' value = 'fetch' ).


    " (...)
    TRY.
        cl_oauth2_client=>create(
          EXPORTING
            i_profile        = '##INPUT'
            i_configuration  = '##INPUT'
          RECEIVING
            ro_oauth2_client = DATA(lo_a2c_client)
        ).
      CATCH cx_oa2c INTO DATA(lx_oa2c).
        WRITE: 'Error calling create.'.
        WRITE: / lx_oa2c->get_text( ).
        RETURN.
    ENDTRY.


    TRY.


        lo_a2c_client->set_token(
          EXPORTING
            io_http_client = lo_http_client
            i_param_kind   = lc_param_kind
        ).


      CATCH cx_oa2c INTO lx_oa2c.


        " (...)
        TRY.
            lo_a2c_client->execute_cc_flow( ).
          CATCH cx_oa2c INTO lx_oa2c.
            WRITE: 'Error calling create.'.
            WRITE: / lx_oa2c->get_text( ).
            RETURN.
        ENDTRY.


        " (...)
        TRY.
            lo_a2c_client->set_token(
              EXPORTING
                io_http_client = lo_http_client
                i_param_kind   = lc_param_kind
            ).
          CATCH cx_oa2c INTO lx_oa2c.
            WRITE: 'Error calling create.'.
            WRITE: / lx_oa2c->get_text( ).
            RETURN.
        ENDTRY.


    ENDTRY.

Create a method to get the data.

    " (...)
    lo_http_client->send(
      EXPORTING
        timeout                    = 9999
      EXCEPTIONS
        http_communication_failure = 1
        http_invalid_state         = 2
        http_processing_failed     = 3
        http_invalid_timeout       = 4
        OTHERS                     = 5
    ).
    IF sy-subrc <> 0.
      MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno
        WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
    ENDIF.


    lo_http_client->receive(
      EXCEPTIONS
        http_communication_failure = 1
        http_invalid_state         = 2
        http_processing_failed     = 3
        OTHERS                     = 4
    ).
    IF sy-subrc <> 0.
      MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno
        WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
    ENDIF.


    " (...)
    lo_http_client->response->get_status(
          IMPORTING
            code   = lv_status_code
            reason = lv_reason
    ).


    " (...)
    IF lv_status_code = 200.
      CALL METHOD lo_http_client->response->get_cdata
        RECEIVING
          data = lv_response_data.
    ENDIF.


    " (...)
    lo_http_client->close(
      EXCEPTIONS
        http_invalid_state = 1
        OTHERS             = 2
    ).
    IF sy-subrc <> 0.
      MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno
        WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
    ENDIF.

Create a method to parse the result. This is an old-school way. Follow up https://nocin.eu/abap-json-to-abap-with-dereferencing/ if you would like to do it in a more modern way.

   FIELD-SYMBOLS:
      <fs_table>      TYPE ANY TABLE,
      <fs_models_tab> TYPE ANY TABLE,
      <fs_data>       TYPE data,
      <field_value>   TYPE data.


    " (...)
    lv_response_data = |\{"d":{ lv_response_data }\}|.


    CALL METHOD /ui2/cl_json=>deserialize
      EXPORTING
        json         = lv_response_data
        pretty_name  = /ui2/cl_json=>pretty_mode-user
        assoc_arrays = abap_true
      CHANGING
        data         = lr_data.


    " (...)
    IF lr_data IS BOUND.
      ASSIGN lr_data->* TO <fs_data>.


      " ---------------------
      " (...)
      " ---------------------
      ASSIGN COMPONENT 'd' OF STRUCTURE <fs_data> TO FIELD-SYMBOL(<fs_results>).
      ASSIGN <fs_results>->* TO <fs_table>.


      LOOP AT <fs_table> ASSIGNING FIELD-SYMBOL(<fs_table_row>).
        ASSIGN <fs_table_row>->* TO FIELD-SYMBOL(<data>).


        ASSIGN COMPONENT 'NAME' OF STRUCTURE <data> TO FIELD-SYMBOL(<field>).
        IF <field> IS ASSIGNED.
          lr_data = <field>.
          ASSIGN lr_data->* TO <field_value>.
          ls_parsed_result_story-name = <field_value>.
        ENDIF.
        UNASSIGN: <field>, <field_value>.


        ASSIGN COMPONENT 'DESCRIPTION' OF STRUCTURE <data> TO <field>.
        IF <field> IS ASSIGNED.
          lr_data = <field>.
          ASSIGN lr_data->* TO <field_value>...

   
        " --------------
        " (...)
        " --------------
        ASSIGN COMPONENT 'MODELS' OF STRUCTURE <data> TO FIELD-SYMBOL(<fs_models>).
        ASSIGN <fs_models>->* TO <fs_models_tab>.


        LOOP AT <fs_models_tab> ASSIGNING FIELD-SYMBOL(<fs_models_row>).
          ASSIGN <fs_models_row>->* TO FIELD-SYMBOL(<data_models>).


          " (...)
          ls_parsed_result_stor_x_models = CORRESPONDING #( ls_parsed_result_story ).


          " (...)
          ASSIGN COMPONENT 'ID' OF STRUCTURE <data_models> TO <field>.
          IF <field> IS ASSIGNED.
            lr_data = <field>.
            ASSIGN lr_data->* TO <field_value>.
            ls_parsed_result_stor_x_models-model_id = <field_value>.
          ENDIF.
          UNASSIGN: <field>, <field_value>.


          ASSIGN COMPONENT 'DESCRIPTION' OF STRUCTURE <data_models> TO <field>.
          IF <field> IS ASSIGNED.
            lr_data = <field>...
 
          UNASSIGN: <field>, <field_value>.


          " --------------
          " (...)
          " --------------
          ASSIGN COMPONENT 'REMOTECONNECTION' OF STRUCTURE <data_models> TO FIELD-SYMBOL(<fs_models_conn>).

          " Some models don't have remote connection information
          IF <fs_models_conn> IS ASSIGNED.


            ASSIGN <fs_models_conn>->* TO FIELD-SYMBOL(<fs_models_conn_struc>).


            ASSIGN COMPONENT 'HOST' OF STRUCTURE <fs_models_conn_struc> TO <field>.
            IF <field> IS ASSIGNED.
              lr_data = <field>.
              ASSIGN lr_data->* TO <field_value>.
              ls_parsed_result_stor_x_models-model_remoteconnection_host = <field_value>.
            ENDIF.
            UNASSIGN: <field>, <field_value>.


            ASSIGN COMPONENT 'NAME' OF STRUCTURE <fs_models_conn_struc> TO <field>.
            IF <field> IS ASSIGNED.
              lr_data = <field>.
              ASSIGN lr_data->* TO <field_value>.
              ls_parsed_result_stor_x_models-model_remoteconnection_name = <field_value>.
            ENDIF.
            UNASSIGN: <field>, <field_value>.


          ENDIF.


          " (...)
          APPEND ls_parsed_result_stor_x_models TO lt_parsed_result.
          CLEAR ls_parsed_result_stor_x_models.


          " (...)
          AT LAST.
            CLEAR ls_parsed_result_story.
          ENDAT.


        ENDLOOP.

      ENDLOOP.


    ENDIF.

Depending on your need, add a method adding a result timestamp to the output.

    MODIFY lt_parsed_result FROM VALUE #(
      request_date = sy-datum
      request_time = sy-uzeit
    )
    TRANSPORTING
      request_date
      request_time
    WHERE
      id IS NOT INITIAL.

Post the data back to BW ADSO. It has to have the exact same structure as your internal table.

   CALL FUNCTION 'RSDSO_WRITE_API'
      EXPORTING
        i_adsonm              = lc_adso_sac_metadata
        i_allow_new_sids      = rs_c_true
        i_activate_data       = rs_c_false
        it_data               = lt_parsed_result
      IMPORTING
        e_lines_inserted      = e_lines_inserted
        e_cold_lines_inserted = e_cold_lines_inserted
        et_msg                = et_msg
        e_upd_req_tsn         = e_upd_req_tsn
        et_act_req_tsn        = et_act_req_tsn
      EXCEPTIONS
        write_failed          = 1
        activation_failed     = 2
        datastore_not_found   = 3
        OTHERS                = 4.
    rc = sy-subrc.
    IF sy-subrc <> 0.
      MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
    ENDIF.

3.5 BW

Depending on the use case, I would recommend posting the data to the staging ADSO with an inbound table only. It is PSA-like, so you can extract the data from it by using a full load and adding the latest timestamp into the DTP filter routine. You can also delete old requests. Then process it to the staging ADSO with a snapshot function. This ADSO can store e.g. monthly snapshots. Then, propagate to the time-dependent InfoObjects. Use Enhanced Master Data Flag in order to automatically invalidate deleted records based on the record mode value. Valid from can be the posting date of the request which was added in ABAP. Apply custom logic where needed. Don’t forget to add the housekeeping tasks.

You can wrap your class into a SE38 program. Then schedule as the first step of the process chain.

Assigned Tags

      Be the first to leave a comment
      You must be Logged on to comment or reply to a post.