REST Services in ABAP

Wer SAPUI5 Apps implementiert kennt OData. OData ist ein Standard für die Implementierung von REST-Services. Dass es auch möglich ist, REST Services in ABAP nativ zu implementieren, ist unserer Erfahrung nach nicht so bekannt. Die Eclipse Plugins der ABAP Development Tools kommunizieren zum Beispiel über REST mit dem SAP ABAP. Du wolltest schon immer wissen, wie das eigentlich geht? Dann bist du hier richtig, denn wir wollen heute versuchen die OData Funktionalitäten CRUDQ (CREATE, READ, UPDATE, DELETE, QUERY) mit einem nativen REST Service zu implementieren – los geht’s!

REST steht erst einmal für Representational State Transfer und hat den Anspruch mit einer einheitlichen Schnittstelle über bereits vorhandene WWW Architektur eine vereinfachte Mashine-2-Mashine-Communication zu ermöglichen. REST selbst ist dabei weder Protokoll noch Standard und wird auch dadurch einzigartig, dass über die angesprochenen URIs keine Methodeninformationen übergeben werden (müssen), sondern nur eine Ressource. SAP liefert bereits die benötigten Grundstrukturen mit, die einfach implementiert werden können um REST Services zu erstellen.

Ziel unseres REST-Service ist es, mithilfe von HTTP-Requests die CRUDQ Funktionalitäten für Salesorder Header und die zugehörigen Items aus dem SAP EPM Model anzuwenden. 

REST Services in ABAP: Klassenmodell

Grundsätzlich besteht unser REST-Service aus zwei zusammenhängenden Klassen. Auf der einen Seite haben wir die Application-Class (ZCL_PH_REST_EPM_APP) auf der anderen Seite die Ressource-Class (ZCL_PH_REST_EPM_RES), welche wir für diesen hier als lokale Objekte illustrativ angelegt haben. Die Resource Klasse regelt dabei die zur Verfügung zu stellenden Datenquellen und DB-Operations, welche wiederum aus der Application-Klasse aufgerufen werden.

Man kann sich das Ganze auch etwas bildlicher vorstellen: Tun wir mal so, als sei der der Ablauf unseres REST Service eine Pizzabestellung. Statt umständlich loszufahren und an der Theke eine Pizza zu ordern und diese wieder mit nach Hause zu nehmen, ist es auch möglich den Online Weg zu nehmen. Als Kunde sind wir der Client, der eine Anfrage losschickt. Das tun wir zum Beispiel über das Portal, welches unser Lieferdienst (in unserem Beispiel der Server) zur Verfügung stellt. Unsere ZCL_PH_REST_EPM_APP Klasse fungiert (hat nichts mit Pizza Fungi zu tun) genau wie dieses Portal als Schablone für die Bestellung einer definierten Ressource. Danach wird diese Anfrage entgegengenommen und vom Personal des Ladens ZCL_PH_REST_EPM_RES bearbeitet, bis schließlich das Ergebnis, unsere Pizza bzw. unsere angeforderten Daten zu uns zurückgeschickt werden.

Die Application-Klasse konstruiert also den Rahmen und legt fest über welche URI welche Handler Klasse angesprochen werden soll. Die Handler Klassen sind hierbei als lokale Klassen der Resource Klasse implementiert.

 

Application Klasse ZCL_PH_REST_EPM_APP: Registrierung von URIs und Handler Klassen

Die Application-Klasse erbt von der von SAP zur Verfügung gestellten Klasse CL_REST_HTTP_HANDLER. Diese Klasse ermöglicht uns die Erstellung eines REST-Services für das HTTP-Protokoll.

REST Application Klasse

Zunächst haben wir die Tabelle (mt_resource_metadata) auf der Basis des Strukturtypen TS_RESOURCE_METADATA angelegt. Die Tabelle dient zum einen zur eindeutigen Definition / Bestimmung der URI und zum anderen der Angabe der genutzten lokalen Handler-Klasse. Im CONSTRUCTOR beginnt die Initialisierung der angesprochenen Tabelle.

Das 1. Url Pattern /header/{HEADER_KEY} wird beispielsweise für unsere GET Methode benötigt. Der Platzhalter {header_key} im Template dient uns – wie wir später sehen – zum Anlegen der eindeutigen URI gemäß des Angesprochenen Node_Key aus dem Sales Order Header bzw. der DB Tabelle snwd_so. Die spezifische Handler Klasse wird in der Resource Metadata dem jeweiligen URI Template zugeordnet. Der CONSTRUCTOR kann dabei natürlich durch beliebig vielen URI-Patterns erweitert werden. Möchtest du beispielsweise statt der der Header-Daten die ITEMS einer Sales-Order ansprechen, so müsstest du hier ein neues Template mit dazugehöriger Handler-Klasse definieren.

