Wednesday, 28 February 2018

How To Connect SAP with PowerShell via HTTP Request

More and more comes the communication with SAP back end system with standard protocols like HTTP in our focus. So it is easy possible to communicate with an SAP system without any additional libraries. In this post I describe an easy example how to connect an SAP system via HTTP request from PowerShell.

Get Data


I start with an easy handle_request method in an ABAP class, called Z_STEFAN, which uses the interface IF_HTTP_EXTENSION. The method detects the request method and delivers always status code 200, which means ok, and a text.

method IF_HTTP_EXTENSION~HANDLE_REQUEST.

  DATA:
    lv_verb TYPE string
    .

  lv_verb = SERVER->REQUEST->GET_HEADER_FIELD( name = '~request_method' ).
  CHECK lv_verb = 'GET'.

  SERVER->RESPONSE->SET_STATUS( code = 200 reason = 'Ok' ).
  SERVER->RESPONSE->SET_CDATA( data = 'Hello Stefan' ).

endmethod.

Now it is necessary to define a service with TAC SICF. I define a service, called stefan, at the highest level and I add my class Z_STEFAN at the handler list.

SAP ABAP Tutorials and Materials, SAP ABAP Certifications, SAP ABAP Guides, SAP ABAP Learning

Now it is possible to call this service from outside. Here a PowerShell program to do that. We start with the preparation of the user credentials and define a header for the HTTP request. The requested URI contains the service name, in this case stefan. Then we invoke the web request with the URI, a GET request and the header. If all is okay, we should see the result in the console.

#-Begin-----------------------------------------------------------------

  #-Sub Main------------------------------------------------------------
  Function Main() {

    $User = "BCUSER";
    $Password = "minisap";
    $Pair = $User + ":" + $Password;
    $encodedCreds = [System.Convert]::ToBase64String(`
      [System.Text.Encoding]::ASCII.GetBytes($Pair));
    $basicAuthValue = "Basic " + $encodedCreds;

    $header = New-Object `
      "System.Collections.Generic.Dictionary[[String],[String]]";
    $header.Add("cache-control", "no-cache");
    $header.Add("authorization", $basicAuthValue);
    $header.Add("accept", "*/*");
    $header.Add("accept-encoding", "gzip, deflate");

    $RequstURI = "http://abap:8000/stefan";

    Try {
      $Result = Invoke-WebRequest -Uri $RequstURI -Method Get `
        -Headers $header -ErrorVariable RestError;
    } Catch {
    } Finally {
      If ($Result.StatusCode -eq 200) {
        Write-Host $Result.Content;
      } Else {
        Write-Host $RestError;
      }
    }

  }

  #-Main----------------------------------------------------------------
  Main;

#-End-------------------------------------------------------------------

And here the result.

SAP ABAP Tutorials and Materials, SAP ABAP Certifications, SAP ABAP Guides, SAP ABAP Learning

Set and Get Data


In the next example I modify the class a little bit, so that it is possible to set and get data via an HTTP request. I add a header field called input which contains data in JSON format, in this case the name. With the method deserialize of the class /ui2/cl_json I dismantle the JSON string.

METHOD IF_HTTP_EXTENSION~HANDLE_REQUEST.

  TYPES: BEGIN OF ts_record,
           key   TYPE string,
           value TYPE string,
         END OF ts_record.

  DATA:
    lv_verb   TYPE string,
    lv_input  TYPE string,
    lt_input  TYPE STANDARD TABLE OF ts_record,
    ls_input  TYPE ts_record,
    lv_name   TYPE string,
    lv_output TYPE string
    .

  lv_verb = SERVER->REQUEST->GET_HEADER_FIELD( name = '~request_method' ).
  CHECK lv_verb = 'GET'.

  lv_input = SERVER->REQUEST->GET_HEADER_FIELD( name = 'input' ).

  /ui2/cl_json=>deserialize(
    EXPORTING
      json             = lv_input
    CHANGING
      data             = lt_input
  ).

  LOOP AT lt_input INTO ls_input.
    CASE ls_input-key.
      WHEN 'name'.
        lv_name = ls_input-value.
    ENDCASE.
  ENDLOOP.

  lv_output = `Hello ` && lv_name.

  SERVER->RESPONSE->SET_STATUS( code = 200 reason = 'Ok' ).
  SERVER->RESPONSE->SET_CDATA( data = lv_output ).

ENDMETHOD.

Here the PowerShell script to call this method. I add the new header field input with a JSON string, which is equal to the ts_record structure in the ABAP class.

#-Begin-----------------------------------------------------------------

  #-Sub Main------------------------------------------------------------
  Function Main() {

    $User = "BCUSER";
    $Password = "minisap";
    $Pair = $User + ":" + $Password;
    $encodedCreds = [System.Convert]::ToBase64String(`
      [System.Text.Encoding]::ASCII.GetBytes($Pair));
    $basicAuthValue = "Basic " + $encodedCreds;

    $header = New-Object `
      "System.Collections.Generic.Dictionary[[String],[String]]";
    $header.Add("cache-control", "no-cache");
    $header.Add("authorization", $basicAuthValue);
    $header.Add("accept", "*/*");
    $header.Add("accept-encoding", "gzip, deflate");

    $JSON = '[{"key":"name","value":"Stefan"}]';
    $header.Add("input", $JSON);

    $RequstURI = "http://abap:8000/stefan?sap-client=001";

    Try {
      $Result = Invoke-WebRequest -Uri $RequstURI -Method Get `
        -Headers $header -ErrorVariable RestError;
    } Catch {
    } Finally {
      If ($Result.StatusCode -eq 200) {
        Write-Host $Result.Content;
      } Else {
        Write-Host $RestError;
      }
    }

  }

  #-Main----------------------------------------------------------------
  Main;

