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 Configuring OAuth 2.0 and Creating an ABAP Program That Uses OAuth 2.0 Client API | SAP Blogs. 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.
Kod:
" 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.
Kod:
" 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 [ABAP] JSON to ABAP with dereferencing – nocin.eu if you would like to do it in a more modern way.
Kod:
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.
Kod:
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.
Kod:
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.
Okumaya devam et...