In der redefinierten Methode if_rest_application~get_root_handler wird unsere Ressource-Klasse angesprochen. Die Metadaten der Resource werden an den Constructor der Resource Klasse übergeben. Dies erleichtert dort die Delegation an die entsprechende Anwendungslogik.

  METHOD if_rest_application~get_root_handler.
    DATA lr_rest_handler TYPE REF TO cl_rest_router.
    data lT_PARAMETER	TYPE ABAP_PARMBIND_TAB.
    data lr_parameter TYPE REF TO data.

    constants: lc_handler_class TYPE SEOCLSNAME value 'ZCL_PH_REST_EPM_RES'.

    CREATE OBJECT lr_rest_handler.

    loop at mt_resource_metadata ASSIGNING FIELD-SYMBOL(<ls_resource_metadata>).

     get REFERENCE OF <ls_resource_metadata> INTO lr_parameter.

     lt_parameter = value #(
       ( name = 'IS_RESOURCE_METADATA'
         kind = cl_abap_objectdescr=>exporting
         value = lr_parameter )
         ).

     lr_rest_handler->attach(
      iv_template = <ls_resource_metadata>-template
      iv_handler_class = lc_handler_class
      iT_PARAMETER = lt_parameter  ).
    endloop.

    ro_root_handler = lr_rest_handler.

  ENDMETHOD.

Resource-Klasse ZCL_PH_REST_EPM_RES: Logik des REST-Services

In der Resource-Class ZCL_PH_REST_EPM_RES spielt die eigentliche Musik. Wie wir sehen, erbt die Klasse von der abstrakten Klasse CL_REST_RESOURCE. Für jede HTTP Method (GET, POST, PUT und DELETE) existiert eine korrespondierende Methode. Diese Methoden sind rezuimplementieren. Die folgende Abbildung zeigt den Aufbau der Resource Klasse und die Implementierung der GET-Methode. Gut zu sehen ist, wie auf Basis der Metainformationen an die lokale Handlerklasse delegiert. Im Rahmen dieser Delegation erfolgt auch die Umsetzung einer HTTP Method in die CRUDQ-Methode.  Beim GET kann entweder ein Function Import, eine Query oder ein Read vorliegen.

REST Resource Klasse

Helper Klasse LCL_HELPER

Die LCL_HELPER Klasse beinhaltet die Funktionen, die wir in allen Handler Klassen benötigen. Dies sind aktuell die Funktionen, um einen beliebige ABAP Datenstruktur in das JSON Format zu serialisieren bzw. eine JSON Nachricht in eine ABAP Datenstruktur zu deserialisieren. Wir benutzen hier die SAP Klasse /ui2/cl_json.

Handler Klasse LCL_BASE_HANDLER

Die Handler Klassen handeln den eigentlichen Teil der Datenbankzugriffe ab. Jede Handler Klasse erbt von LCL_BASE_HANDLER. LCL_BASE_HANDLER implementiert jede CRUDQ-Methode, indem sie eine NotImplemented Ausnahme wirft. Motivation hierfür, dass eine Handler nicht zwingend für alle CRUDQ-Methoden definiert ist. Konkrete Handler Klassen wie lcl_header_handler oder lcl_item_handler redefinieren diese Methode nach Bedarf.

Implementierung Read-Methode im Handler

Die Funktionsweise der Read-Methode lässt sich am besten an der Unit Test Methode header_read nachvollziehen.

  METHOD header_read.
    DATA lr_entity TYPE REF TO if_rest_entity.
    DATA lv_response TYPE string.

    mr_rest_client->set_request_uri( |/header/0050568500ED1EE2A5A7CF2ACDB80AD0| ).
    mr_rest_client->get( ).

    cl_abap_unit_assert=>assert_equals(
        act                  = mr_rest_client->get_status( )
        exp                  = '200' ).

    lr_entity = mr_rest_client->get_response_entity( ).
    lv_response = lr_entity->get_string_data( ).
  ENDMETHOD.

Über die Url /header/0050568500ED1EE2A5A7CF2ACDB80AD0 wird der Salesorder Header mit dem angegebenen Schlüssel gelesen. Setz mal einen Breakpoint in die Methode LCL_HEADER_HANDLER, READ und führe den Unit Test aus. Die folgende Abbildung aus dem Debugger zeigt, wie der Kontrollfluss vom Unit Test zur Application Klasse und danach zur Resource Klasse abläuft.

REST Resource Read Debug

