SSIFrame extension tutorial

# Jigsaw
SSI extension tutorial

Jigsaw Home / Documentation Overview / Tutorials

The SSIFrame (org.w3c.jigsaw.ssi.SSIFrame) is a Jigsaw frame that provides a flexible way of generating part of the content of a document from individual pieces. This may sound too general, and that's because there is little constraint on the way the constituent pieces are generated. For example, one use of the SSIFrame is the traditional one: the content of any resource can be embedded within any document exported by the SSIFrame, by using the `include` command from the default command registry. Some other of the default commands allow you to include the size of the document, the time of day, the hit count, and other general data.

One of the goals of this tutorial is to show that the SSIFrame is useful beyond its traditional use, as a powerful way of creating documents with a dynamically generated content. It is assumed that you are familiar with the administration of Jigsaw in general.

# Commands and registries

The SSIFrame will scan through the text of the file looking for a special kind of HTML comment. If it finds something of the form `<!--#command par_1=val_1 par_2=val_2 ... par_n=val_n -->`, it will interpret it as a command. `par_1 ... par_n` are the names of the parameters, and `val_1 ... val_n` are their values. The values can optionaly be enclosed in single or double quotes; otherwise they are delimited by ASCII white space. For example, the string `<!--#include virtual="doc.html"-->` denotes a call to a command called "include", with one parameter called "virtual" that has a value of "doc.html".

Upon finding a command, the SSIFrame will look it up in an object called the command registry. The command registry returns the command that is registered by that name. Then, it will call the command's `execute` method with the specified parameters, and with other contextual data.

Command registries are objects of class org.w3c.jigsaw.ssi.commands.CommandRegistry. Since this is an abstract class, a concrete implementation of one must be available for SSIFrame to work. One such implementation is supplied with the distribution: it is org.w3c.jigsaw.ssi.commands.DefaultCommandRegistry, which includes the bread-and-butter SSI commands. Commands are implementations of the org.w3c.jigsaw.ssi.commands.Command interface or org.w3c.jigsaw.ssi.commands.ControlCommand. The SSIFrame declares a `registryClass` attribute, which is set to the particular command registry to use in parsing a given document.

Therefore, the way to extend the SSIFrame is to create (either from scratch or by subclassing an existing one) a command registry that knows about the new commands that are being added. A good way to become familiar with these classes is too look at the code for DefaultCommandRegistry and its superclass, BasicCommandRegistry, and at the code for the default commands (in rough order of complexity): SampleCommand, CountCommand, ConfigCommand, FSizeCommand, FLastModCommand, EchoCommand, IncludeCommand, jdbcCommand, CounterCommand, ServletCommand.

SSIFrame allows you to create control commands like loop and test. These commands implements the org.w3c.jigsaw.ssi.commands.ControlCommand interface. Here is the code of the default control commands : IfCommand, ElseCommand, EndifCommand, LoopCommand, ExitloopCommand, EndloopCommand. The org.w3c.jigsaw.ssi.commands.Command interface has been modified, a new method was added (getValue). This method is used by some control commands (if) to get some value relative to the command.

Let's have a look of what can be done with control commands :

This shtml page display the content of the users database.

    <html>
      <head>
        <title>Database SSI</title>
      </head>

      <body>
          <h1>Database SSI</h1>
          <p>This Server Side Include extension allows you to query a database,
             to make some loop and some tests.
            (which I am doing right now)

    <!--#jdbc select="SELECT * FROM users" name="result" driver="COM.imaginary.sql.msql.MsqlDriver" url="jdbc:msql://www43.inria.fr:4333/users" -->

            <p>The query has run, here is all the results:<p>
            <table border=2>
            <tr><td><b>Name</td><td><b>Login</td>
            <td><b>Email</td><td><b>Age</td></tr>
    <!--#loop name="loop1" -->
          <!--#jdbc name="result" next="true" -->

          <!--#if name="if1" command="jdbc" var="result" equals="empty" -->
                    <!--#exitloop name="loop1" -->
          <!--#endif name="if1" -->

          <!-- the three lines above can be changed in : -->

          <!--#exitloop name="loop1" command="jdbc" var="result" equals="empty" -->

            <tr><td>
            <!--#jdbc name="result" column="1" -->
            </td><td>
            <!--#jdbc name="result" column="2" -->
            </td><td>
            <!--#jdbc name="result" column="3" -->
            </td><td>
            <!--#jdbc name="result" column="4" -->
            </td></tr>
    <!--#endloop name="loop1" -->
            </table>
          <hr>
      </body>
    </html>

