Embedding a Jelly Scripting Capability in Your Java Application

Jelly is an XML-based scripting language designed to be easily embedded in other applications. As an example of embedding, Jelly provides the primary extensibility mechanism for Maven 1.x, a widely used Java build tool (although that support was dropped in Maven 2.x). In the commercial realm, Jelly is used to provide custom scripting capabilities for Jira, a useful and low-cost issue tracking system.

Jelly offers a several advantages as an embedded application feature:

  • Jelly provides a relatively simple XML-based scripting language that users can learn without much difficulty.

  • Jelly is equipped with a wide range of tags for performing various tasks, including setting variables, looping, performing database queries, executing Ant tasks, sending mail, etc. In part, the wide range of available tags is due to the fact that Jelly includes jelly-ized versions of all of the tags in the Java Server Pages Standard Tag Library (JSTL).

  • Jelly is easily extensible through the creation of Jelly tags using Java. This means that custom tags can easily be created by application vendors (or by users themselves with a suitably-documented, vendor-provided API).

  • Jelly is easy, almost trivial, to embed within a Java application.

Jelly also has a few disadvantages:

  • Documentation on how to effectively code in Jelly is scarce. This is exacerbated by the wide range of tags available to users right "out-of-the-box," i.e. — it can be very confusing for users to figure out how to even begin writing Jelly scripts.

  • The verboseness of XML is more suitable for data representation than it is as the basis of a programming language.

  • Error reporting in Jelly is minimal, i.e. — debugging a non-functional Jelly script can be painful (and is exacerbated again by the scarcity of documentation).

While not a perfect technology, Jelly still offers a solid mechanism for quickly and easily adding a scripting capability to a Java application. In this article, I'll demonstrate how to execute Jelly scripts from within a Java program.

The Jelly Ecosystem

To embed a Jelly capability within a Java application, you'll need some basic knowledge about the various libraries that Jelly depends upon. Jelly can be downloaded from the Jelly web site, where it is hosted as one of the tools under the Apache Commons initiative.

Jelly has quite a few open source dependencies, which are included in the regular distribution. Alternatively, it can be downloaded using Maven, with all of the dependencies pulled in automatically (see the section on Maven and Jelly for further details).

The only Jelly library that's absolutely required is commons-jelly. However, numerous other tag libaries are available; there's a list of them on the Jelly web site. Some of the most commonly used ones are listed below:

   commons-jelly-1.0.jar
   commons-jelly-tags-define-1.0.jar
   commons-jelly-tags-email-1.0.jar
   commons-jelly-tags-http-1.0.jar
   commons-jelly-tags-log-1.0.jar
   commons-jelly-tags-sql-1.0.jar
   commons-jelly-tags-util-1.0.jar

These libraries should be sufficient to support most Jelly scripts, although the only one used in the Jelly sample code in this article is commons-jelly itself, i.e. — the example used only the core Jelly tags. Additionally, if the "sql" tag library is used, appropriate database drivers will need to be added to the class path.

A Simple Jelly Script

So, what does a Jelly script look like? Listing 1 shows a simple Jelly script.

Listing 1: The properties.jelly File

<?xml version="1.0" ?>
<jelly xmlns="jelly:core" trim="no">
   Properties
 ----------
   <forEach var="iter" items="${systemScope}">
      ${iter.key} = ${iter.value}
   </forEach>
</jelly>

A Jelly script is simply an XML file. It consists of a standard XML declaration, followed by a pair of <jelly> tags. Between the <jelly> tags, other XML tags represent executable statements. Text not contained in tags will be passed on to the output when the Jelly script is executed.

In this example, the Jelly script will iterate over all of the environment variables in the "systemScope" collection, which is automatically made available to Jelly scripts. Variables are referenced using ${variable-name}, so this script will output a list of the environment variables and their corresponding values.

