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:
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:
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!
Hello, world!
Here’s how the full code looks at this point in time:
CLASS zcl_ss_awd_demo DEFINITION PUBLIC CREATE PUBLIC.
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:
Hello, Bobby!
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.
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:
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!
Home page
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:
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:
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.
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:
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.
This time, after pressing the create button, we see the rendered HTML!
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!| ).
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