Wednesday 8 September 2021

Web Development with (Cloud) ABAP

Introduction

Using Cloud ABAP, we will create an HTTP service, and use it to serve dynamically, server-side rendered HTML. I.e., we will be doing web development in ABAP. Our use of ABAP will be quite similar to, for example, using Flask in Python. Of course, however, we don’t actually have a framework to use, so we will have to do some things – like the template rendering – ourselves. This tutorial includes every step of the process, from creating the HTTP service, to using the web app. To do this, I will use the demo use-case of a movie database.

All of the code is available in this GitHub repository. Keep in mind that there is some extra ABAP logic in there that is used for storing the web app’s HTML. I briefly explain what it is in the demo section, but you can pretty much ignore it. The only important classes are Demo (zcl_ss_awd_demo), which is the auto-generated handler class, Helper (zcl_ss_awd_helper) and Movies (zcl_ss_awd_movies). All of them and their purposes will be explained below.

Note about the naming: ss = Stoyko Stoev and awd = ABAP Web Development

Creating the HTTP service

This step could not be any simpler – after logging in Eclipse ABAP Development Tools with our SAP Cloud Platform user data, we choose a package, select New > Other ABAP Repository Object > HTTP Service, input some name… and we are done! We then see the URL and the Handler Class for this service, which the system auto-generated for us. Besides this, a lot of other auto-generated things now show up in our package:

Web Development with (Cloud) ABAP, SAP ABAP Certification, SAP ABAP Career, SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning
Auto-generated objects related to the HTTP service

However, for the purpose of our tutorial, we don’t really care about anything other than the Handler Class and the URL.

Implementing the Handle Request method and app paths


Our newly generated Handler Class implements the if_http_service_extension interface. This means that we have to have the interface’s method handle_request in our class. I’ll present the different elements of its logic in the easiest to grasp order, while the full code will be available at the end of the section (as well as in the GitHub repository).

To get the path the user is requesting, we do the following:

DATA(path) = request->get_header_field( '~path_info' ).

So, for example, if the user is on
https://<id>.abap-web.eu10.hana.ondemand.com/sap/bc/http/sap/zss_awd_demo/browse, the method call will return the string “/browse”. We will be using this akin to Flask’s routes, if you are familiar with Flask (if not, doesn’t matter, it’s not important at all). Then, we format the path:

path = zcl_ss_awd_helper=>format_path( path ).

I will not be showing the format_path method in this section (it is available in the GitHub), as all it does is: (1) remove the first character; and (2) convert the string to uppercase. So, “/browse” simply becomes “BROWSE”.

The next step is to dynamically call a method with this name:

CALL METHOD me->(path).

This is why we needed to convert to uppercase (dynamic method call only works with uppercase). From here on, when we want to add a new route to our application, we simply add a new method with the route name that we want… and that’s literally all of it! So, if we want to actually implement a “/browse” route, we add a new method called “browse”. This is (visually) a bit similar to doing this in Flask – which is what I meant we will be using the path akin to Flask’s routes:

@app.route('/browse')
def browse():

But, again, if you don’t know Python and/or Flask, this is not important at all.

Then, what we would like to do is to be able to use the request and response objects inside the methods for the different paths. We obviously need this, to be able to return something to the user, and to also know what exactly they are requesting. To do this, we will simply make the request and response instance attributes:

    me->request = request.
    me->response = response.

Now, for the impatient, let’s immediately see the fruits of our labor. With only this, we can simply add a method hello_world (without any parameters), and add the following code inside:

  METHOD hello_world.
    response->set_text( 'Hello, world!' ).
  ENDMETHOD.

The method set_text of the response object “sets the HTTP body of this entity to the given string data”.

So, if we go to https://<our HTTP Service URL>/hello_world, we see this greeting indeed!

Web Development with (Cloud) ABAP, SAP ABAP Certification, SAP ABAP Career, SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning
Hello, world!

Here’s how the full code looks at this point in time:

CLASS zcl_ss_awd_demo DEFINITION PUBLIC CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES:
      if_http_service_extension.

  PRIVATE SECTION.
    DATA:
      request  TYPE REF TO if_web_http_request,
      response TYPE REF TO if_web_http_response.

    METHODS:
      hello_world.
ENDCLASS.

CLASS zcl_ss_awd_demo IMPLEMENTATION.
  METHOD if_http_service_extension~handle_request.
    me->request = request.
    me->response = response.

    DATA(path) = request->get_header_field( '~path_info' ).
    path = zcl_ss_awd_helper=>format_path( path ).

    CALL METHOD me->(path).
  ENDMETHOD.

  METHOD hello_world.
    response->set_text( 'Hello, world!' ).
  ENDMETHOD.
