Using the Ada Web Server (AWS), part 2

In the Using the Ada Web Server (AWS), part 1 article I showed you how to setup a simple Hello world! server powered by the awesomeness that is the Ada Web Server (AWS) project. In this second part I will show you how to utilize the Templates_Parser module to build your HTML and I’ll also give a very short example on how to serve a flat file to visitors.

If you haven’t already read part 1, I strongly suggest doing so before proceeding with this article, as we will be building upon the code from part 1 in the following. I will not waste time on how to get the server going, or how to setup a dispatcher. We will jump straight into the template fray.

But first things first:

$ git clone git://github.com/ThomasLocke/AWS_Tutorial_2.git

The instructions on how to compile and run the program are the same as for part 1 of the article.

So what exactly is the point of using templates to generate your content, instead of just building the HTML directly in the Ada code? Well there are several good reasons for using templates:

  • With templates you don’t have to re-compile due to changes in the HTML.
  • With templates you get a very strong separation between logic and view.
  • With templates you can easily localize content by simply loading a different template file based on user preferences/IP address/whatever.
  • Your HTML people won’t need to learn a single line of Ada. They can stick to what they are good at.

There are probably more good reasons, but the ones mentioned above should be more than enough to convince you that using a template system is a good idea.

Lets take a peek at a very simple template file:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Not found</title>
  </head>
  <body>
    <h1>Not found</h1>
    <p>Resource @_RESOURCE_@ not found</p>
  </body>
</html>

As you can see it looks very much like normal HTML except for the special @_RESOURCE_@ tag (third line from below), which is what turns this otherwise plain looking HTML snippet into a proper template: @_RESOURCE_@ is a template tag that is replaced by some value defined in the Ada code. As you might’ve guessed this template is used to generate the “404 not found” content, so lets see how the Ada code looks now that we’ve moved the HTML into a template. This is our new Generate_Content function from the src/not_found.adb file:

with AWS.Messages;
with AWS.MIME;
with AWS.Response;
with AWS.Status;
with AWS.Templates;

package body Not_Found is

   --  Stuff...

   ------------------------
   --  Generate_Content  --
   ------------------------

   function Generate_Content
     (Request : in AWS.Status.Data)
      return AWS.Response.Data
   is
      use AWS.Templates;

      Resource : constant String := AWS.Status.URI (Request);

      Translations : Translate_Set;
   begin
      Insert (Translations, Assoc ("RESOURCE", Resource));

      return AWS.Response.Build
        (Content_Type  => AWS.MIME.Text_HTML,
         Message_Body  => Parse ("templates/not_found.tmpl", Translations),
         Status_Code   => AWS.Messages.S404);
   end Generate_Content;

end Not_Found;

If you compare it to the src/not_found.adb file from part 1 you’ll notice that the changes really aren’t that huge, but lets start at the top: At line 5 we with AWS.Templates; to bring in the templates parser module. With that we now have all the templating power at out fingertips.

The next change happens in the declarative part of Generate_Content where we now have a Use AWS.Templates; line and a Translations : Translate_Set; line. A Translate_Set is basically a dictionary to which you add your tags and their associated values, and to see how that is done we go down two lines to the Insert (Translations, Assoc ("RESOURCE", Resource)); line. Starting from the inside we associate the value of the Resource string with the template tag RESOURCE using the Assoc procedure and then we add the association to Translations with the Insert procedure.

Note that if you insert an association that already exists, then the existing association is overwritten by the new association.

In order to use the Translations dictionary we’ve created, we must call the AWS.Templates.Parse function:

function Parse
  (Filename          : String;
   Translations      : Translate_Set;
   Cached            : Boolean               := False;
   Keep_Unknown_Tags : Boolean               := False;
   Lazy_Tag          : Dyn.Lazy_Tag_Access   := Dyn.Null_Lazy_Tag;
   Cursor_Tag        : Dyn.Cursor_Tag_Access := Dyn.Null_Cursor_Tag)
   return String;

Only Filename and Translations are required, so that is exactly what we’re going to give Parse:

return AWS.Response.Build
  (Content_Type  => AWS.MIME.Text_HTML,
   Message_Body  => Parse ("templates/not_found.tmpl", Translations),
   Status_Code   => AWS.Messages.S404);

Parse then goes to work, matching all the added associations to the tags found in the template file, which in our case means replacing @_RESOURCE_@ with the value of RESOURCE.

And that is all. The 404 not found content is now fully templated.

Next lets take a peek at Generate_Content from src/hello_world.adb where we use a few more features from the AWS.Templates module:

with AWS.Messages;
with AWS.Templates;

package body Hello_World is

   --  Stuff...

   ------------------------
   --  Generate_Content  --
   ------------------------

   function Generate_Content
     (Request : in AWS.Status.Data)
      return AWS.Response.Data
   is
      use AWS.Templates;

      type Natural_List is array (0 .. 9) of Natural;
      Fibs : Natural_List;

      Browser : constant String := AWS.Status.User_Agent (Request);

      Fibonacci    : Vector_Tag;
      Position     : Vector_Tag;
      Translations : Translate_Set;
   begin
      Insert (Translations, Assoc ("BROWSER", Browser));

      for i in Fibs'Range loop
         case i is
            when 0 => Fibs (i) := 0;
            when 1 => Fibs (i) := 1;
            when others => Fibs (i) := Fibs (i - 1) + Fibs (i - 2);
         end case;

         Append (Position, i);
         Append (Fibonacci, Fibs (i));
      end loop;

      Insert (Translations, Assoc ("POSITION", Position));
      Insert (Translations, Assoc ("FIBONACCI", Fibonacci));

      return AWS.Response.Build
        (Content_Type  => AWS.MIME.Text_HTML,
         Message_Body  => Parse (Filename     => "templates/hello_world.tmpl",
                                    Translations => Translations,
                                    Cached       => True),
         Status_Code   => AWS.Messages.S200);
   end Generate_Content;