#-End-------------------------------------------------------------------
The result is exactly the same as in the example before but now you have the possibility to set each name you like.

Interface Test


In the next step we will change the design of the class for a better unit testing and using of the functionality in a possible different context. This has no effect on the functionality.

CLASS Z_STEFAN DEFINITION
  PUBLIC
  CREATE PUBLIC.

  PUBLIC SECTION.

    INTERFACES IF_HTTP_EXTENSION.

    METHODS get_data
      IMPORTING
        VALUE(iv_input)  TYPE string
      RETURNING
        VALUE(rv_output) TYPE string.

  PROTECTED SECTION.

  PRIVATE SECTION.

ENDCLASS.

CLASS Z_STEFAN IMPLEMENTATION.


* <SIGNATURE>----------------------------------------------------------+
* | Instance Public Method Z_STEFAN->IF_HTTP_EXTENSION~HANDLE_REQUEST
* +--------------------------------------------------------------------+
* | [--->] SERVER                         TYPE REF TO IF_HTTP_SERVER
* +---------------------------------------------------------</SIGNATURE>
  METHOD IF_HTTP_EXTENSION~HANDLE_REQUEST.

    DATA:
      lv_verb   TYPE string,
      lv_output TYPE string
      .

    lv_verb = SERVER->REQUEST->GET_HEADER_FIELD( name = '~request_method' ).
    CHECK lv_verb = 'GET'.

    lv_output = me->get_data(
      iv_input = SERVER->REQUEST->GET_HEADER_FIELD( name = 'input' )
    ).

    SERVER->RESPONSE->SET_STATUS( code = 200 reason = 'Ok' ).
    SERVER->RESPONSE->SET_CDATA( data = lv_output ).

  ENDMETHOD.