# IfCommand : the source code

This command implements the classic if statement. This command can only be used with EndifCommand and (optionnaly) with ElseCommand.

    package org.w3c.jigsaw.ssi.commands;

    import java.util.*;

    import org.w3c.jigsaw.http.* ;
    import org.w3c.www.http.* ;
    import org.w3c.tools.resources.* ;
    import org.w3c.util.* ;
    import org.w3c.jigsaw.ssi.*;

    /**
     * Implementation of the SSI if command.
     * @author Benoit Mahe :bmahe@sophia.inria.fr
     */

    public class IfCommand implements ControlCommand {
      private final static String  NAME  = "if";
      private final static boolean debug = true;

      // The parameters accepted by the if command
      private static final String keys[] = {
        "name",
        "command",
        "var",
        "equals"
      };

      // Used to store the position of each if command
      protected static Hashtable ifstore = null;

      static {
         ifstore = new Hashtable(23);
      }

      /**
       * Returns the (String) value of the given variable.
       * @return a String instance.
       */
      public String getValue(Dictionary variables, String var) {
        return null;
      }

      protected static int getPosition(String name)
        throws ControlCommandException
      {
        Integer pos = (Integer)ifstore.get(name);
        if (pos == null)
          throw new ControlCommandException(NAME,"Position unknown.");
         else return pos.intValue();
      }

      /**
       * register the command position in the structure
       * witch store the SSIFrame.
       */
      public void setPosition(SSIFrame ssiframe,
                              Request request,
                              CommandRegistry registry,
                              ArrayDictionary parameters,
                              Dictionary variables,
                              int position)
      {
        Object values[] = parameters.getMany(keys);
        String name     = (String) values[0];
        if (name != null)
          ifstore.put(ssiframe.getResource().getURLPath()+":"+name, new Integer(position));
      }

      /**
       * Executes this command. Might modify variables.
       * Must not modify the parameters.
       * It may handle conditional requests, except that if
       * it replies with a status of HTTP.NOT_MODIFIED, it must
       * still reply with a content (the same content that it would have
       * returned for an inconditional request).  This is because
       * further SSI commands down the line may decide thay they have
       * been modified, and then a content must be emitted by SSIFrame.
       * @param request the original HTTP request
       * @param parameters The parameters for this command
       * @param variables The global variables for the parse
       * @return a Reply with the output from the command */

      public Reply execute(SSIFrame ssiframe,
                           Request request,
                           ArrayDictionary parameters,
                           Dictionary variables)
      {
        // Empty reply
        return ssiframe.createCommandReply(request, HTTP.OK);
      }

      protected boolean check(CommandRegistry registry,
                              Request request,
                              ArrayDictionary parameters,
                              Dictionary variables)
      {
        Object values[] = parameters.getMany(keys);
        String name     = (String) values[0];
        String command  = (String) values[1];
        String var      = (String) values[2];
        String equals   = (String) values[3];

        if ((command == null) || (var == null) || (equals == null))
          return false;
        Command cmd = registry.lookupCommand(command);
        String value = cmd.getValue(variables,var);
        // here is the test
        return value.equals(equals);
      }

      /**
       * Give the next position in the structure witch
       * store the SSIFrame.
       */
      public int jumpTo(SSIFrame frame,
                        Request request,
                        CommandRegistry registry,
                        ArrayDictionary parameters,
                        Dictionary variables)
        throws ControlCommandException
      {
        Object values[] = parameters.getMany(keys);
        String name     = (String) values[0];
        if (name != null) {
          if (check(registry,parameters,variables))
            return getPosition(frame.getURLPath()+":"+name)+1;
          try {
            return (ElseCommand.getPosition(frame.getURLPath()+":"+name)+1);
          } catch (ControlCommandException ex) {
            return (EndifCommand.getPosition(frame.getURLPath()+":"+name)+1);
          }
        }
        throw new ControlCommandException(NAME,"name not initialized.");
      }

      /**
       * Returns the name of this command. (Case sensitivity is up to
       * the lookupCommand method in the command registry.)
       * @return the name of the command
       * @see org.w3c.jigsaw.ssi.commands.CommandRegistry#lookupCommand
       */
      public String getName() {
        return NAME;
      }

    }

