BTP - Create Entity für OData v4 mit Draft
In diesem Artikel geht es um den Flow zum Anlegen und Aktivieren einer Instanz in der BTP aus On-Premise, dabei ist der Endpunkt eine OData Version 4 der Draft unterstützt.
Inhaltsverzeichnis
In einem aktuellen Projekt möchten wir aus einem On-Premise System einen Endpunkt in der BTP (ABAP Environment) aufrufen, um eine neue Entität anzulegen. Dabei handelt es sich um einen OData der Version 4 und der Endpunkt arbeitet mit Draft. Es sind einige Aufrufe nötig, um einen neuen Eintrag in der Entität zu erzeugen und auch zu aktivieren, sodass er in der Anwendung sichtbar wird.
Einleitung
Um was geht es genau in diesem Artikel? Der OData Endpunkt ist eine RAP Anwendung, die eine API zur Verfügung stellt, um die Möglichkeit zu bieten neue Einträge auf dem ABAP Environment anzulegen. Events und den Event Mesh können wir hier nicht nutzen, da wir von der neuen Instanz die Identifikation benötigen, um sie der Anwendung On-Premise zur Verfügung zu stellen, wir benötigen daher eine synchrone Schnittstelle.
Flow
Bevor wir also in die Details des Prozesses gehen, hier schon einmal der gesamte Flow in einer Grafik zusammengefasst. Für mehr Details einfach die Grafik anklicken und heranzoomen. In den folgenden Abschnitten werden wir dann die verschiedenen Aufrufe durchführen und den entsprechenden Nutzen erklären.
Destination
Bevor wir allerdings Aufrufe durchführen können, benötigen wir eine Destination bzw. RFC-Verbindung in der SM59. Dafür benötigen wir unseren Endpunkt auf der BTP und Zugangsdaten in Form von Basic Authentification (User und Passwort) oder einen OAuth Endpunkt. In unserem Beispiel nutzen wir die Basic Authentification zum Anmelden an dem Endpunkt. Dazu legen wir eine Verbindung vom Typ "G" an, Ziel ist der Account des ABAP Environments "<SUBACCOUNT-ID>.abap.eu10.hana.ondemand.com", hier solltest du deine Adresse eintragen.
Unter Login & Sicherheit dann den User und das Passwort hinterlegen und im unteren Teil SSL aktivieren.
Vorbereitung
HTTP Client
Als erstes benötigen wir einen HTTP Client, um Abfragen durchführen zu können. Dazu legen wir einen Client auf Basis der angelegten Verbindung an und speichern uns die Instanz in einer Member-Variable der Klasse. Im Anschluss aktvieren wir noch die Annahme von Cookies, da wir noch einige Anfragen durchführen werden.
cl_http_client=>create_by_destination( EXPORTING destination = 'TEST_xxx'
IMPORTING client = mo_http_client ).
mo_http_client->propertytype_accept_cookie = if_http_client=>co_enabled.
Senden und Empfangen
Weiterhin verwenden wir eine Hilfsmethode zum Senden des Requests und dem Empfangen des Response, diese Methode werden wir in den folgenden Abschnitten immer wieder aufrufen und sparen uns somit etwas Coding.
METHOD process_send_and_receive.
mo_http_client->send( EXCEPTIONS http_communication_failure = 1
http_invalid_state = 2
http_processing_failed = 3
http_invalid_timeout = 4
OTHERS = 5 ).
IF sy-subrc <> 0.
ENDIF.
mo_http_client->receive( EXCEPTIONS http_communication_failure = 1
http_invalid_state = 2
http_processing_failed = 3
OTHERS = 4 ).
IF sy-subrc <> 0.
ENDIF.
ENDMETHOD.
Zusätzlich definieren wir in der Klasse eine Konstante, die den Grundpfad zum OData Endpunkt enthält, am Ende sollte der Slash stehen.
CONSTANTS c_root_path TYPE string VALUE `/sap/opu/odata4/sap/<SERVICE>/srvd_a2x/sap/<NAME>/0001/`.
POST Request
Da unsere POST Requests immer recht ähnlich aufgebaut sind, definieren wir eine Methode, die wir in den folgenden Verarbeitungen immer wieder verwenden werden und die uns eine flexible Schnittstelle zur Verfügung stellt. Die Definition der Typen sieht wie folgt aus:
TYPES:
td_process TYPE char15,
BEGIN OF ts_answer,
code TYPE i,
reason TYPE string,
location TYPE string,
content TYPE string,
action TYPE string,
END OF ts_answer,
BEGIN OF ts_post_request,
process TYPE td_process,
uri TYPE string,
payload TYPE string,
if_match TYPE string,
END OF ts_post_request.
METHODS process_post
IMPORTING is_post_request TYPE ts_post_request
RETURNING VALUE(rs_result) TYPE ts_answer
RAISING cx_t100_msg.
Für das Logging gibt es in "TS_POST_REQUEST" eine Prozess ID, um die Fehlermeldung besser zuordnen zu können. Die URI enthält den angefragten Endpunkt und in der Payload würden die Daten als JSON liegen. Das Feld IF_MATCH setzt später noch einen Header in der Anfrage. Die Implementierung der Methode sieht wie folgt aus:
METHOD process_post.
DATA(ld_uri) = |{ c_root_path }{ is_post_request-uri }|.
mo_http_client->request->set_method( if_http_request=>co_request_method_post ).
mo_http_client->request->set_header_field( name = 'x-csrf-token' value = ms_auth-token ).
mo_http_client->request->set_header_field( name = '~request_uri' value = ld_uri ).
mo_http_client->request->set_header_field( name = 'Content-Type' value = 'application/json' ).
IF is_post_request-if_match IS NOT INITIAL.
mo_http_client->request->set_header_field( name = 'If-Match' value = is_post_request-if_match ).
ENDIF.
IF is_post_request-payload IS NOT INITIAL.
mo_http_client->request->set_cdata( is_post_request-payload ).
ENDIF.
process_send_and_receive( is_post_request-process ).
mo_http_client->response->get_status( IMPORTING code = rs_result-code
reason = rs_result-reason ).
rs_result-content = mo_http_client->response->get_cdata( ).
ENDMETHOD.
Folgende Schritte werden in der Methode durchgeführt:
- Erzeugung der Ziel URI
- Setzen der Header Informationen (Wichtig vor allem das Setzen des Tokens)
- Übernahme der Payload
- Senden und Empfangen der Anfrage
- Auslesen des Status und des Contents
Token
Bevor wir nun unseren ersten POST Request gegen den Endpunkt absetzen können, benötigen wir einen gültigen X-CSRF-Token und eine gültige SAP Session auf dem ABAP Environment. Dazu setzen wir das entsprechende Header Feld auf "fetch". Die aufzurufende URI bauen wir entsprechend aus Root und Entitätsnamen zusammen und übergeben sie ebenfalls an den Header.
mo_http_client->request->set_header_field( name = 'x-csrf-token' value = 'fetch' ).
mo_http_client->request->set_header_field( name = '~request_uri' value = |{ c_root_path }Entity| ).
process_send_and_receive( ).
ms_auth-token = mo_http_client->response->get_header_field( 'x-csrf-token' ).
Zum Abschluss führen wir die Anfrage aus und holen uns aus dem Antwort Header den entsprechenden Token, den wir in einem Attribut in der Klasse speichern, da wir diesen noch für weitere Anfragen benötigen.
Anlegen
Als nächsten Schritt wollen wir nun einen neuen Eintrag in der Entität anlegen, dazu führen wir unseren ersten POST gegen die Schnittstelle aus. Zuerst müssen wir die Payload nach JSON konvertieren, dazu verwenden wir eine Struktur, die dem Zieltypen entspricht, aber Vorsicht, da hier nicht die CamelCase Notation verwendet wird, sondern alle Feldbezeichner klein bleiben. Im Anschluss starten wir die Anlage über die Methode PROCESS_POST für unsere Zielentität (siehe URI).
DATA(ld_payload) = NEW /ui2/cl_abap2json( )->struc2json( is_payload ).
DATA(ls_answer) = process_post( is_post_request = VALUE #( process = cs_process-create
uri = `Entity`
payload = ld_payload ) ).
IF ls_answer-code = 201.
ms_auth-location = mo_http_client->response->get_header_field( 'location' ).
ELSE.
" Error Handling
ENDIF.
Der Server sollte uns ein HTTP Status "201 Created" melden, dann befindet sich ein neuer Eintrag in der Draft-Tabelle der Entität auf der BTP. In den Header Feldern erhalten wir auch im Feld "location" die nötige Informationen, um mit der neuen Entität zu arbeiten. Die Location übernehmen wir ebenfalls in unser Klassen Attribut, da wir es für die nächsten Anfragen benötigen.
Aktionen
Bevor wir nun die Aktionen ausführen können, benötigen wir erst einmal die entsprechende Information aus der Metadata des OData Services. Dazu den Endpunkt mit dem Zusatz $metadata aufrufen und in der XML Datei nach Activate suchen, entsprechend erhalten wir unsere definierten Actions.
Für die weitere Verarbeitung benötigen wir Prepare, Activate und Discard. Ebenso wichtig ist der Type, ansonsten wird die Aktion beim Ausführen nicht gefunden.
Prepare
Als nächster Schritt steht nun die Prepare Phase an, dazu bauen wir die URI mit Hilfe der gespeicherten Location auf, gefolgt von der Action mit dem Typ. Den Platzhalter und gegebenenfalls die Version musst du durch deine Daten austauschen. Die Anfrage geschieht wieder über einen POST Request an den Endpunkt. Wichtig bei diesem Schritt ist auch die Mitgabe des "IF-MATCH" Headers, hier hast du die Möglichkeit das ETag mitzugeben, in diesem Fall haben wir nur eine Instanz und geben einen Stern mit.
DATA(ld_uri) = |{ ms_auth-location }/com.sap.gateway.srvd_a2x.<NAME>.v0001.Prepare|.
DATA(ls_answer) = process_post( is_post_request = VALUE #( process = cs_process-prepare
uri = ld_uri
if_match = '*' ) ).
CASE ls_answer-code.
WHEN 200 OR 204.
" Success message
WHEN OTHERS.
add_error_to_log( ls_answer ).
discard_draft( ).
" Error Handling
ENDCASE.
Die Behandlung des Fehlers findest du im Abschnitt "Discard", dort gehen wir noch einmal auf das Mapping ein.
Activate
Wurde die Prepare Phase so weit ohne Fehler durchlaufen, kann der Activate durchgeführt werden. In dieser Phase werden die Validations im RAP auf der BTP durchlaufen und es können Fehlermeldungen zurückkommen, die eine Aktivierung des Datensatzes verhindern. Bei diesem Schritt ist ebenfalls der "IF-MATCH" Header wichtig.
DATA(ld_uri) = |{ ms_auth-location }/com.sap.gateway.srvd_a2x.<NAME>.v0001.Activate|.
DATA(ls_answer) = process_post( is_post_request = VALUE #( process = cs_process-activate
uri = ld_uri
if_match = '*' ) ).
CASE ls_answer-code.
WHEN 200 OR 204.
rs_result = ls_answer.
" Success message
WHEN OTHERS.
add_error_to_log( ls_answer ).
discard_draft( ).
" Error Handling
ENDCASE.
Vom Quellcode her ähnlich zum Prepare, es erfolgt die gleiche Fehlerbehandlung am Ende der Verarbeitung. Wenn wir einen HTTP Status "200 OK" erhalten, befindest sich auch im Body der Antwort die entsprechenden Daten zur Entität. Aus diesen können wir nun auch den neuen Schlüssel ableiten. Dazu ein Stück Quellcode, um den JSON-String in eine interne Struktur zu konvertieren.
DATA ls_entity TYPE ts_entity.
/ui2/cl_json=>deserialize( EXPORTING json = is_answer-content
CHANGING data = ls_entity ).
rd_result = ls_entity-key.
Discard
Löschen
Sollte es bei den Schritten Prepare oder Activate zu Fehlern kommen, dann wollen wir die "halbe" Instanz nicht stehen lassen, sondern diese wieder aus der Draft Tabelle entfernen. Dazu können wir nicht mit einem DELETE arbeiten, sondern müssen die Aktion "Discard" für den Datensatz auslösen.
DATA(ld_uri) = |{ ms_auth-location }/com.sap.gateway.srvd_a2x.<NAME>.v0001.Discard|.
DATA(ls_answer) = process_post( is_post_request = VALUE #( process = cs_process-discard
uri = ld_uri ) ).
CASE ls_answer-code.
WHEN 200 OR 204.
" Success message
WHEN OTHERS.
" Error Handling
ENDCASE.
Zum Abschluss erhalten wir den HTTP Status "200 OK" und die Instanz wurde aus der Draft Tabelle entfernt.
Fehlerbehandlung
Im Falle eines Fehlers kann der Body auch geparst werden, um die Fehlermeldungen zu extrahieren. Schauen wir uns im ersten Schritt einmal eine komplexe Meldung an.
Die Message im oberen Bereich ist die Hauptmeldung für den Fehler, im Detailbereich werden die weiteren Meldungen aufgelistet, die während der Validierung erzeugt wurden. Entsprechend legen wird für das Parsen der Meldung einen Typen an.
TYPES:
BEGIN OF ts_detail,
code TYPE string,
message TYPE string,
END OF ts_detail,
tt_detail TYPE STANDARD TABLE OF ts_detail WITH EMPTY KEY,
BEGIN OF ts_error,
code TYPE string,
message TYPE string,
details TYPE tt_detail,
END OF ts_error,
BEGIN OF ts_message,
error TYPE ts_error,
END OF ts_message.
Im letzten Schritt parsen wir das JSON in eine interne Struktur und können dann die Meldungen in unser Logobjekt übernehmen oder auf eine andere Weise ausgeben. Die TODOs sind im Quellcode markiert und müssen entsprechend implementiert werden.
DATA ls_message TYPE ts_message.
/ui2/cl_json=>deserialize( EXPORTING json = is_answer-content
CHANGING data = ls_message ).
" TODO: Log ls_message-error-message
LOOP AT ls_message-error-details INTO DATA(ls_detail).
" TODO: Log ls_detail-message
ENDLOOP.
Komplettes Beispiel
Wie immer hier noch einmal das komplette Beispiel dieses Artikels. Das Beispiel ist dieses Mal sehr lang und wird nicht ohne Anpassungen funktionieren. Erst einmal benötigst du eine Verbindung Richtung Cloud und dann einen entsprechenden Endpunkt als OData v4.
CLASS zcl_24bs_create_review DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
PRIVATE SECTION.
TYPES:
td_process TYPE char15,
BEGIN OF ts_answer,
code TYPE i,
reason TYPE string,
location TYPE string,
content TYPE string,
action TYPE string,
END OF ts_answer,
BEGIN OF ts_auth,
token TYPE string,
location TYPE string,
END OF ts_auth,
BEGIN OF ts_post_request,
process TYPE td_process,
uri TYPE string,
payload TYPE string,
if_match TYPE string,
END OF ts_post_request,
BEGIN OF ts_payload,
field1 TYPE string,
field2 TYPE string,
field3 TYPE string,
END OF ts_payload,
BEGIN OF ts_detail,
code TYPE string,
message TYPE string,
END OF ts_detail,
tt_detail TYPE STANDARD TABLE OF ts_detail WITH EMPTY KEY,
BEGIN OF ts_error,
code TYPE string,
message TYPE string,
details TYPE tt_detail,
END OF ts_error,
BEGIN OF ts_message,
error TYPE ts_error,
END OF ts_message,
BEGIN OF ts_entity,
key TYPE char32,
status TYPE char2,
isactiveentity TYPE abap_bool,
END OF ts_entity.
CONSTANTS c_root_path TYPE string VALUE `/sap/opu/odata4/sap/<SERVICE>/srvd_a2x/sap/<NAME>/0001/`. " TODO
CONSTANTS c_destination TYPE string VALUE 'TEST_xxx'. " TODO
CONSTANTS:
BEGIN OF cs_process,
token TYPE td_process VALUE 'TOKEN',
create TYPE td_process VALUE 'CREATE_ENTITY',
prepare TYPE td_process VALUE 'PREPARE',
activate TYPE td_process VALUE 'ACTIVATE',
discard TYPE td_process VALUE 'DISCARD',
END OF cs_process.
DATA mo_http_client TYPE REF TO if_http_client.
DATA ms_auth TYPE ts_auth.
METHODS create_token_and_cookie
RAISING cx_t100_msg.
METHODS create_entity
IMPORTING is_payload TYPE ts_payload
RAISING cx_t100_msg.
METHODS process_send_and_receive
IMPORTING id_process TYPE td_process
RAISING cx_t100_msg.
METHODS process_post
IMPORTING is_post_request TYPE ts_post_request
RETURNING VALUE(rs_result) TYPE ts_answer
RAISING cx_t100_msg.
METHODS prepare
RAISING cx_t100_msg.
METHODS activate
RETURNING VALUE(rs_result) TYPE ts_answer
RAISING cx_t100_msg.
METHODS discard_draft
RAISING cx_t100_msg.
METHODS extract_key_from_content
IMPORTING is_answer TYPE ts_answer
RETURNING VALUE(rd_result) TYPE char32.
METHODS add_error_to_log
IMPORTING is_answer TYPE ts_answer.
ENDCLASS.
CLASS zcl_24bs_create_review IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
CLEAR ms_auth.
cl_http_client=>create_by_destination( EXPORTING destination = CONV char20( c_destination )
IMPORTING client = mo_http_client ).
mo_http_client->propertytype_accept_cookie = if_http_client=>co_enabled.
TRY.
create_token_and_cookie( ).
create_entity( VALUE #( ) ).
prepare( ).
DATA(ls_activated_entity) = activate( ).
DATA(ld_new_key) = extract_key_from_content( ls_activated_entity ).
CATCH cx_t100_msg.
" TODO: Error Handling
ENDTRY.
mo_http_client->close( ).
" TODO: Set returning values
ENDMETHOD.
METHOD create_token_and_cookie.
mo_http_client->request->set_header_field( name = 'x-csrf-token' value = 'fetch' ).
mo_http_client->request->set_header_field( name = '~request_uri' value = |{ c_root_path }Review| ).
process_send_and_receive( cs_process-token ).
ms_auth-token = mo_http_client->response->get_header_field( 'x-csrf-token' ).
ENDMETHOD.
METHOD create_entity.
DATA(ld_payload) = NEW /ui2/cl_abap2json( )->struc2json( is_payload ).
DATA(ls_answer) = process_post( is_post_request = VALUE #( process = cs_process-create
uri = `Entity`
payload = ld_payload ) ).
IF ls_answer-code = 201.
ms_auth-location = mo_http_client->response->get_header_field( 'location' ).
ELSE.
" TODO: Error Handling
ENDIF.
ENDMETHOD.
METHOD process_post.
DATA(ld_uri) = |{ c_root_path }{ is_post_request-uri }|.
mo_http_client->request->set_method( if_http_request=>co_request_method_post ).
mo_http_client->request->set_header_field( name = 'x-csrf-token' value = ms_auth-token ).
mo_http_client->request->set_header_field( name = '~request_uri' value = ld_uri ).
mo_http_client->request->set_header_field( name = 'Content-Type' value = 'application/json' ).
IF is_post_request-if_match IS NOT INITIAL.
mo_http_client->request->set_header_field( name = 'If-Match' value = is_post_request-if_match ).
ENDIF.
IF is_post_request-payload IS NOT INITIAL.
mo_http_client->request->set_cdata( is_post_request-payload ).
ENDIF.
process_send_and_receive( is_post_request-process ).
mo_http_client->response->get_status( IMPORTING code = rs_result-code
reason = rs_result-reason ).
rs_result-content = mo_http_client->response->get_cdata( ).
ENDMETHOD.
METHOD prepare.
DATA(ld_uri) = |{ ms_auth-location }/com.sap.gateway.srvd_a2x.<NAME>.v0001.Prepare|.
DATA(ls_answer) = process_post( is_post_request = VALUE #( process = cs_process-prepare
uri = ld_uri
if_match = '*' ) ).
CASE ls_answer-code.
WHEN 200 OR 204.
" TODO: Success message
WHEN OTHERS.
add_error_to_log( ls_answer ).
discard_draft( ).
" TODO: Error Handling
ENDCASE.
ENDMETHOD.
METHOD activate.
DATA(ld_uri) = |{ ms_auth-location }/com.sap.gateway.srvd_a2x.<NAME>.v0001.Activate|.
DATA(ls_answer) = process_post( is_post_request = VALUE #( process = cs_process-activate
uri = ld_uri
if_match = '*' ) ).
CASE ls_answer-code.
WHEN 200 OR 204.
rs_result = ls_answer.
" TODO: Success message
WHEN OTHERS.
add_error_to_log( ls_answer ).
discard_draft( ).
" TODO: Error Handling
ENDCASE.
ENDMETHOD.
METHOD discard_draft.
DATA(ld_uri) = |{ ms_auth-location }/com.sap.gateway.srvd_a2x.<NAME>.v0001.Discard|.
DATA(ls_answer) = process_post( is_post_request = VALUE #( process = cs_process-discard
uri = ld_uri ) ).
CASE ls_answer-code.
WHEN 200 OR 204.
" TODO: Success message
WHEN OTHERS.
" TODO: Error Handling
ENDCASE.
ENDMETHOD.
METHOD process_send_and_receive.
mo_http_client->send( EXCEPTIONS http_communication_failure = 1
http_invalid_state = 2
http_processing_failed = 3
http_invalid_timeout = 4
OTHERS = 5 ).
IF sy-subrc <> 0.
" TODO: Error Handling
ENDIF.
mo_http_client->receive( EXCEPTIONS http_communication_failure = 1
http_invalid_state = 2
http_processing_failed = 3
OTHERS = 4 ).
IF sy-subrc <> 0.
" TODO: Error Handling
ENDIF.
ENDMETHOD.
METHOD extract_key_from_content.
DATA ls_entity TYPE ts_entity.
/ui2/cl_json=>deserialize( EXPORTING json = is_answer-content
CHANGING data = ls_entity ).
rd_result = ls_entity-key.
ENDMETHOD.
METHOD add_error_to_log.
DATA ls_message TYPE ts_message.
/ui2/cl_json=>deserialize( EXPORTING json = is_answer-content
CHANGING data = ls_message ).
" TODO: Log ls_message-error-message
LOOP AT ls_message-error-details INTO DATA(ls_detail).
" TODO: Log ls_detail-message
ENDLOOP.
ENDMETHOD.
ENDCLASS.
Learnings
Zum Zeitpunkt der Implementierung gab es leider keine Quelle bei SAP, die beschrieb wie man so etwas genau durchführt und auch nicht die ganzen Besonderheiten, die es zu beachten gibt. Hier noch einmal kurz die Zusammenfassung und die Lernings der Anbindung:
- Aktivierung der Cookies im HTTP Client (ACCEPT_COOKIE)
- Beachtung des RAP Flows (Prepare -> Activate)
- Ermittlung der korrekten Namen der Aktionen
- Löschen der Instanz bei Fehler (Discard)
- Verwendung des IF-MATCH Header für die Draft Instanz
Fazit
Die Implementierung solch einer Funktion ist nicht so schwer, wenn man alle Probleme gelöst hat. Doch da solche konkreten Beispiele aktuell fehlen, kann die Recherche aufwändig sein. Wenn du die Funktionsweise erst einmal verstand hast, dann sollten die weiteren Implementierungen kein Problem mehr sein und du für die nächsten Anfragen gewappnet sein.