Editor's note: IBM® WebSphere® sMash and IBM WebSphere sMash Developer Edition are based on the highly acclaimed Project Zero incubator project. Project Zero is the development community for WebSphere sMash and will continue to offer developers a cost-free platform for developing applications with the latest builds, the latest features, and the support of the community.
This article assumes that you have downloaded Project Zero M2 and either completed the introductory tutorial or written a simple application yourself. You should also read the predecessor to this article, "Add Ruby scripting to your Project Zero applications" and understand the concepts behind server-side template languages like JSP, PHP, and RHTML.
In the predecessor to this article, "Add Ruby scripting to your Project Zero applications," you learned how Project Zero could be augmented to provide support for the Ruby scripting language, but our solution did not cover all of the ways in which Ruby on Rails developers use Ruby to create applications. The Rails framework includes a way to mix Ruby code with HTML templates, an idea that is similar to JSP and PHP. These templates, called RHTML files, are very useful for creating dynamic user interfaces, and prevent the programmer from having to use print statements to create HTML (as with J2EE servlets). Zero already allows users to create templates with Groovy and PHP (.gt and .php files, respectively), so why shouldn't the same be true of Ruby?
Strategy for creating an RHTML processor
When deciding how to implement an RHTML processor for Zero, our main goal is to avoid writing an RHTML parser and interpreter from scratch. Ruby templating is a nice feature, but not so nice that it's worth dragging in a large set libraries and writing compiler code that will be fairly bug-ridden until a large number of users have dedicated time to testing it. With Ruby scripts, we were able to delegate all of the script execution work to JRuby; it would be nice to have JRuby handle the code embedded in our RHTML files in a similar manner so that we didn't have to write our own specialized Ruby interpreter.
The trouble, of course, is that JRuby cannot execute RHTML files, only Ruby scripts that have valid Ruby syntax. How can we make it so that the Ruby code embedded in the RHTML file is executed by JRuby while the rest of the content (plain HTML) is sent to the client without modification? And if we do manage to extract the Ruby code and pass it off to JRuby, how do we integrate the output of the code with the static HTML? When you consider the fact that the various snippets of Ruby code spread across the file are all meant to be executed in the same scope (with access to each other's variables), it becomes clear that separating the code from the markup and passing it to JRuby is not sufficient. We need a better way.
The real answer is to convert the entire RHTML file -- code and markup -- into a Ruby script, similiar to how a JSP compiler turns JSP files into J2EE servlet classes. Once the template is a valid Ruby script, we can pass it along to JRuby without the interpreter even being aware of its origins. The following sections will discuss the details of RHTML syntax and the conversion process that will be central to our RHTML processor.
Understanding the RHTML syntax
RHTML files are HTML files that have Ruby code embedded inside special tags: <% and %>. Everything inside these special tags is processed on the server before the page is returned to the client, so the end user can never see the original Ruby code. The Ruby code must have one or more complete Ruby statements, and statements in one group (<% ... %>) are affected by statements that were executed in previous groups. The fact that code snippets can be broken up into multiple groups helps programmers avoid situations where they have to generate HTML with print statements, creating a clean separation of code and markup. Listing 1 shows an example of RHTML content:
Listing 1. Sample RHTML file with embedded statements
<html>
<body>
<p>
Here is a list of numbers 1 - 10:
<%
(1..10).each do |n|
print n, ' '
end
%>
</p>
</body>
</html>
|
There is one special case to consider when embedding Ruby code into HTML: if the developer just wants to insert a single value or the result of a simple expression, the start tag can be modified to be <%=. >Listing 2 has an example of embedding simple values into a page without using complete statements.
Listing 2. Sample RHTML file with embedded expressions
<html>
<body>
<p>
The sum of 3 and 4 is <%= 3 + 4 %>
</p>
</body>
</html>
|
In order to generate Ruby scripts from files like those in Listings 1 and 2, we will need to create Ruby statements to represent the static HTML as well as the embedded code. All non-Ruby text (such as HTML and JavaScript) should be converted into print statements that output the string literals. All Ruby code should be copied in as-is, with print statements (for HTML) interspersed as necessary. The special case of embedded values (<%= ... %>) will require you to create a print statement that outputs the Ruby expression found between the tags. Listing 3 shows what a generated script should look like for the file in Listing 2:
Listing 3. Ruby script equivalent of RHTML file
print "<html><body><p>The sum of 3 and 4 is "
print 3 + 4
print "</p></body></html>"
|
As you can see, the scripts should be conceptually simple -- they are mostly print statements, with other Ruby statements mixed in as appropriate. One optimization that could be made to the code in Listing 3 is the use of newline characters to make the generated HTML look more like the original file -- this would make it much easier for developers to debug their RHTML templates. We will include this optimization in our own implementation.
The final decision that we have to make when planning for our RHTML processor is what to do with the scripts once they've been generated. The easiest thing to do would be to write the script to disk with a file name that could be easily mapped to the original RHTML file. For example, if the original file were named my-file.rhtml, the generated script could be my-file.rhtml.rb (.rb being the usual extension for Ruby scripts). Our processor can then use the following algorithm to generate and execute scripts:
- Create file name for the script that will represent the RHTML file.
- If the script exists and the RHTML file has not been recently modified, skip generation.
- Otherwise, generate the script and save it to disk using the name from Step 1.
- Execute the script.
Step 2 serves two purposes: it's an optimization because it prevents us from generating the same script over and over again, and it's a convenience for developers because changes they make to an RHTML file will show up immediately with the next request (a big time saver during development time).
Implementing the RHTML processor
Now that we have a strategy for implementing the RHTML processor, we need to relate it to our original Ruby processor. As with the previous article, you can follow along by adding code step-by-step to your sample project, or you can download the completed project from the Download section. If you're following along, you will want to start with the sample project you used for the last article so that the regular Ruby support is already in place.
Currently our Ruby support is handled by a class named JRubyInterpreter that implements Zero Core's Interpreter interface and executes HTTP requests using the JRuby interpreter. Processing RHTML files will require us to write another Interpreter class that first generates a Ruby script before invoking JRuby. We can extend the JRubyInterpreter class to avoid rewriting the JRuby-oriented code. Listing 4 shows an implementation of an RHTMLInterpreter class that does what we want.
Listing 4. The RHTMLInterpreter class
package zero.scripting.ruby;
import java.io.File;
import zero.core.context.GlobalContext;
import zero.core.events.HandlerInfo;
import zero.scripting.html.HTMLCompiler;
public class RHTMLInterpreter extends JRubyInterpreter
{
public void invoke(HandlerInfo handlerInfo)
{
try
{
File scriptFile = new File(handlerInfo.handler);
HTMLCompiler compiler = new HTMLCompiler();
//
// convert RHTML file to a Ruby script file
//
File compiledFile = compiler.compile(scriptFile, ".rb");
super.invoke(compiledFile);
}
catch (Throwable error)
{
GlobalContext.put("/request/status", 500);
GlobalContext.put("/request/error", error);
}
}
}
|
Notice that the RHTMLInterpreter.invoke() method uses a class named HTMLCompiler to handle the dirty work of converting RHTML to Ruby; it's only real responsibility is to ensure that this happens before delegating execution to the parent class. The HTMLCompiler class is large, but the compile() method used above is the most important part. Listing 5 shows the code from the compile() method. You can get the rest of the code from the Download section.
Listing 5. The HTMLCompiler.compile() method
public File compile(File htmlFile, String extension)
throws IOException, HTMLCompilerException
{
File compiledFile = getCompiledFile(htmlFile, extension);
//
// make sure we don't re-compile files that haven't changed
//
if (compiledFile.exists() &&
compiledFile.lastModified() > htmlFile.lastModified())
return compiledFile;
String html = getHTML(htmlFile);
int current = 0;
int codeStart = html.indexOf(_EMBEDDED_START_TOKEN, current);
FileWriter writer = new FileWriter(compiledFile);
//
// look for the code fragments and add them to the script as
// regular code statements. all HTML gets converted into
// print statements
//
while (codeStart >= 0)
{
//
// record all HTML preceding next code fragment
//
writePrintStatements(writer, html.substring(current, codeStart));
int codeEnd = html.indexOf(_EMBEDDED_END_TOKEN, codeStart);
if (codeEnd < 0)
throw new HTMLCompilerException("No matching end token found.");
int stmtStart = codeStart + _EMBEDDED_START_TOKEN.length();
String code = html.substring(stmtStart, codeEnd);
//
// special case - for <%= expression %> fragments, we just
// want to insert the expression's value, not make it a
// separate statement
//
if (html.charAt(stmtStart) == _EMBEDDED_VALUE_TOKEN)
writeValuePrintStatement(writer, code.substring(1));
//
// normal case - code recorded as it was written
//
else
{
writer.write(code);
writer.write('\n');
}
current = codeEnd + _EMBEDDED_END_TOKEN.length();
codeStart = html.indexOf(_EMBEDDED_START_TOKEN, current);
}
//
// take care of HTML after last code fragment, if any
//
if (current < html.length())
writePrintStatements(writer, html.substring(current));
writer.flush();
writer.close();
return compiledFile;
}
|
Notice that the compile() method includes the optimization specified by Step 2 of our algorithm. The first if statement makes sure that we only generate a script if we really need to. Also, if you look at the code for the writePrintStatements() method, you will see that it includes one newline character for every one found in the original file, so the generated HTML will look almost exactly the same.
Updating the Zero configuration file
Our code is in place, but we still have one more step to complete: like the Ruby interpreter before it, our RHTML interpreter must be registered with Zero Core so that it knows how to handle requests for .rhtml files. Adding the stanza from Listing 6 to your application's /config/zero.config file will make things official.
Listing 6. Add to your application's /config/zero.config file
[/app/interpreters]
.rhtml=zero.scripting.ruby.RHTMLInterpreter
|
Now that our RHTML support is in place, let's test it out by adding an RHTML file to our application and invoking it from a Web browser. To illustrate the complete integration of Ruby and RHTML with Zero, our template will use Zero's GlobalContext API to read request data and create a response. The RHTML file in Listing 7 shows the entire contents of the global context by serializing it within the HTML response.
Listing 7. RHTML file for testing our code
<%require "java"%>
<%include_class "zero.core.context.GlobalContext"%>
<%include_class "zero.core.context.SimpleFormatter"%>
<html>
<head>
<title>Sample RHTML Page</title>
</head>
<body>
<p>The current contents of the global context are:
<pre>
<%
formatter = SimpleFormatter.new
GlobalContext.dump formatter
print formatter
%>
</pre>
</p>
</body>
</html>
|
Add the markup from Listing 7 to a file named test-ruby-and-markup.rhtml in your application's /public directory. You should then start the Zero application with the zero run command and test things out by pointing your favorite Web browser to http://localhost:8080/test-ruby-and-markup.rhtml. The result should be a simple HTML page that displays the contents of the global context. Figure 1 shows a screenshot of what the page looks like in Mozilla Firefox.
Figure 1. Screenshot of HTML page created by our Ruby template
Being able to implement Zero applications with Ruby is great, but having the template features of Rails is even better. With the code that we wrote in this article, Ruby users can now do everything that Groovy and PHP users can do when creating Zero applications. Finally, the strategy that we used for converting HTML with embedded code into executable scripts is fairly generic and can be used to add more powerful templating features or new languages to the platform if desired.
| Description | Name | Size | Download method |
|---|---|---|---|
| Sample JRuby integration code and test files | zero.scripting.zip | 8419 KB | HTTP |
Information about download methods
Learn
-
Read the predecessor to this article, Add Ruby scripting to your Project Zero applications (developerWorks, October 2007).
-
Need an introduction to Project Zero? Take a look at "Get started with Project Zero and PHP" (developerWorks, updated January 2008).
-
The developerWorks Ajax resource center is packed with tools, code, and information to get you started developing slick Ajax applications today.
- With Web 2.0 being a hot area within development
circles, you'll find an ever-growing collection of resources in the Web development zone.
- Need to get up to speed on PHP? Take a look at our
PHP area.
-
The developerWorks Project Zero space is packed with resources to get you started developing with Project Zero now.
-
You'll find a nicely growing library of Project Zero content here on developerWorks.
Get products and technologies
-
Download Project Zero M2 and start applying the best practices covered in this article.
-
Download JRuby 1.0 to build and run the code included with this article.
Discuss
-
Participate in the projectzero.org discussion forum.
-
developerWorks
blogs: Get involved in the developerWorks community.

Dan Jemiolo is an Advisory Software Engineer on IBM's Project Zero team in Research Triangle Park, NC. He is currently working on reusable components for the Zero platform and its service catalog. His previous work includes the design and development of Apache Muse 2.0 and participation in OASIS Web services standards bodies. Dan came to IBM three years ago after earning his Master of Science degree in Computer Science from Rensselaer Polytechnic Institute.
Comments (Undergoing maintenance)