With this in mind, let's implement a useful extension of SSIFrame.

# A server statistics page with SSIFrame

There is an existing Jigsaw resource (org.w3c.jigsaw.status.StatisticsFrame) that is used to display the internal statistics of the server. In what follows, we will mimic the functionality of this frame with an SSI command. There is an object that supplies all these statistics for us; its class is org.w3c.jigsaw.http.httpdStatistics and it can be obtained from the server. Our SSI command will query this object and emit the values. We'd like to be able to use it like this: <!--#stat data=<type> -->, where <type> specifies the particular statistic that is going to be inserted, and is one of:

- `serverLoad`
- `freeThreads`
- `idleThreads`
- `totalThreads`
- `hitCount`
- `meanReqTime`
- `maxReqTime`
- `maxReqURL`
- `minReqTime`
- `minReqUrl`
- `emittedBytes`

Each of them will correspond to one of the methods in httpdStatistics.

## Writing the `stat` command

This command can be written in a very straightforward manner. All we have to do is:

1. Obtain the httpdStatistics instance from the server.
2. Call in it the appropriate method, according to the data parameter.
3. Return a reply with this value as content.

This translates to the following java class, which will be called StatCommand:

Listing 1: The command class
    package org.w3c.jigsaw.tutorials ;

    import java.util.* ;

    import org.w3c.jigsaw.http.* ;
    import org.w3c.www.http.HTTP ;
    import org.w3c.jigsaw.ssi.* ;
    import org.w3c.util.* ;
    import org.w3c.jigsaw.ssi.commands.* ;
    public class StatCommand implements Command {
        private static final String NAME = "stat" ;

        public final String getName()
        {
            return NAME ;
        }

        // Unuseful here
        public String getValue(Dictionary variables, String variable) {
            return null;
        }

        public Reply execute(SSIFrame frame,
                              Request request,
                              ArrayDictionary parameters,
                              Dictionary variables)
        {
            // Obtain the statistics from the server
            httpdStatistics stats = frame.getServer().getStatistics() ;

            // Get the parameter specifying the kind of statistic to emit.
            String data = (String) parameters.get("data") ;

            // If the parameter is not supplied, do nothing
            if(data == null)
                return null ;

            // Otherwise, compare it against the possible different keywords
            // (Since there are no "pointers to methods", this is the simplest way it
            // can be written)
            long result = -1 ;
            String urlResult = null ;
            if(data.equalsIgnoreCase("serverload")) {
                result = stats.getServerLoad() ;
            } else if(data.equalsIgnoreCase("freethreads")) {
                result = stats.getFreeThreadCount() ;
            } else if(data.equalsIgnoreCase("idlethreads")) {
                result = stats.getIdleThreadCount() ;
            } else if(data.equalsIgnoreCase("totalthreads")) {
                result = stats.getTotalThreadCount() ;
            } else if(data.equalsIgnoreCase("hitcount")) {
                result = stats.getHitCount() ;
            } else if(data.equalsIgnoreCase("meanreqtime")) {
                result = stats.getMeanRequestTime() ;
            } else if(data.equalsIgnoreCase("maxreqtime")) {
                result = stats.getMaxRequestTime() ;
            } else if(data.equalsIgnoreCase("maxrequrl")) {
                urlResult = stats.getMaxRequestURL().toExternalForm() ;
            } else if(data.equalsIgnoreCase("minreqtime")) {
                result = stats.getMinRequestTime() ;
            } else if(data.equalsIgnoreCase("minrequrl")) {
                urlResult = stats.getMinRequestURL().toExternalForm() ;
            } else if(data.equalsIgnoreCase("emittedbytes")) {
                result = stats.getEmittedBytes() ;
            } else return null ;

            // Make a reply with the datum and return it
            Reply reply = frame.createCommandReply(request, HTTP.OK) ;
            reply.setContent( urlResult == null
                              ? Long.toString(result)
                              : urlResult ) ;
            return reply ;
        }
    }

The Command interface defines three methods. The `getName` method simply returns a String with the name of the command. The getValue method returns a value relative to the given parameter (Used by control commands).The `execute` method is the one that does the work. This method can be thought of as the `get` method in a frame: it takes, among other things, a Request object, and it produces a Reply object. The SSIFrame will insert the contents of the replies of each of the commands (partial replies) into the main, global, content, and it will also merge the relevant headers of the partial replies into the headers of the global reply. Besides taking a request, the `execute` method takes these arguments as well:

`org.w3c.jigsaw.ssi.SSIFrame frame`
  This is the SSIFrame that is executing the command.
`org.w3c.util.ArrayDictionary parameters`
  The parameters that the command is called with. An ArrayDictionary is a subclass of java.util.Dictionary. The parameters are stored as strings with the parameter names as keys.
`java.util.Dictionary variables`
  The current set of variables. A command may change its behavior according to the values of these variables, and it can also modify the variables. The meaning of the variables is almost completely command- and command registry-dependent. The DefaultCommandRegistry uses the variables to keep state across different command calls in the same document, such as the current date and time formats.

The `execute` method can also return `null`, which is interpreted as the absence of output. There are some subtle differences between the `execute` method and a regular frame `get` method. In particular, care must be taken if dealing with conditional requests. This example is simple enough that this is not a concern.

Now that the command itself is finished, we need to make it part of a command registry, so that it can be actually used in documents.

## Writing a command registry

Since we'd like to be able to use the "standard" SSI commands in adition to our brand-new `stat` command, it's not a bad idea to make our new registry a subclass DefaultCommandRegistry. The way to do this is very straightforward:

Listing 2: The command registry class
    package org.w3c.jigsaw.tutorials ;

    import org.w3c.jigsaw.ssi.* ;
    import org.w3c.jigsaw.ssi.commands.* ;
    public class MyCommandRegistry extends DefaultCommandRegistry {
        public MyCommandRegistry()
        {
            registerCommand(new StatCommand()) ;
        }
    }

The constructor simply calls the `registerCommand` method (defined in BasicCommandRegistry), with a new instance of the command that we're adding.

We're now ready to use this command in a future document.

## Using the new registry

One way of using the newly-created command registry is to change the `registryClass` attribute defined for the `.shtml` extension to "`org.w3c.jigsaw.tutorials.MyCommandRegistry`". After doing that, Jigsaw will use the new registry when indexing new files with the `.shtml` extension (or reindexing old files). Then we can create a file that makes use of the new command, and place it in a Jigsaw-accesible directory. For example, we could do this:

Listing 3: A possible use of the new command
    <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
    <html>
      <head>
        <meta http-equiv="Refresh" content="5">
        <title>Server Statistics</title>
      </head>
      <body>
          <ul>
            <li>hits: <!--#stat data=hitCount  -->
            <li>bytes: <!--#stat data=emittedBytes  -->
          </ul>
          <p>Request processing times:
          <table border>
            <tr>
              <th align="center"> min
              <th align="center"> avg
              <th align="center"> max
            </tr>
            <tr>
              <th align="center"> <!--#stat data=minReqTime  -->
              <th align="center"> <!--#stat data=meanReqTime  -->
              <th align="center"> <!--#stat data=maxReqTime  -->
            </tr>
          </table>
          <p>Thread counts:
          <table border>
            <tr>
              <th align="center"> free
              <th align="center"> idle
              <th align="center"> total
            </tr>
            <tr>
              <th align="center"> <!--#stat data=freeThreads  -->
              <th align="center"> <!--#stat data=idleThreads  -->
              <th align="center"> <!--#stat data=totalThreads  -->
          </table>
          <p>Current load: <!--#stat data=serverLoad  -->
      </body>
    </html>

The above document will produce exactly the same output that the Statistics frame would emit.

# What have we gained?

At this point we can compare two different approaches to generating HTML dynamically. The first one involves writing a new, specialized, frame. The approach illustrated in this tutorial consists of writing an SSI command and serving the document with the SSIFrame. Doing it this way has these advantages:

- The structure of the served document is not hard-coded. Instead, the Java code only expresses the minimal needed functionality (in this case, that of emitting server statistics), while the markup of the document can be modified without having to recompile.
- Different functionalities can be mixed in a more orthogonal way. That is, it is inefficient at best to create a resource that combines the function of two existing resources. By distilling the functionality into SSI commands, it is straightforward to make a registry that has all the needed commands.

One disadvantage of the SSI approach is the extra overhead incurred at serve-time of constructing the content from the pieces supplied by the commands. The SSIFrame tries to avoid this overhead as much as possible. The most important optimization in this respect is the fact that the parsing of the document (i.e., scanning the text for commands, and reading the parameters) is done only when the file is modified. Even then, each command needs to check its parameters, which does add to serve-time overhead.