The <jelly> tag has a few attributes that are used in the example. The "xmlns" attribute defines the namespace. The "trim" attribute determines what happens to any generated whitespace. By default, a blank line appears in the output for each line of XML. The "trim" attribute allows the script creator to define whether the extra whitespace should be suppressed; the obvious values for the attribute are "yes" and "no."

When executed, the Jelly script should produce output similar to that shown in Output 1, with the normal variations due to different environments.

Output 1: Environment Settings

Properties
----------
java.runtime.name = Java(TM) SE Runtime Environment
sun.boot.library.path = C:\Program Files\Java\jre1.6.0_03\bin
java.vm.version = 1.6.0_03-b05
java.vm.vendor = Sun Microsystems Inc.
java.vendor.url = http://java.sun.com/
path.separator = ;
java.vm.name = Java HotSpot(TM) Client VM
file.encoding.pkg = sun.io
sun.java.launcher = SUN_STANDARD
user.country = US
sun.os.patch.level = Service Pack 2
java.vm.specification.name = Java Virtual Machine Specification
user.dir = C:\keener\dev\projects_java\jelly
java.runtime.version = 1.6.0_03-b05
java.awt.graphicsenv = sun.awt.Win32GraphicsEnvironment
java.endorsed.dirs = C:\Program Files\Java\jre1.6.0_03\lib\endorsed
os.arch = x86
java.io.tmpdir = C:\DOCUME~1\DAVIDK~1\LOCALS~1\Temp
line.separator = 
java.vm.specification.vendor = Sun Microsystems Inc.
user.variant = 
os.name = Windows XP
sun.jnu.encoding = Cp1252
java.library.path = // Eliminated for bevity
java.specification.name = Java Platform API Specification
java.class.version = 50.0
sun.management.compiler = HotSpot Client Compiler
os.version = 5.1
user.home = C:\Documents and Settings\davidkeener
user.timezone = 
java.awt.printerjob = sun.awt.windows.WPrinterJob
file.encoding = Cp1252
java.specification.version = 1.6
java.class.path = // Eliminated for brevity
user.name = DavidKeener
java.vm.specification.version = 1.0
java.home = C:\Program Files\Java\jre1.6.0_03
sun.arch.data.model = 32
user.language = en
java.specification.vendor = Sun Microsystems Inc.
awt.toolkit = sun.awt.windows.WToolkit
java.vm.info = mixed mode
java.version = 1.6.0_03
java.ext.dirs = // Eliminated for brevity
sun.boot.class.path = // Eliminated for brevity
java.vendor = Sun Microsystems Inc.
file.separator = /
java.vendor.url.bug = // Eliminated for brevity
sun.io.unicode.encoding = UnicodeLittle
sun.cpu.endian = little
sun.desktop = windows
sun.cpu.isalist = pentium_pro+mmx i486 i386 i86

Embedding Jelly Capabilities in an Application

There are two ways to execute a Jelly script: the simple way and the hard way. The hard way is actually more useful, because it provides more flexibility in how Jelly scripting capabilities are integrated into an application. We'll cover the hard way first, and hit the simple way later in this article.

Listing 2 shows the JellyRunner class, a Java class that is capable of executing a Jelly script.

Listing 2 - The JellyRunner Class

package com.keenertech.jelly;
import java.io.*;
import java.util.*;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.XMLOutput;