* <SIGNATURE>----------------------------------------------------------+
* | Instance Public Method Z_STEFAN->GET_DATA
* +--------------------------------------------------------------------+
* | [--->] IV_INPUT                       TYPE        STRING
* | [<-()] RV_OUTPUT                      TYPE        STRING
* +---------------------------------------------------------</SIGNATURE>
  METHOD get_data.

    TYPES: BEGIN OF ts_record,
             key   TYPE string,
             value TYPE string,
           END OF ts_record.

    DATA:
      lt_input  TYPE STANDARD TABLE OF ts_record,
      ls_input  TYPE ts_record,
      lv_name   TYPE string
      .

    /ui2/cl_json=>deserialize(
      EXPORTING
        json             = iv_input
      CHANGING
        data             = lt_input
    ).

    LOOP AT lt_input INTO ls_input.
      CASE ls_input-key.
        WHEN 'name'.
          lv_name = ls_input-value.
      ENDCASE.
    ENDLOOP.

    rv_output = `Hello ` && lv_name.

  ENDMETHOD.


ENDCLASS.

And generally is it on this way possible to use e.g. Postman to check the correct functionality of the interface.

SAP ABAP Tutorials and Materials, SAP ABAP Certifications, SAP ABAP Guides, SAP ABAP Learning

The German Umlauts Problem


It is not possible to transfer German umlauts via the HTTP request header on this way. But you can pack those kind of information in a HTTP request body. In this case you can’t use the GET method for the HTTP request, it is necessary to change it to POST. Here the adjusted method. Now we check the POST method and convert the body data via two function calls in a string.

  METHOD IF_HTTP_EXTENSION~HANDLE_REQUEST.

    DATA:
      lv_verb   TYPE string,
      lv_length TYPE i,
      lt_bin    TYPE STANDARD TABLE OF solisti1,
      lv_output TYPE string,
      lv_data   TYPE xstring,
      lv_input  TYPE string
      .

    lv_verb = SERVER->REQUEST->GET_HEADER_FIELD( name = '~request_method' ).
    CHECK lv_verb = 'POST'.

    CALL FUNCTION 'SCMS_XSTRING_TO_BINARY'
      EXPORTING
        buffer        = SERVER->request->get_data( )
      IMPORTING
        output_length = lv_length
      TABLES
        binary_tab    = lt_bin.

    CALL FUNCTION 'SCMS_BINARY_TO_STRING'
      EXPORTING
        input_length  = lv_length
      IMPORTING
        text_buffer   = lv_input
      TABLES
        binary_tab    = lt_bin.

    lv_output = me->get_data(
      iv_input = lv_input
    ).

    SERVER->RESPONSE->SET_STATUS( code = 200 reason = 'Ok' ).
    SERVER->RESPONSE->SET_CDATA( data = lv_output ).

  ENDMETHOD.

Here now the PowerShell script. We add in the header a new field with the content type and the web request invoke contains now a body parameter with the JSON data.

