Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
wounky
Participant


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


" (...)
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. Optionally, you can add one more ADSO to store e.g. monthly snapshots on top. 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.
Labels in this area