Die Methode get_uri_attributes im LCL_BASE_HANDLER liest die Attribute der Url und mappt diese auf die übergebene Struktur. Dies ist notwendig, wenn wir über einen Schlüssel einen eindeutigen Datensatz ansprechen, wie es beim READ, UPDATE und DELETE der Fall ist.

  
  METHOD get_uri_attributes.
    DATA lt_name_value TYPE tihttpnvp.
    FIELD-SYMBOLS <ls_name_value> TYPE ihttpnvp.
    FIELD-SYMBOLS <lv_value> TYPE any.

    lt_name_value = ir_request->get_uri_attributes( ).

    LOOP AT lt_name_value ASSIGNING <ls_name_value>.
      ASSIGN COMPONENT <ls_name_value>-name OF STRUCTURE rs_uri_attributes TO <lv_value>.
      <lv_value> = <ls_name_value>-value.
    ENDLOOP.
  ENDMETHOD.

Implementierung der Query-Methode im Handler

Mit der Implementierung der Read-Methode im Hinterkopf geht die Implementierung von Create, Update und Delete schnell von der Hand. Ein bisschen schwieriger ist die Query Methode. Die Query Methode soll folgende Url verarbeiten können: /header?filter=so_id bt 0500000001:0500000010,gross_amount gt 1000&top=5&orderby=so_id desc, lifecycle_status. Wir wollen also nach einem Wertebereich filtern, pagen und sortieren. Diese Query wird im Unit Test header_query verwendet.

  METHOD query.
    DATA ls_url_param TYPE ts_url_param.
    DATA lt_query_filter TYPE tt_query_filter.
    FIELD-SYMBOLS <ls_query_filter> TYPE ts_query_filter.
    DATA lt_rng_so_id TYPE tt_rng.
    DATA lt_rng_created_at TYPE tt_rng.
    DATA lt_rng_gross_amount TYPE tt_rng.
    DATA lt_rng_lifecycle_status TYPE tt_rng.
    DATA ls_ent_headers TYPE ts_ent_header.
    DATA ls_headers TYPE snwd_so.
    DATA lt_headers TYPE TABLE OF snwd_so.
    DATA lt_ent_headers LIKE TABLE OF ls_ent_headers.
    DATA lv_max_rows TYPE i.
    DATA lt_sql_order_by TYPE string_table.

    ls_url_param = get_url_param( ir_request ).
    "Skip und Top addieren
    lv_max_rows = ls_url_param-top + ls_url_param-skip.

    lt_query_filter = parse_query_filter( ls_url_param-filter ).

    lt_sql_order_by = parse_query_orderby(
      iv_tabname = 'SNWD_SO'
      iv_orderby = ls_url_param-orderby ).

    LOOP AT lt_query_filter ASSIGNING <ls_query_filter>.
      CASE <ls_query_filter>-property.
        WHEN 'SO_ID'.
          lt_rng_so_id = <ls_query_filter>-t_rng.
        WHEN 'CREATED_AT'.
          lt_rng_created_at = <ls_query_filter>-t_rng.
        WHEN 'GROSS_AMOUNT'.
          lt_rng_gross_amount = <ls_query_filter>-t_rng.
        WHEN 'LIFECYCLE_STATUS'.
          lt_rng_lifecycle_status = <ls_query_filter>-t_rng.
        WHEN OTHERS.
          RAISE EXCEPTION TYPE lcx_rest_resource_exception
            EXPORTING
              iv_msg = |Property { <ls_query_filter>-property } ist ungültig|.
      ENDCASE.
    ENDLOOP.

    "selectieren bis skip+top rows
    SELECT * FROM snwd_so INTO TABLE lt_headers
      UP TO  lv_max_rows ROWS
      WHERE so_id IN lt_rng_so_id
        AND created_at IN  lt_rng_created_at      "Filterkriterien
        AND gross_amount IN lt_rng_gross_amount
        AND lifecycle_status IN lt_rng_lifecycle_status
      ORDER BY (lt_sql_order_by).

    "Zeile 1 bis zeile "skip" löschen
    IF ls_url_param-skip > 0.
      DELETE lt_headers FROM 1 TO ls_url_param-skip.
    ENDIF.

    MOVE-CORRESPONDING lt_headers TO lt_ent_headers.

    mr_helper->serialize_json(
        iv_data     = lt_ent_headers "Header-Daten werden an json übergeben
        ir_response = ir_response ).

  ENDMETHOD.

Die Methode get_url_param dient zur Extraktion der Parameter filter, top, skip und oderby aus der Url.

Das Schwierigste

Für die Query der Handler reichen diese Methoden natürlich noch nicht aus. Wir müssen ebenfalls identifizieren ob und wenn ja welche Operatoren einem Filter zugeordnet sind. Z.B. benötigen wir dies, wenn wir alle Salesorder-Header haben wollen, die in einer bestimmten Zeile einen Wert überschreiten oder unterschreiten. Die Methode parse_query_filter parse die im filter Parameter übergebenen Werte und konvertiert diese in einen ABAP Range.