/**
* The JellyRunner class runs a Jelly script, where a Jelly script is an * XML file containing tags that represent executable statements. * * @author David Keener */ public class JellyRunner { public static void main(String[] args) { System.out.println( "Processing Jelly Script: " + args[0]); File file = new File(args[0]); if ((file != null) && (file.exists())) { try { // Create the context in which the Jelly script will be run JellyContext context = new JellyContext(); // Set variables which will be available to the Jelly script context.setContextVariable("jelly_script", args[0]); // Create a Writer object which will be used to collect the // output from executing the Jelly script. The StringWriter // allows the output to be collected in a buffer, from which // it can easily be converted into a string. Writer objWriter = new StringWriter(); XMLOutput objOut = XMLOutput.createXMLOutput(objWriter); // Now run the Jelly script; flush the output to make sure // all of it is collected by the StringWriter object context.runScript(file, objOut); objOut.flush(); // Show the XML output produced by the Jelly script System.out.print(objWriter.toString()); } catch (Exception e) { System.out.println("ERROR: Failed - " + e.getMessage()); } } } // End Method } // End Class

Most programs run in some sort of well-defined context. In a JSP web application, for example, code within the web page runs in a context in which certain features and objects are automatically made available, such as the Session, Request and Response objects.

It should come as no surprise that the first thing that needs to be instantiated in order to run a Jelly script is the JellyContext class. Once the context has been created, it can be modified as needed. The setContextVariable() method is used to define a variable that will be available to the Jelly script when it is executed.

      context.setContextVariable("jelly_script", args[0]);

This variable could then be referenced within the Jelly script:

      ${jelly_script}

Thus, an application can not only create the context in which a Jelly script will be executed, but also ensure that application-specific variables and objects are pre-defined for use by the scripts.

To actually run the script, the runScript() method is used, which seems obvious enough. The method is overloaded, so there are a number of choices for how to run a script. In the code above, the chosen method accepts a File object and an XMLOutput object as parameters.

      context.runScript(file, objOut);

The first argument seems logical, but what is the XMLOutput object needed for? Well, Jelly scripts theoretically output XML when executed (although they don't have to — our example Jelly script is going to produce just text). The runScript() method needs to be able to write out its output.

Let's look at how the XMLOutput object was defined prior to the runScript() call.

      Writer objWriter = new StringWriter();
      XMLOutput objOut = XMLOutput.createXMLOutput(objWriter);

First, a StringWriter object is created. Then, an XMLOutput object is created that will write its output to the StringWriter. Thus, when the Jelly script is executed by the runScript() call, the output that is produced is sent to the StringWriter object. Finally, the content of the StringWriter object is converted to a String and output for the benfit of the user.

If the script is successfully executed, the output will look something like that shown in Output 1. If unsuccessful, XML will be produced that provide some information about the error encountered.

JellyRunner and Maven 2

The JellyRunner application was built using Maven 2. The pom.xml (Project Object Model) file for the project is shown in Listing 3. This file describes various aspects of the project including, most importantly, its dependencies.

Listing 3 - The Maven pom.xml File

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.keenertech</groupId>
  <artifactId>jelly</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>Jelly</name>
  <url>http://www.keenertech.com</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>maven</groupId>
      <artifactId>commons-jelly</artifactId>
      <version>1.0.1-20060717</version>
    </dependency>
  </dependencies>
</project>

While the Maven build process is beyond the scope of this article, it should be mentioned that Maven tracks dependencies very effectively. By including commons-jelly as a dependency in the pom.xml file, the commons-jelly library, and any of its dependencies, will be downloaded automatically by Maven when a build is done.

Running the JellyRunner Class

Since the JellyRunner class was developed in a Windows environment, the DOS batch file shown in Listing 4 can be used to run the class once it has been compiled successfully. For those developing in UNIX or Mac environments, it is a fairly simple exercise to convert the batch file into a shell script (or Ant script). Basically, all the batch file does is set up the class path properly and then execute the JellyRunner class.

Listing 4 - Batch File

@echo off

REM Handle the necessary setup details
SET BUILD_DIR=C:\keener\dev3\projects_java\jelly
SET LIB=C:\Documents and Settings\davidkeener\.m2\repository

REM Set up the class path for Jelly-related libraries
SET Jelly=%LIB%\maven\commons-jelly\1.0.1-20060717\
    commons-jelly-1.0.1-20060717.jar
SET Jelly=%Jelly%;%LIB%\commons-jexl\commons-jexl\1.0\commons-jexl-1.0.jar

REM Set up the class path for dependencies
SET UTIL=%LIB%\commons-logging\commons-logging\ 
    1.0.3\commons-logging-1.0.3.jar
SET UTIL=%UTIL%;%LIB%\dom4j\dom4j\1.6.1\dom4j-1.6.1.jar
SET UTIL=%UTIL%;%LIB%\commons-collections\commons-collections\
    2.1\commons-collections-2.1.jar
SET UTIL=%UTIL%;%LIB%\commons-beanutils\commons-beanutils\
    1.6\commons-beanutils-1.6.jar

SET JAVA_EXEC=java
SET CP="%BUILD_DIR%\target\jelly-1.0-SNAPSHOT.jar;%Jelly%;%UTIL%"

REM Run the JellyRunner
%JAVA_EXEC% -cp %CP% com.keenertech.jelly.JellyRunner properties.jelly

In the batch file, BUILD_DIR is the top-level directory of the project. LIB represents the location of the local Maven repository, where downloaded dependencies are stored. The setting shows the standard location for the local repository for Windows environments. For UNIX environments, the local Maven repository would typically reside at:

      ~/.m2/repository

How to Execute a Jelly Script the Simple Way

We've seen how it's possible to create an instance of the JellyContext class, and then use that class to run a Jelly script. Using that method, the execution of a Jelly script is completely under the control of the application developer. The developer can control who can run Jelly scripts, where the application looks for Jelly scripts, where output from scripts go, etc. The developer can also define application-specific variables that can be used by Jelly scripts.

That's pretty much what you need to do if you want to embed Jelly within an application. But what if you don't really have a significant application? What if you just simply want to run a Jelly script?

Well, Apache provides a Jelly class (yes, it's actually called "Jelly") with a main() method. Just call the main method of the class, passing in the path of a Jelly. The Jelly class will execute the script and send the output to the terminal.

In the batch file from Listing 4, replace the line that runs the JellyRunner class with the line below:

      %JAVA_EXEC% -cp %CP% org.apache.commons.jelly.Jelly properties.jelly

Conclusion

Jelly is by no means a perfect technology for all occasions. Quite frankly, scripting in XML can be painful, except in constrained domains (such as automating build logic using Ant). On the other hand, Jelly represents a quick and effective way to add scripting logic to an application. Jelly provides application designers with a simple way for users to extend the capabilities of an application themselves.

Additionally, application designers can easily create application-specific Jelly tags. A well-defined set of Jelly tags can make application features directly accessible to users for scripting purposes, but in a well-defined and structured manner.

References

Here are a few references to aid you in scripting using Jelly.

Apache Commons Jelly Web Site
http://commons.apache.org/jelly/index.html
The official location for documentation related to Jelly.

Jelly Javadocs
http://commons.apache.org/jelly/apidocs/index.html
The most recently generated Javadocs for the Jelly API classes.

Java Server Pages Standard Tag Library (JSTL)
http://java.sun.com/products/jsp/jstl/reference/docs/index.html
This is Sun's official reference for JSTL. A solid knowledge of JSTL is essential in order to program effectively using Jelly.



Comments

David Keener By janko on Sunday, January 02, 2011 at 05:16 PM EST

Would you say that jelly is to painful for non-programmers?


David Keener By dkeener on Tuesday, January 04, 2011 at 06:05 PM EST

Janko, I would definitely say that Jelly can be painful for non-programmers. It's easy to integrate into your Java app, but certainly not trivial for your users.


David Keener By ycs on Wednesday, August 15, 2012 at 05:50 AM EST

hello,can you give me your code source in Windows environment? I built a project with maven2, but I can't run the project.


David Keener By dkeener on Saturday, August 18, 2012 at 06:10 PM EST

Hi ycs. I will try to find the code, but bear in mind that this article is more than 4-and-a-half years old, so it will take me some time to find the relevant code.


Leave a Comment

Comments are moderated and will not appear on the site until reviewed.

(not displayed)