The Locomotive is a "server program", which means that it listens on a TCP port for requests from clients. Once it starts running, it can handle requests forever, until you explicitly shut it down. You can run several Locomotive servers on your machine at once, to improve parallelization.
The Locomotive is not part of the web server, nor does it require the web server to be running. In order for the web server to communicate with a Locomotive, the web server executes a program which serves as a tunnel between the web server and the Locomotive. A few different "tunnel" programs are provided, including a CGI program written in C, an NSAPI shared object, and an Apache module. Each of these can be configured to pass information on to multiple Locomotives. The locolink "tunnel" chooses which Locomotive to connect to based on random selection, or on the system CPU load. The tunnel provides you the freedom to run as many Locomotives on your system as you would like, on as many separate machines as you like, so that your web site can scale gracefully as your web traffic increases.
Once the data reaches a Locomotive, all the information, including the URL, any HTML form data, and any Cookies from the browser, are all processed by the Locomotive. The Locomotive then passes the request to the particular 'Module'. Right now the Locomotive supports two different kinds of modules- Servlets and Handlers. Servlets are java classes which perform server side activities by implementing the Java Servlet API. Handlers are classes that perform actions through implementing the Handler Interface. The Locomotive will assign a Java thread to "run" an instance of a module in order to handle a particular web request. A Locomotive usually has many threads running different modules at the same time. You can set the minimum or maximum number of these threads per Locomotive in the loco.conf file. From now on, we'll speak of different kinds of modules as services. So, the Locomotive has two different services available to it- Handler and Servlet.
Depending on the type of web request, the module may communicate with the database, via the JDBC API. The module is also responsible for displaying the dynamic web pages. The Locomotive makes this easy by providing a simple language you can embed in your HTML pages to specify what and where your dynamic insertions should go. We call this embedded language 'Steam'. Services can read in Steam embedded templates and execute the various Steam commands in the template, and substitute values for Steam variables set in the template. The service can then flush the page evaluated page back to the web server as a web page. We'll talk more about the Steam language later on.
What Happens within the Locomotive, Step-by-Step
Here's a more detailed breakdown of what happens when the Locomotive handles a web request:
We'll see how each of these steps is implemented in three different examples of Handlers discussed later in this manual. We'll also go through a short demonstration of how to do all these things with Servlets.
As we talked about before, modules are Classes you write that implement a particular interface. The Locomotive decides which module type, or service, a request is looking for based on the URL path of the request. Specifically, it looks for the value of one of two config variables which are set in the loco.conf file- HANDLER_URI_TOKEN_PREFIX and SERVLET_URI_TOKEN_PREFIX. If it doesn't find either, then it reverts to the default service, which is set with the config variable SERVICE_DEFAULT. For more information on these variables, you can see the Administrators Guide.
Once it figures out what service the request should be routed to, it then attempts to match the next URL token to a table that maps URL tokens to modules. When you start to write code for the Locomotive, one of the first things you'll have to do is assign it a URL token. Once you've mapped your module to a token, when the Locomotive finds a request that has that URL token, it will know to send it to your module. Let's take a look at the URL of an example request to make this more clear. Say your Locomotive was running on the machine www.wowzers.com, and you configured your web server to activate the locolink tunnel when it found 'locolink' as the first url token. And, say that you wrote a module whose class was called com.nowhere.Bogomodule, and you associated it with a url token of 'bogo'. Then, in your web browser, you would type in the following URL:
http://www.wowzers.com/locolink/handler/bogo?some+more+url+tokens
When the Locomotive received your request, it would know to route your
request to your class, because it would first send the request to the
Handler Routing Table, and the handler routing table would look up
'bogo', find your class, and execute an instance of it to handle the
request. Now, we use the '?' and '+' separators to make it clear that
we're not actually requesting a path. If you prefer the more traditional
URL look, you could just as easily have typed:
http://www.wowzers.com/locolink/handler/bogo/some/more/url/tokens
and the Locomotive would have reacted the same. Which ever way you like.
In this manual we'll use URLs like the first one above- just keep in
mind that everything we say about them will work just as well for URL
tokens like the second, more traditional one.
// The entry on the next line(s) are implied, but may be overridden: // default org.locomotive.loco.DefaultHandler // unknown org.locomotive.loco.UnknownHandler // A great handler to start with; includes sample registration and login // template base org.locomotive.module.base.BaseHandler // These are our example modules; to use them, additional database tables // must be installed. See the installation guide. // graf org.locomotive.module.graffiti.GraffitiWallHandler // disp org.locomotive.module.DisplayPageHandler // These handlers below are currently supported on Oracle only. // btrack org.locomotive.module.btrack.BugTrackerHandler // conf org.locomotive.module.discuss.DiscussionHandler // pub org.locomotive.module.pub.PublisherHandler "/usr/local/loco/config/pub.properties" // new handlers go here // default org.locomotive.loco.DefaultHandler // unknown org.locomotive.loco.UnknownHandler // A great handler to start with; includes sample registration and login // template base org.locomotive.module.base.BaseHandler // These are our example modules; to use them, additional database tables // must be installed. See the installation guide. // graf org.locomotive.module.graffiti.GraffitiWallHandler // disp org.locomotive.module.DisplayPageHandler // These handlers below are currently supported on Oracle only. // btrack org.locomotive.module.btrack.BugTrackerHandler // conf org.locomotive.module.discuss.DiscussionHandler // pub org.locomotive.module.pub.PublisherHandler "/usr/local/loco/config/pub.properties" // new handlers go here(Notice that you can add comments in your hrt.conf file using //.)
Let's discuss some more detailed examples of the way the URL might get routed to a particular Handler. Let's say again that your site was called www.wowzers.com, and that your web server sent requests to the Locomotive when it found 'locolink' as the first URL token. Now, from a web browser, if you typed:
Then, once the locolink tunnel passed the information to your Locomotive, the Locomotive would look for a word appearing after the "locolink/" in the URL, as we said before. Now, once the Loco saw that there were no more URL tokens, it would send the request to the default service type, which, let's say, was set to HANDLER. So, the Locomotive would go to the Handler Routing Table and find out what class was mapped to the 'default' url token, since that's what the Locomotive invokes when now URL token is specified. Since we saw above that 'default' was mapped to 'org.locomotive.loco.DefaultHandler', the Locomotive would create a new DefaultHandler to handle the request, and you would see something like:http://www.wowzers.com/locolink/
HEY! This is the sample default handler
Now, let's take a look at another URL. If we mistakenly typed in a URL that referred to a Handler that did not exist, for example:
Then you might see an error message similar to the following:http://www.wowzers.com/locolink/blah
UNKNOWN HANDLER: The handler specified could not be foundIn this case, the Locomotive would have looked up 'unknown' in the Handler Routing Table, and the response you would have seen was the response the org.locomotive.loco.UnknownHandler module returns.
Now, let's say that we figured out what we were doing wrong, and we typed in the URL:
Then, the Locomotive would realize that we were trying to access the BaseHandler, because it would map the 'base' to the BaseHandler class. In this case, the BaseHandler sees that the first word after "base?" is "login", and so it will display an HTML form allowing you to log into the system.http://www.wowzers.com/locolink/base?login
Now that you understand how Locomotives decide which Handler to run, let's talk about how to build a module. We'll choose to talk about Handlers because there's already ample resources on the basics of Servlets. Even if your more interested in Servlets, you should know that everything that's available to Handler is also available to Servlets which run on the Locomotive. We'll talk about how to access Locomotive specific resources A little later.
There are several alternative ways for you to implement your own Handlers. If the functionality you need to implement is small, then you can probably fit all the code into one Handler class. However, your functionality may be too complex, or there may be too many different actions to fit into one class. One way to manage this complexity is to create one central Handler for an entire web site, which uses the URL to decide what action to take, and then delegates to helper objects. This is how Leverage's Professional Services Group implemented the Purple Moon web site, where we used helper classes called "Requests" to handle different groups of features. The advantage here is that the central Handler can handle common functions that need to be called every time a Purple Moon page is accessed, no matter what the particular action is.
Another way to do the same thing is to implement a generic Handler class which performs all the common initialization functions. Then, for each specific action, you implement a subclass of that Handler which does specific things. This architecture is closer to the conventional model of object-oriented inheritance, so if you're more comfortable with that, you can do it this way.
Yet another way to do it is to allow Handlers to call each other in a "daisy chain". This is similar to the way Unix commands link with each other using pipes. The advantage of this method is that you get a lot more power and functionality by connecting Handlers together, but the disadvantage is that each Handler requires memory and execution time to create and destroy. Also, you have to make sure that the arguments passed into individual Handlers are correct -- the arguments must look exactly the same as if the Handler had been invoked directly by the Locomotive.
You can decide which of these styles is best for you, or use another style. In the examples that we use, we chose to use the first style -- having one central Handler pass different requests to different subclasses of one GenericHandler class. We chose this architecture because we wanted you to have the flexibility to use one central Handler to call many different subclasses of GenericHandler, while, on the other hand, if you don't need many different subclasses, you can simply create one GenericHandler subclass inside your own Handler, which is just a small overhead. So, we're giving you the flexibility to choose your own architecture.
The GenericHandler class has many methods which simplify the amount of Locomotive API you need to learn. As you become a more advanced Locomotive developer, you can look at the source code inside the GenericHandler class to see examples of how you can make Locomotive API calls at a more advanced level.
The GenericHandler class has many component variables which you
can access. Here are the ones you will probably use most often.
this is a "super data structure" which contains all the information that the Locomotive passes into the Handler, and that the Handler passes into any GenericHandler. The GenericHandler then puts the information within hd into its separate local variables. Usually, you don't need to use hd, unless you're doing something advanced. |
|
String url |
contains the last part of the URL, starting from the Handler identifier word to the end. ex: "base?login+submit". |
String[] url_token |
the "url" field broken down into an array of words, using the '?' and the '+' symbols as separators. ex: the String array: { "base", "login", "submit" }. |
java.SQL.Connection conn |
a JDBC Connection you can use to access the database you're using to store the Locomotive user and session information. |
org.locomotive.loco.FormData form_data |
A utility object that allows you to access and error-check the data in an HTML form that a user submits. |
If a user has logged in with a password, then this User object contains their personal information; if the user is anonymous, then this contains generic information. |
|
org.locomotive.loco.Session session |
If the user's Web browser accepts cookies, then he or she can start a new "session", or a continuous series of authenticated web page views, grouped together by a single session ID that is associated with this user. |
Hashtable steam_vars |
Stores the names and values of all Steam variables to be inserted into HTML templates. Usually, you will be storing into steam_vars rather than accessing it. |
long bstamp_id |
The bstamp_id is a unique number associated with each browser that generates a request to the Locomotive. It is sent to and from the Locomotive via a cookie. While the other means of identifying a user or sessions are set to expire after a given amount of inactivity, the bstamp is permanent, allowing you to record, if you like, how many session a user who used that browser generated, how many different users used a particular browser, etc. |
One of the most powerful of the helper classes is the FormData class, which allows you to access the data from a HTML form submission. FormData's methods do error-checking and automatically generate error messages, which you can use to tell the user how to enter data correctly in the form. You can also override these error messages. FormData has methods for reading numbers, series of digits, alphanumeric words, dates, and email addresses, from form fields. We'll cover them in the second code example.
Now that you've learned about the basic structures a Handler has to
work with, you may want to know how to retrieve and utilize the same
Structures. The Servlet API, as of version 2.1, allows the Servlet
Engine to pass a set of generic objects to a servlet via the
ServletRequest Object. The servlet can then retrieve these objects using
the request.getAttribute()
method, which takes a String key
and returns the object you're looking for. Let's take a look at the
various objects available to Servlet's via this call:
Key | Object Returned |
---|---|
locomotive.url_tokens |
Just as above, a String array that contains the part of the URL that appears after the service identifier token, broken down into an array of words. ex: the URL base?login+submit would be translated to the String array: { "base", "login", "submit" }. |
locomotive.connection |
a JDBC Connection to the Database the Locomotive is using to store user and session information. |
locomotive.form_data |
A FormData Object that makes it easier to handle form submissions. It provides an easy way to type check and report errors on the form content This won't be available unless a POST was sent by the client. |
locomotive.steam_vars |
This is a Hashtable that stores the names and values of all Steam variables to be inserted into HTML templates. Usually, you will be storing into steam_vars rather than accessing it. This is here to provide a standardized place to access Steam Variables, which becomes an issue when other Servlets may have placed values here before your particular Servlet was activated, as in the case of Servlet Includes and Servlet Forwarding. |
locomotive.bstamp_id |
The bstamp_id is a unique long number associated with each browser that generates a request to the Locomotive. It is sent to and from the Locomotive via a cookie. While the other means of identifying a user or sessions are set to expire after a given amount of inactivity, the bstamp is permanent, allowing you to record, if you like, how many session a user who used that browser generated, how many different users used a particular browser, etc. |
In addition to those objects, the Locomotive also supplies a
User object to
each request. Because a user is associated with a session, the User is
stored in the Session Objects table, which can be accessed via the
session.getObject("locomotive.user")
method. If a
user has not logged in, the the User object contains only generic
information. To associate a new User to a session, all you have to do is
replace the old session using the
session.put("locomotive.user")
command, and the new
User will automatically be associated with the User in the Database.
If you're writing a Servlet from scratch to be used with the Locomotive, you may want to have your Servlet subclass the LocoServlet class. This class provides most of the same functionality of the GenericHandler class, for Servlets. All the fields and methods discussed above in the GenericHandler section are provided in the LocoServlet class.
One interesting thing to note about LocoServletRequest is that it implements SingleThreadModel. Servlets, unlike Handlers, are instantiated once when the server starts up, as opposed to on every request. While that may make it easier to store persistent information in instance fields, it also means you have to make your Servlet thread safe, which is sometimes more trouble than it is worth. To make developing Servlets easier, the Servlet API provides the SingleThreadModel interface. When a Servlet implements this, then the ServletManager is not allowed to let more than one request access an instantiation of your Servlet at once. The Locomotive accomplishes this by creating a pool of instances for that Servlet, and handing instances from the pool off to each request. This means that your Servlet will perform just as well as a SingleThreadModel as it would if it was a normal Servlet. Because LocoServlets implement SingleThreadModel, they can safely set the same instance fields as the GenericHandler, and access those fields during each request. We'll show how to subclass a LocoServlet in Chapter 3, when we create a LoginServlet to show how to log in Users with the Locomotive.
SUMMARYIn this chapter we got acquainted with the way the Locomotive maps URLs to modules, and we introduced the basic building blocks of the Locomotive environment. In the next chapter we go through a basic Handler example to show how to use these objects to handle a simple request. If you're interested in Servlets, you may want to skip directly to Chapter 3, where we go into detail with Session handling and using the Steam template language.