end Hello_World;

Let me start by saying that my Fibonacci implementation probably isn’t fast nor elegant, but it works and it doesn’t clutter the example with a whole bunch of code, and really the interesting part here is not how the Fibonacci sequence is generated but how it is added to the Translations Translate_Set. For this we have the Vector_Tag, which in all its simplicity allows us to build lists of values. As you can see we have two such vector tags: Fibonacci and Position. These two are populated by the Append calls in the for loop:

for i in Fibs'Range loop
   case i is
      when 0 => Fibs (i) := 0;
      when 1 => Fibs (i) := 1;
      when others => Fibs (i) := Fibs (i - 1) + Fibs (i - 2);
   end case;

   Append (Position, i);
   Append (Fibonacci, Fibs (i));
end loop;

Append add the values of i and Fibs (i) to Position and Fibonacci respectively. In case you're wondering about what kinds of data you can append to a vector, here's the specification of all the Append procedures:

procedure Append (T : in out Tag; Value : String);
procedure Append (T : in out Tag; Value : Character);
procedure Append (T : in out Tag; Value : Boolean);
procedure Append (T : in out Tag; Value : Unbounded_String);
procedure Append (T : in out Tag; Value : Integer);
procedure Append (T : in out Tag; Value : Tag);

Note the last one: Yes, you can append a vector tag to a vector tag, making it possible to build multi-dimensional arrays.

Adding our vector tags to Translations is still done using the Insert procedure, so nothing new there.

Finally we generate the content with Parse, where it is worth noting that we've now added the Cached => True parameter. What this does is allow the AWS.Templates module to cache the template itself. If you do this the server no longer read and parse the template file on every hit. The downside is that if you make changes to the template file, you will have to restart the server for the changes to be registered.

Now lets see how we deal with vector tags and do some other tricks in the template file:

@@--
@@--  First we define a macro.
@@--
@@MACRO(F)@@
<span style="font-style: italic;">F</span><span style="font-size: 60%;">@_$1_@</span>
@@END_MACRO@@
@@--
@@--  And then comes the actual HTML5 document
@@--
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Hello world!</title>
  </head>
  <body>
    <h1>Hello world!</h1>
    <p>Browser used : @_BROWSER_@</p>
    <h2>Fibonacci's from 0 to 9</h2>
    <table>
      @@TABLE@@
        <tr>
          <td>@_F(@_POSITION_@)_@</td>
          <td>@_FIBONACCI_@</td>
        </tr>
      @@END_TABLE@@
    </table>
    <p>Now is @_NOW_@</p>
    <p>Now reversed is @_REVERSE:NOW_@</p>
    <p>Click <a href="/helloworld.tmpl">here</a> to see the hello_world.tmpl file.</p>
  </body>
</html>

Lines starting with @@-- are comments.

At line three we define a macro with the name F. When you call macros you can give them parameters which are then referenced in the macro as @_$n_@, where n corresponds to the N.th. parameter passed to the macro.

Moving on we add the visitors browser to the HTML using the @_BROWSER_@, and just below that we build the Fibonacci table, and here we encounter the @@TABLE@@ tag. Code between the @@TABLE@@ and @@END_TABLE@@ tags are repeated as many times as there are values in the POSITION and FIBONACCI vectors. @@TABLE@@ acts very much like an implicit iterator. Much fun can be had with the @@TABLE@@ tag - this example is the very simplest way to utilize it. Check the manual for more extensive examples.

There's a few more tricks in this template worth mentioning. The templates parser module sports a bunch of constants and filters, one of these being @_NOW_@ which is replaced with a time stamp in the format "YYYY-MM-DD HH:MM:SS". At the next line we reverse the contents of @_NOW_@ using the @_REVERSE:VAR_@ filter. There's a whole bunch of filters available and multiple filters can be applied to tags for pure awesomeness. As an added bonus you can even create your own filters.

And that was all I had about the templates parser module, but before I end this article I'd like to direct your attention to this new dispatcher in src/handlers.adb:

Dispatcher.Register (URI    => "/helloworld.tmpl",
                     Action => Hello_World.Hello_World_Template'Access);

The goal of this dispatcher is to return the contents of the exe/templates/hello_world.tmpl file to the user as text/plain. The Hello_World.Hello_World_Template function takes care of that:

function Hello_World_Template
  (Request : in AWS.Status.Data)
   return AWS.Response.Data
is (AWS.Response.File (AWS.MIME.Text_Plain,
                       "templates/hello_world.tmpl"));

That right there is an Ada 2012 expression function. Since all this function does is call AWS.Response.File we don't really need a body. Expression functions provides a shorthand to declare a function whose body consists of a single return statement. That is IMHO one very nice feature of Ada 2012.

And with that final piece of AWS.Response.File magic I will close this article. Stay tuned for part 3, where I plan on showing you a bit about how to handle HTTP request parameters using the Ada Web Server.