#-Begin-----------------------------------------------------------------

  #-Sub Main------------------------------------------------------------
  Function Main() {

    $User = "BCUSER";
    $Password = "minisap";
    $Pair = $User + ":" + $Password;
    $encodedCreds = [System.Convert]::ToBase64String(`
      [System.Text.Encoding]::ASCII.GetBytes($Pair));
    $basicAuthValue = "Basic " + $encodedCreds;

    $header = New-Object `
      "System.Collections.Generic.Dictionary[[String],[String]]";
    $header.Add("cache-control", "no-cache");
    $header.Add("authorization", $basicAuthValue);
    $header.Add("accept", "*/*");
    $header.Add("accept-encoding", "gzip, deflate");
    $header.Add("content-type" , "application/json; charset=utf-8");

    $JSON = '[{"key":"name","value":"RĂ¼diger"}]';

    $RequstURI = "http://abap:8000/stefan?sap-client=001";

    Try {
      $Result = Invoke-WebRequest -Uri $RequstURI -Method Post `
        -Headers $header -Body $JSON -ErrorVariable RestError;
    } Catch {
    } Finally {
      If ($Result.StatusCode -eq 200) {
        Write-Host $Result.Content;
      } Else {
        Write-Host $RestError;
      }
    }

  }

  #-Main----------------------------------------------------------------
  Main;

#-End-------------------------------------------------------------------

SAP ABAP Tutorials and Materials, SAP ABAP Certifications, SAP ABAP Guides, SAP ABAP Learning

SAP ABAP Tutorials and Materials, SAP ABAP Certifications, SAP ABAP Guides, SAP ABAP Learning

Read Any Table and Get Their Content in JSON Format


Here now a tiny modification of the get_data method. Now the name contains a table name and the method delivers the whole content of a table in JSON format.

CLASS Z_STEFAN DEFINITION
  PUBLIC
  CREATE PUBLIC.

  PUBLIC SECTION.

    INTERFACES IF_HTTP_EXTENSION.

    METHODS get_data
      IMPORTING
        VALUE(iv_input)  TYPE string
      RETURNING
        VALUE(rv_output) TYPE string.

  PROTECTED SECTION.

  PRIVATE SECTION.

ENDCLASS.


CLASS Z_STEFAN IMPLEMENTATION.


* <SIGNATURE>----------------------------------------------------------+
* | Instance Public Method Z_STEFAN->GET_DATA
* +--------------------------------------------------------------------+
* | [--->] IV_INPUT                       TYPE        STRING
* | [<-()] RV_OUTPUT                      TYPE        STRING
* +---------------------------------------------------------</SIGNATURE>
  METHOD get_data.

    TYPES: BEGIN OF ts_record,
             key   TYPE string,
             value TYPE string,
           END OF ts_record.

    FIELD-SYMBOLS:
      <lt_table> TYPE STANDARD TABLE
      .

    DATA:
      lt_input  TYPE STANDARD TABLE OF ts_record,
      ls_input  TYPE ts_record,
      lv_name   TYPE string,
      lo_table  TYPE REF TO data
      .

    /ui2/cl_json=>deserialize(
      EXPORTING
        json             = iv_input
      CHANGING
        data             = lt_input
    ).

    LOOP AT lt_input INTO ls_input.
      CASE ls_input-key.
        WHEN 'name'.
          lv_name = ls_input-value.
      ENDCASE.
    ENDLOOP.

    CREATE DATA lo_table TYPE TABLE OF (lv_name).
    ASSIGN lo_table->* TO <lt_table>.

    SELECT * INTO TABLE <lt_table> FROM (lv_name).

    rv_output = /ui2/cl_json=>serialize( data = <lt_table> ).

  ENDMETHOD.


* <SIGNATURE>----------------------------------------------------------+
* | Instance Public Method Z_STEFAN->IF_HTTP_EXTENSION~HANDLE_REQUEST
* +--------------------------------------------------------------------+
* | [--->] SERVER                         TYPE REF TO IF_HTTP_SERVER
* +---------------------------------------------------------</SIGNATURE>
  METHOD IF_HTTP_EXTENSION~HANDLE_REQUEST.

    DATA:
      lv_verb   TYPE string,
      lv_length TYPE i,
      lt_bin    TYPE STANDARD TABLE OF solisti1,
      lv_output TYPE string,
      lv_data   TYPE xstring,
      lv_input  TYPE string
      .

    lv_verb = SERVER->REQUEST->GET_HEADER_FIELD( name = '~request_method' ).
    CHECK lv_verb = 'POST'.

    CALL FUNCTION 'SCMS_XSTRING_TO_BINARY'
      EXPORTING
        buffer        = SERVER->request->get_data( )
      IMPORTING
        output_length = lv_length
      TABLES
        binary_tab    = lt_bin.

    CALL FUNCTION 'SCMS_BINARY_TO_STRING'
      EXPORTING
        input_length  = lv_length
      IMPORTING
        text_buffer   = lv_input
      TABLES
        binary_tab    = lt_bin.

    lv_output = me->get_data(
      iv_input = lv_input
    ).

    SERVER->RESPONSE->SET_STATUS( code = 200 reason = 'Ok' ).
    SERVER->RESPONSE->SET_CDATA( data = lv_output ).

  ENDMETHOD.


ENDCLASS.

SAP ABAP Tutorials and Materials, SAP ABAP Certifications, SAP ABAP Guides, SAP ABAP Learning

1 comment:

  1. Im really impressed with your blog articale, such great’s useful information you declared here.
    SAP FICO Training Institute Gurgaon

    ReplyDelete