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

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;

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

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

3.3 SAML

Enter the STRUST transaction and upload the certificates of your SAC tenant. Note later on SSL_ID defaults to ANONYM and 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, go to the SAC page, click the locker icon at the beginning of the search bar, click Connection is secure, Certificate is valid, Details, copy to file. Do the same for the API page. Upload.

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.

 " Create HTTP client and set up the proxy, use DFAULT SSL certificates (added in STRUST) 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' ). " Add headers required by SAC API for oAuth connection 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' ). " Set oAuth 2.0 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. " Execute Client Credentials Flow before Token TRY. lo_a2c_client->execute_cc_flow( ). CATCH cx_oa2c INTO lx_oa2c. WRITE: 'Error calling create.'. WRITE: / lx_oa2c->get_text( ). RETURN. ENDTRY. " Initiate the token 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.

 " Send / Receive request 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. "Display result lo_http_client->response->get_status( IMPORTING code = lv_status_code reason = lv_reason ). " Request response received OK IF lv_status_code = 200. CALL METHOD lo_http_client->response->get_cdata RECEIVING data = lv_response_data. ENDIF. " Close 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. " Add header so that the JSON can be parsed via dynamic ABAP 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. " Process the parsed result into ABAP table IF lr_data IS BOUND. ASSIGN lr_data->* TO <fs_data>. " --------------------- " Lookup story metadata " --------------------- 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>... " -------------- " Lookup models " -------------- 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>). " Add story metadata ls_parsed_result_stor_x_models = CORRESPONDING #( ls_parsed_result_story ). " Assign elements of the model table 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>. " -------------- " Remote connection information is a nested structure " -------------- 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. " Add story metadata with current model to the result set and clear for the next model APPEND ls_parsed_result_stor_x_models TO lt_parsed_result. CLEAR ls_parsed_result_stor_x_models. " Clean main structure for the next story after the last model of the current story 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 use a full load based on the timestamp and delete old requests. Then process it to the staging ADSO with a snapshot function, and to the time-dependent InfoObjects. Apply custom logic where needed.

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