Eine Referent zu den in ABAP nutzbaren RegEx Ausdrücken findest du hier.

Andere  Handler

Die lokalen Klasse LCL_HEADER_HANDLER und LCL_ITEM_HANDLER erben vom LCL_BASE_HANDLER und redefinieren die Datenbanktypischen CRUDQ Methoden für die tatsächliche Anwendung.

Bei den GET-Operationen der Methoden READ und QUERY arbeiten wir mit einfachen SELECT–Anweisung. Die mr_helper-Klasse serialisiert im Folgenden den zurückgegebenen Datensatz. Bei den anderen drei Methoden folgt am Anfang eine Deserialisierung der Response, gefolgt von den Anweisungen INSERT (POST), UPDATE (PUT) und DELETE (DELETE). Am Ende steht wieder die Serialisierung der Daten.

 

Function Import Handler (lif_fctimp_handler, lcl_fctimp_lifecycle_handler)

In einer typischen Anwendung gibt es neben den CRUDQ-Funktionen auf Datenobjekten auch sogenannte Function Imports. Dies sind Funktionalitäten, die nicht in das CRUDQ-Schema passen. Ein Beispiel ist das Setzen des Lifecycle Status Feldes bei einer Sales Order Header. Die Url sieht so aus: /setLifecycleStatus?SO_ID=0500000002&status=N (Unit Test setlifecyclestatus). Auch das kann man einfach im Rahmen dieses Service umsetzen.

Zunächst erstellen wir uns in der Resource-Klasse ein lokales Interface. Dieses Interface beinhaltet lediglich die execute-Methode. Die Implementierung des Function Import nehmen wir in der lokalen Klasse lcl_fctimp_lifecycle_handler, welche dieses Interface implementiert.

In der Application Class definieren wir das Url Template sowie die Handler Klasse. Damit wir in der Resource Klasse ermitteln können, ob ein Function Import aufzurufen ist, sind die Metadaten um das Boolen Feld is_function_import erweitert.

…um sie dann auf Basis des Templates im Constructor() der APPLICATION  Klasse zur passenden Funktion zu leiten die wir für diesen Zweck implementiert haben:

SICF: Publizierung des REST-Service als HTTP-Endpunkt

Um den Service auch von außen per HTTP erreichbar zu machen, musst Du ihn im SAP erst einmal registrieren. Dafür startest du die Transaktion SICF (System Internet Communication Framework) und rufst die Übersicht “Pflege der Services” auf. Dort legen wir unter default_host/sap/bc/rest unseren Service an, damit dieser als HTTP Endpunkt zur Verfügung steht.

Das sieht dann folgendermaßen aus:

 

Output & Unit Tests

Als Beispiel ist im Folgenden einmal das Resultat unserer GET-Methode aufgeführt.

In der oberen Abbildung siehst du einen Ausschnitt der erwähnten Datenbank Tabelle snwd_so. Der Node_Key ist Bestandteil der DB-Tabelle und eine von SAP erstellte GUID und dient dir zur eindeutigen Identifikation von Datensätzen. Deshalb verwendest du ihn hier auch als Paramter für die URI. In der folgenden Abbildung siehst du das Resultat unserer GET-Methode, zum Aufruf des gewünschten Datensatzes im Browser, in JSON:

Um alle Anwendungsfälle auch richtig und vor allem persistent durchtesten zu können solltest du noch entsprechende Unit Tests erstellen – so stellst du sicher, dass auch potenzielle Änderungen in den Handlern keine bösen Überraschungen bergen. Dafür kannst du in der APPLICATION Klasse eine lokale Testklasse anlegen und in dieser die Testmethoden definieren und implementieren. Am Beispiel der GET Methode oben könnte eine Implementierung etwa so aussehen:

 

Und damit sind wir auch durch! Wir hoffen, dass wir dir einen guten Einblick verschaffen konnten wie du REST Services in ABAP erstellen kannst. Obwohl CRUDQ und Function Imports mit diesem Ansatz maßgeschneidert und recht komfortabel möglich sind, ist native REST Services in ABAP allerdings keine vollständige Alternative zum klassischen OData Ansatz. Eine Serialisierung in atom bzw. xml ist hier nämlich nicht möglich.

Wenn du noch Fragen zum Thema hast schreib uns einfach – wir freuen uns über eure Nachrichten.

Du programmierst, bist ABAP-interessiert und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.

Veröffentlicht in ABAP

Über den Autor

Andreas Fischer

Kommentar verfassen