ENDCLASS.

To help you get acquainted also with the request object, let’s add some dynamicity to our application. Let’s, instead of saying Hello, world!, say Hello, <name>!, where <name> will be a URL query parameter. The way to get a query parameter is to use the method get_form_field of the request object. We modify our hello_world method as follows:

  METHOD hello_world.
    DATA(name) = request->get_form_field( 'name' ).
    response->set_text( |Hello, { name }!| ).
  ENDMETHOD.

Now, if we request the URL https://<our HTTP Service URL>/hello_world?name=<some name> we will see a dynamic greeting! Here’s the demo:

Web Development with (Cloud) ABAP, SAP ABAP Certification, SAP ABAP Career, SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning
Hello, Bobby!

Web Development with (Cloud) ABAP, SAP ABAP Certification, SAP ABAP Career, SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning
Hello, Dessy!

Anyway, back to our handle_request method. We are almost done, but let’s add some edge-case considerations. One, if the path is empty, serve the homepage, and two – if there is no such method in the class, serve a 404 page.

  METHOD if_http_service_extension~handle_request.
    me->request = request.
    me->response = response.

    DATA(path) = request->get_header_field( '~path_info' ).

    IF strlen( path ) EQ 0.
      home(  ).
      RETURN.
    ENDIF.

    path = zcl_ss_awd_helper=>format_path( path ).
    TRY.
        CALL METHOD me->(path).
      CATCH cx_sy_dyn_call_illegal_method.
        not_found_404(  ).
    ENDTRY.
  ENDMETHOD.

With this, we conclude the method! These less than 15 lines of code (without the white-space) are all it takes to create a basic web app in ABAP – impressive! But how do we serve HTML? Very simple! We just pass HTML as the string parameter for set_text of the response object, like this for example:

  METHOD home.
    response->set_text( |<html> <head> <title> Home </title> </head> <body> <p>Home page</p> </body> </html>| ).
  ENDMETHOD.
 
We do something similar with our 404 method:

  METHOD not_found_404.
    response->set_text( |<html> <head> <title> 404 </title> </head> <body> <p>404: no such page exists</p> </body> </html>| ).
  ENDMETHOD.

And we have our working web app!

Web Development with (Cloud) ABAP, SAP ABAP Certification, SAP ABAP Career, SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning
Home page

Web Development with (Cloud) ABAP, SAP ABAP Certification, SAP ABAP Career, SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning
404 page

Admittedly, that’s not very impressive, is it? So let’s do something more interesting!

Demo use-case


Here, I’ll have to make one note. Due to ABAP’s restriction of not more than 255 characters per line, I decided to store the HTML in a DB table. I also exposed the table using a REST service supporting all the HTTP verbs, so that I can edit the HTML files locally and then store them in the DB by connecting with a Python script to the REST service. But that’s not really related to our main goal, so I will not be explaining how I did this. Still, the code for this is also in the GitHub if you would like to check it out.

To get the relevant HTML, I added a method to my helper class that fetches it from the DB table:

  METHOD get_page_html.
    SELECT SINGLE content
      FROM zss_awd_html
      WHERE page_name = @page_name
      INTO @result.
  ENDMETHOD.

(in a productive environment, you probably wouldn’t want to have SQL code in your Helper class, but we are aiming for simplicity)

So, I created the HTML files index.html, browse.html, add.html, 404.html that are all using this W3 Schools template. The only interesting thing happening inside is that I use variables that would later be replaced with the dynamically rendered content. Once again, similar to Flask (through the jinja rendering) – and once again, totally cool if you are unfamiliar with Flask. To identify the variables, I prefix them with $. Here’s one example:

<div class="row">
  <div class="main">
    <h2>Here is our movie catalog:</h2>
    $movies
  </div>
</div>

Here, $movies would be replaced with a list of all the movies in the database.

Speaking of the movies database, here’s how the Movies (zss_awd_movies) table looks like:

Web Development with (Cloud) ABAP, SAP ABAP Certification, SAP ABAP Career, SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning
Movies Table

The underscore at the end of the year field is because year is a reserved word and cannot be used as a fieldname.

I also created a class to read from and write to the table, that also has one method that converts an ABAP internal table to an HTML unordered list:

CLASS zcl_ss_awd_movies DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    TYPES:
      movies_tt TYPE TABLE OF zss_awd_movies WITH EMPTY KEY.

    CLASS-METHODS:
      create
        IMPORTING name          TYPE string
                  year          TYPE i
        RETURNING VALUE(result) TYPE sy-subrc,

      read_all
        RETURNING VALUE(result) TYPE movies_tt,

      itab_to_html_tab
        IMPORTING movies        TYPE zcl_ss_awd_movies=>movies_tt
        RETURNING VALUE(result) TYPE string.
ENDCLASS.

CLASS zcl_ss_awd_movies IMPLEMENTATION.
  METHOD create.
    DATA(movie) = VALUE zss_awd_movies( name = name year_ = year ).

    INSERT zss_awd_movies FROM @movie.

    result = sy-subrc.
  ENDMETHOD.


  METHOD read_all.
    SELECT *
      FROM zss_awd_movies
      INTO TABLE @result.
  ENDMETHOD.


  METHOD itab_to_html_tab.
    result = |<ul>|.

    LOOP AT movies REFERENCE INTO DATA(movie).
      result &&= |<li>{ movie->name } ({ movie->year_ })</li>|.
    ENDLOOP.

    result &&= |</ul>|.
  ENDMETHOD.
ENDCLASS.

(again, in production you probably should not have one class with both DB operations and business logic – but we are going for simplicity)

Now, let’s start to implement some web app-related logic in our handler class! First, let’s get the relevant HTMLs:

  METHOD home.
    response->set_text( zcl_ss_awd_helper=>get_page_html( 'index' ) ).
  ENDMETHOD.

  METHOD browse.
    response->set_text( zcl_ss_awd_helper=>get_page_html( 'browse' ) ).
  ENDMETHOD.

  METHOD add.
    response->set_text( zcl_ss_awd_helper=>get_page_html( 'add' ) ).
  ENDMETHOD.

  METHOD not_found_404.
    response->set_text( zcl_ss_awd_helper=>get_page_html( '404' ) ).
  ENDMETHOD.

With this, we have a static, but at least good looking web app! Here’s what everything up till now looks like:


Towards the end of the video, we see the “/add” route, which looks as follows in HTML code:

<div class="row">
  <div class="main">
    <h2>Extend our movie catalog:</h2>
    <form action="/sap/bc/http/sap/zss_awd_demo/add" method="get">
  <input type="hidden" id="action" name="execute" value="X">
      <label for="name">Movie name:</label><br>
      <input type="text" id="name" name="name"><br><br>
      <label for="year">Movie year:</label><br>
      <input type="text" id="year" name="year"><br><br>
  <input type="submit" value="Create">
</form>
  </div>
</div>

Now, we will implement our first logic handling in a route! We add a new HTML template, executed_add.html which will show up in the “/add” route after the form is submitted. Here’s what the server-side logic looks like:

  METHOD add.
    IF request->get_form_field( 'execute' ) = abap_true.
      response->set_text( zcl_ss_awd_helper=>get_page_html( 'executed_add' ) ).
    ELSE.
      response->set_text( zcl_ss_awd_helper=>get_page_html( 'add' ) ).
    ENDIF.
  ENDMETHOD.

This works because of the hidden input called execute which has as value ‘X’ ( = abap_true). The executed_add.html template has two variables – response_title and response_body:

<div class="row">
  <div class="main">
    <h2>$response_title</h2>
    $response_body
  </div>
</div>

And from now on, when we press the Create button, we are still in the “/add” route, but, under the hood, we get served the executed_add.html template:

Web Development with (Cloud) ABAP, SAP ABAP Certification, SAP ABAP Career, SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning
executed_add.html

Of course, we are still not doing any rendering at this point. So let’s get there! To do this, we will use our Helper class. First, we introduce a public type in the class:

    TYPES:
      BEGIN OF var_and_content_s,
        variable TYPE string,
        content  TYPE string,
      END OF var_and_content_s,

      var_and_content_tt TYPE TABLE OF var_and_content_s WITH EMPTY KEY.

This would be used by passing the variable name (for example, response_title) and with what content it should be replaced (for example, “Successfully added movie!”). The actual rendering will happen in a method, also in the Helper, called render_html with the following signature:

      render_html
        IMPORTING html                TYPE string
                  var_and_content_tab TYPE var_and_content_tt
        RETURNING VALUE(result)       TYPE string

And the following implementation:

  METHOD render_html.
    result = html.

    LOOP AT var_and_content_tab REFERENCE INTO DATA(var_and_content).
      result = replace( val = result sub = |${ var_and_content->variable }| with = var_and_content->content ).
    ENDLOOP.
  ENDMETHOD.

Let’s test it in our “/add” route:

  METHOD add.
    IF request->get_form_field( 'execute' ) = abap_true.
      DATA(html) = zcl_ss_awd_helper=>get_page_html( 'executed_add' ).

      html = zcl_ss_awd_helper=>render_html( html = html var_and_content_tab = VALUE #(
        ( variable = 'response_title' content = 'Testing rendering... (response_title)' )
        ( variable = 'response_body' content = 'Testing rendering... (response_content)' )  ) ).

      response->set_text( html ).
    ELSE.
      response->set_text( zcl_ss_awd_helper=>get_page_html( 'add' ) ).
    ENDIF.
  ENDMETHOD.

This time, after pressing the create button, we see the rendered HTML!

Web Development with (Cloud) ABAP, SAP ABAP Certification, SAP ABAP Career, SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning
Testing rendering (successfully!)

Of course, at this point, we are not really creating anything. But we are almost there! We already have the class (Movies) to handle this, with its create method. All we have to do is just call it, evaluate the subrc it returned, and set the response based on this! There’s two options: first, all’s good – the movie was created (subrc 0) and second, the movie already exists (subrc 4). Here’s what we do:

  METHOD add.
    IF request->get_form_field( 'execute' ) = abap_true.
      " add the movie
      DATA(subrc) = zcl_ss_awd_movies=>create( name = request->get_form_field( 'name' )
        year = CONV #( request->get_form_field( 'year' ) ) ).

      " prepare response based on subrc
      DATA(response_title) = SWITCH #( subrc WHEN 0 THEN |Successfully added movie { request->get_form_field( 'name' ) }|
        ELSE |Sorry, we could not add the movie...| ).

      DATA(response_content) = SWITCH #( subrc WHEN 0 THEN |Thank you for extending our movie database!|
        ELSE |...because we already have it in our database!| ).

      " return the dynamically rendered HTML response
      DATA(html) = zcl_ss_awd_helper=>get_page_html( 'executed_add' ).

      html = zcl_ss_awd_helper=>render_html( html = html var_and_content_tab = VALUE #(
        ( variable = 'response_title' content = response_title )
        ( variable = 'response_body'  content = response_content ) ) ).

      response->set_text( html ).
    ELSE.
      response->set_text( zcl_ss_awd_helper=>get_page_html( 'add' ) ).
    ENDIF.
  ENDMETHOD.

Now, let’s test it! We can add Titanic:

Web Development with (Cloud) ABAP, SAP ABAP Certification, SAP ABAP Career, SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning
Adding Titanic

After doing this, we get a success message:

Web Development with (Cloud) ABAP, SAP ABAP Certification, SAP ABAP Career, SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning
Successfully added Titanic!

But did we actually succeed? Let’s use the SQL console to find out…

Web Development with (Cloud) ABAP, SAP ABAP Certification, SAP ABAP Career, SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning
Output for selecting everything from the Movies table

It definitely seems so! In fact, if we now try to add Titanic again we will not succeed:

Web Development with (Cloud) ABAP, SAP ABAP Certification, SAP ABAP Career, SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning
We already have Titanic!

This means we are almost finished with our application! Let’s just add the functionality to list the movies in the “/browse” route:

  METHOD browse.
    DATA(movies) = zcl_ss_awd_movies=>read_all(  ).

    DATA(html) = zcl_ss_awd_helper=>render_html(
     html = zcl_ss_awd_helper=>get_page_html( 'browse' ) var_and_content_tab = VALUE #(
     ( variable = 'movies' content = zcl_ss_awd_movies=>itab_to_html_tab( movies ) ) ) ).

    response->set_text( html ).
  ENDMETHOD.

Which results in the following:

Web Development with (Cloud) ABAP, SAP ABAP Certification, SAP ABAP Career, SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning
Our movie catalog

To reward ourselves for making it till here, here’s a video of this, albeit simple, fully fledged web app:


Limitations


One important limitation I found, and is not discussed above, is that if the HTML form is sending the data with a POST method, we get a 403 error (forbidden). This is why I went with the implementation using a GET to the same route, and an execute query parameter. I haven’t really investigated much why this happens. A prima vista, I think this is probably caused by the lack of CSRF token, which is required for executing modifying HTTP verbs (such as POST or DELETE). It might be possible to overcome this with a JavaScript modifying the sent request. It might also not be. The issue itself might have a completely different cause, instead of a CSRF token.

No comments:

Post a Comment