Sprox is a small Java 7/8/11 library (around 50 kB) with zero dependencies that provides a simple, annotation-based API for processing XML documents. Sprox can be used in a standalone environment as well as in an OSGi environment (up to version 3).
When you need to process an XML in Java, you basically have three types of libraries at your disposal:
- Document Object Model: W3C, JDOM, DOM4J, XOM and many others.
- Object binding: JAXB, XmlBeans and again many others.
- Low-level parsing: SAX, StAX.
After using each of these options many times in many projects, I found that there's room for a fourth.
XML is a complex beast. There's a lot you can do with it. That's one reason why all existing XML libraries are so complicated. Sprox limits itself to a subset. This allows for a small and simple API. You can use Sprox if:
- You can process the XML in one go. Sprox goes from front to back through the XML, exactly once.
- You need access only to nodes, attributes and/or node content. Sprox doesn't give you access to document preambles for example.
- You do not need to process mixed content, like in XHTML for example. Sprox can process documents containing mixed content, as long as it valid XML, but it cannot process the mixed content itself.
In case you missed it: Sprox is one-way. From XML into your system. Not the other way around.
Adding Sprox to a Maven project is easy. Just add the following dependency:
<dependency>
<groupId>nl.ulso.sprox</groupId>
<artifactId>sprox</artifactId>
<version>4.0.1</version>
</dependency>
This assumes that you use JDK 11+. On JDK 8, use the latest 3.x version. On JDK 7, use the latest 2.x version.
Note that snapshot releases are not available in central repositories. You'll have to git clone
and mvn deploy
this repository yourself if you want to use the latest versions. See the list of tags for the available stable releases.
- Requires Java 11+
- Interface compatible with Sprox 3.x.
- Introduces a Java module
nl.ulso.sprox
. Make sure to read the section on Java 11 below. - Drops support for OSGi.
- Requires Java 8+.
- Replaces the custom 2.x
@Nullable
annotation to denote optional parameters with the built-injava.util.Optional
. - Supports Java 8's lambda's and method references, offering additional methods for synthetic types in the
XmlProcessorBuilder
interface. - Supports Java 8's repeatable annotations for
@Namespace
. No need to wrap them in a@Namespaces
annotation anymore. - Introduces optional values for
@Node
,@Attribute
and@Source
annotations, as names can be resolved from the corresponsing methods and parameters automatically on Java 8. For parameters this does require the-parameters
compiler option to be used when compiling controller classes!
- Requires Java 7+.
This tutorial uses Atom feeds as a running example. All code is delivered as working source code, in test cases. The goal of this tutorial is to introduce the Sprox API step by step. Once you've finished reading the tutorial, you've seen all of it.
Let's say we have an Atom feed and we need to know how many entries there are. In this example, we'll use the Google Webmaster Central Blog. Their feed normally contains the last 25 blog entries. Let's verify that.
First, we need to implement a controller for Sprox's processor to use when going through the XML. Here it is:
public class FeedEntryCounter {
private int numberOfEntries;
public int getNumberOfEntries() {
return numberOfEntries;
}
@Node("entry")
public void countEntry() {
numberOfEntries++;
}
}
There's nothing special about this class. It's just a POJO. The magic is at the countEntry
method. It's annotated with @Node("entry")
. This tells Sprox to call this method whenever it encounters that node in the feed. Here we just increment a counter. Once the feed is processed completely, the counter will be equal to the total number of entries in the feed.
By the way: if the method
countEntry
was namedentry
instead, then the@Node
annotation wouldn't need a value. Sprox would use the name of the method as the name of the node.
Now we need to use this controller with some XML. Here's a JUnit test method (from class FeedEntryCounterTest
) that does just that:
public void countAllEntriesInFeed() throws Exception {
final FeedEntryCounter entryCounter = new FeedEntryCounter();
final XmlProcessor<Void> processor = createXmlProcessorBuilder(Void.class)
.addControllerObject(entryCounter)
.buildXmlProcessor();
processor.execute(
getClass().getResourceAsStream("/google-webmaster-central-2013-02-01.xml"));
assertThat(entryCounter.getNumberOfEntries(), is(25));
}
First we instantiate our controller. Then we build an XmlProcessor
using this controller. Don't worry about the Void
stuff in there yet; it'll be explained later. Once the processor is constructed, we execute it, in this case on a file. Finally we check that there are indeed 25 entries.
Pretty easy, right?
But there's an issue: the processor cannot be used concurrently, or even more than once, because our controller uses an instance variable to store its state and Sprox uses it as a singleton in the processor. Let's fix that.
The XmlProcessor
in the previous example uses Void
as the generic type parameter. Now is the time to explain what that parameter does: it defines the result type of the processor. In our example, what we're really interested in is a number, an Integer
. So that's what we'd like our processor to produce.
First, we need to make our controller a little smarter:
public class BetterFeedEntryCounter {
private int numberOfEntries;
@Node("feed")
public Integer getNumberOfEntries() {
return numberOfEntries;
}
@Node("entry")
public void countEntry() {
numberOfEntries++;
}
}
This class is almost the same as the previous one, except that the getNumberOfEntries
is also annotated, with @Node("feed")
. The method doesn't return an int
any longer. It now returns an Integer
. Any annotated method must return either void
or a non-primitive type. It needs to be public
as well.
Sprox calls your annotated methods not at the start of the node you're interested in, but at the end. Why it does that will be made clear later on. In this case it means that the countEntry
method is called first - once for every entry - and only then the getNumberOfEntries
method. So by the time the latter method is called, the numberOfEntries
member variable will have been incremented 25 times.
Here's the code that uses our new controller:
public void countAllEntriesInFeedWithResult() throws Exception {
final XmlProcessor<Integer> processor = createXmlProcessorBuilder(Integer.class)
.addControllerClass(BetterFeedEntryCounter.class)
.buildXmlProcessor();
final int numberOfEntries = processor.execute(
getClass().getResourceAsStream("/google-webmaster-central-2013-02-01.xml"));
assertThat(numberOfEntries, is(25));
}
Two things are different from the previous example:
- The
XmlProcessor
is configured to return anInteger
. Sprox is smart enough to guess that this matches thegetNumberOfEntries
method in the controller. It will use the result from that method as the result of the processor execution. - Instead of creating the processor with an instance of our controller, we reference the controller class. Sprox will instantiate it for us. It does so exactly once in each execution. The controller needs to have a constructor with zero arguments of course.
The processor created in this example can safely be used concurrently. All state that is built up in the controller is bound to a single execution. Additionally, the code using the processor doesn't need to know anything about the controller used within the processor. The processor itself collects the result and returns it.
This is mighty nice, but if this would be all that Sprox could do, it would still be pretty useless. Time to make it a bit more interesting!
Let's say we still want to count entries, but only the entries published in 2013. Every entry has a node published
, containing the publication date.
Here's one way to access that data and use it: (Don't worry, we'll replace it with a better one later.)
public class FeedEntryFrom2013Counter {
private int numberOfEntries;
@Node("feed")
public Integer getNumberOfEntries() {
return numberOfEntries;
}
@Node("entry")
public void countEntry(@Node("published") String publicationDate) {
if (Integer.parseInt(publicationDate.substring(0, 4)) == 2013) {
numberOfEntries++;
}
}
}
Straightforward, right? By adding parameters and annotating them with @Node("nodeName")
, Sprox injects the contents of those nodes in the controller method.
For parameters annotated with @Node("nodeName")
, the following rules apply:
- The node you're referring to must be either the node from the controller method itself, or a node inside that node.
- If there are multiple nodes with the same name found in the XML, then you'll get the content from the closest node. That is:
- In a hierarchy of nodes, the node whose depth is closest to the node the controller method is annotated with.
- In a list of nodes with the same name, all on the same depth, the first node.
- If the node you want the content of contains something other than characters, the node is ignored. Only nodes with character data or CDATA are allowed.
The previous section introduced a parameter annotated with @Node
to inject node content into a controller method. You might now guess that it's also possible to inject an attribute value, with @Attribute
. That guess would be completely correct: add a parameter, annotate it with @Attribute("attributeName")
and Sprox will automatically hand the value of that attribute over to your method.
But there's more.
As mentioned briefly earlier, controller methods can return values. Sprox collects these values, for you to inject elsewhere. To be more precise: if there's a controller method that produces values of type T
, then you can write a second controller method that accepts a parameter of type T
, or of List<T>
.
Note that the order in which methods are called depends on the structure of the XML, not on the definition of your methods. Sprox does not build a dependency tree of your methods, it just processes XML events in the order found in the input. So if you define an object or list parameter in a controller method, make sure that the method that produces those objects comes first!
Some more rules that apply:
- If a controller method was invoked by Sprox many times but you inject only a single value elsewhere, you'll get the value that was generated first. All other values are simply discarded.
- If a controller method returns
null
instead of a value, Sprox will ignore it. - If a controller method produces data that you never inject anywhere, then Sprox will still collect it. At the end of the processing run the data is simply discarded. If you care about memory usage, don't create data you don't need.
Parameter injection is why Sprox calls your methods at the end of the node you annotated the controller method with and not at the beginning. It has to collect the data for the parameters first.
This explanation might look a little complicated now. It really isn't. You'll find that Sprox simply does what you expect it to. Except when it doesn't. Then you'll need to read this again.
Sprox tries to support the convention over configuration pattern as much as possible. This applies to the mapping of methods and parameters to XML elements especially.
All annotations - @Node
and @Attribute
, as well as the @Source
annotation you'll be introduced to later - have an optional value. If you omit the value, Sprox will pull the name of the XML element (node or attribute) from the method or parameter name.
Beware! This works for parameters only if you have set the -parameters
option on the Java compiler when compiling your controller classes. If you don't do that, the names of parameters are not retained in the class files, and therefore Sprox cannot access them.
Sometimes it's not possible to name your methods and/or parameters to XML elements, for example because the XML elements contain hyphens, start with upper case characters, or are just plain ugly. Or maybe you prefer better names on your methods, e.g. processElement(...)
instead of element(...)
. In these cases you still don't need to define the element names as the annotation values. Instead you can provide a custom ElementNameResolver
that implements some algorithm to translate method and parameter names into XML element names. You don't need to worry too much about this algorithm being expensive (in terms of CPU, time or memory). Sprox precomputes all mappings while inspecting the controller classes, when the processor is built. Resolvers are not used during XML processing.
By default, Sprox assumes that all data it needs to inject is required. That means that if any of the parameters is not available, Sprox will not call your method at all. You'll never get null
injected.
This is not always what you want. In such cases you can instruct Sprox to inject empty values. You do that by wrapping the optional parameters in a java.util.Optional
.
Let's say the published
node is optional (which it isn't) and we assume that every entry without a publication date was published in 2013. Then we'd have to implement our controller as follows:
public class FeedEntryFrom2013CounterWithOptionalPublicationDate {
private int numberOfEntries;
@Node("feed")
public Integer getNumberOfEntries() {
return numberOfEntries;
}
@Node("entry")
public void countEntry(@Node("published") Optional<String> publicationDate) {
if (!publicationData.isPresent()) {
numberOfEntries++;
} else {
if (Integer.parseInt(publicationDate.get().substring(0, 4)) == 2013) {
numberOfEntries++;
}
}
}
}
If entries without a publication date shouldn't count for 2013, then we could just use the controller from the previous example: without the Optional
wrapper, Sprox would skip all nodes that don't have a publication date.
In the example above, the publication date that is injected into the controller method is a String
. Couldn't it be a DateTime
(from Joda-Time)? Yes it can! Any parameter annotated with @Node
or @Attribute
can have any type you want. Sprox might not know how to convert the data from the XML (a String
) to that type, but that's something you can teach it to. You do that by creating a custom Parser
.
Here's a parser for dates in Atom feeds:
public class DateTimeParser implements Parser<DateTime> {
@Override
public DateTime fromString(String value) throws ParseException {
try {
return DateTime.parse(value);
} catch (IllegalArgumentException e) {
throw new ParseException(DateTime.class, value, e);
}
}
}
Provided that Sprox knows about this parser, we can now implement our controller like this:
public class BetterFeedEntryFrom2013Counter {
private static final DateTime JANUARY_1ST_2013 = DateTime.parse("2013-01-01");
private int numberOfEntries;
@Node("feed")
public Integer getNumberOfEntries() {
return numberOfEntries;
}
@Node("entry")
public void countEntry(@Node("published") DateTime publicationDate) {
if (publicationDate.isAfter(JANUARY_1ST_2013)) {
numberOfEntries++;
}
}
}
Much cleaner! Controllers are defined with the types that you choose. Nowhere do you need to depend on an XML- or Sprox-specific type. The only thing that Sprox adds are annotations. No leaky abstractions.
So how do we teach Sprox to use our custom parser? By passing it to the XmlProcessorBuilder
. Here's a test method that does that:
public void countOnlyEntriesPublishedIn2013InFeedWithCustomParser() throws Exception {
final XmlProcessor<Integer> processor = createXmlProcessorBuilder(Integer.class)
.addControllerClass(BetterFeedEntryFrom2013Counter.class)
.addParser(new DateTimeParser())
.buildXmlProcessor();
final int numberOfEntries = processor.execute(
getClass().getResourceAsStream("/google-webmaster-central-2013-02-01.xml"));
assertThat(numberOfEntries, is(1));
}
By default Sprox is able to parse XML content as each of the Java primitive types. If these parsers don't do what you want them to, you can replace them with your own.
Now that we got introduced to most of the features of Sprox, it's time to give a more useful example. Let's say we want to build an in-memory representation of an Atom feed, using our own (immutable) domain model:
- A feed has an author and holds a list of entries.
- An author has a name, an email address, and a uri.
- An entry has an author, an ID, title, subtitle, publication date and the actual text.
- A piece of text has a type - it's either plaintext or (X)HTML) - and the content itself.
It's important to note that we're only modeling the data that we're interested in. An Atom feed contains a lot more data. With Sprox, you never need to see that data; you can just act as if it doesn't exist. Your codebase isn't polluted as it would be if you were generating code from XSD's.
To build our model from an XML input source, all we need is the following controller. Note that all methods and parameters are named according to the XML element they refer to, so that the values of all annotations can be omitted.
public class FeedFactory {
@Node
public Feed feed(@Source Text title, @Source Text subtitle,
Author author, List<Entry> entries) {
return new Feed(title, subtitle, author, entries);
}
@Node
public Author author(@Node String name, @Node String uri, @Node String email) {
return new Author(name, uri, email);
}
@Node
public Entry entry(@Node String id, @Node DateTime published, @Source Text title,
@Source Text content, Optional<Author> author) {
return new Entry(id, published, title, content, author);
}
@Node
public Text title(@Attribute Optional<TextType> type, @Node String title) {
return createText(type, title);
}
@Node
public Text subtitle(@Attribute Optional<TextType> type, @Node String subtitle) {
return createText(type, subtitle);
}
@Node
public Text content(@Attribute Optional<TextType> type, @Node String content) {
return createText(type, content);
}
private Text createText(Optional<TextType> textType, String content) {
return textType.map(type -> {
switch (type) {
case TEXT:
return new SimpleText(content);
case HTML:
return new HtmlText(content);
default:
// XHTML is not supported
throw new IllegalArgumentException(
"Unsupported text type: " + textType);
}
}).orElse(new SimpleText(content));
}
}
This code pretty much speaks for itself, doesn't it?
To be able to read feeds into memory, we set up a processor and use it, like this:
final XmlProcessor<Feed> processor = createXmlProcessorBuilder(Feed.class)
.addControllerClass(FeedBuilder.class)
.addParser(new DateTimeParser())
.addParser(new TextTypeParser())
.buildXmlProcessor();
final Feed feed = processor.execute(
getClass().getResourceAsStream("/google-webmaster-central-2013-02-01.xml"));
The astute reader will have noticed the sneaky introduction of another Sprox feature: the @Source
annotation. The following section explains what it does.
Sometimes controller methods for different nodes produce the same result type. In the example above, that's true for the controller methods for the nodes title
, subtitle
and content
. All methods return objects of type Text
. How can you collect and correctly inject those? Up to now, you would have to collect them as a List<Text>
. That could work, provided that the nodes are always present in the XML and in the same order.
That feels awkward.
Enter the @Source
annotation. With this annotation you can refer back to the node the output of which you're interested in. You can put it on object and list parameters. The method will then receive values generated from the controller method for that node only.
Both the title
method and the subtitle
method return an object of type Text
. Both are needed in the feed
method. By declaring two separate parameters of type Text
and annotating them with @Source
we are able to get the right values injected.
We've been completely ignoring XML namespaces thus far. Therefore so did Sprox. By default Sprox processes all elements in the default namespace of XML documents. That's fine in many cases. When it isn't, you can enable support for namespaces.
If all XML elements processed by a controller belong to the same namespace, declare it on the controller with a @Namespace
annotation. For example:
@Namespace("http://www.w3.org/2005/Atom")
public class FeedFactory {
...
}
This declaration ensures that FeedFactory
processes only XML elements in the http://www.w3.org/2005/Atom
namespace, even if that namespace is not the default.
The Google Webmaster Central Blog uses multiple namespaces. For example it uses a namespace http://schemas.google.com/g/2005
to refer to images for authors. What if we would like to add these images to our domain model? After adding a class Image
, we can do this:
@Namespace("http://www.w3.org/2005/Atom")
@Namespace(shorthand = "g", value = "http://schemas.google.com/g/2005")
public class FeedFactory {
...
@Node
public Author author(@Node String name, @Node String uri, @Node String email, Image image) {
return new Author(name, uri, email, image);
}
...
@Node("g:image")
public Image image(@Attribute String src, @Attribute Integer width, @Attribute Integer height) {
return new Image(src, width, height);
}
}
The FeedFactory
now declares it processes two namespaces, the first being the default. The new method image
triggers on the node image
belonging to a different namespace. The rest of the class remains the same.
Note that the shorthand for the namespace in the code - g:
- has absolutely nothing to do with namespace prefixes in the XML itself. They don't need to match. Namespace shorthands just look a lot like namespace prefixes because it is convenient.
Here are the rules regarding namespaces:
- If your controller supports a single namespace and you want to strictly process it, declare it in a
@Namespace
annotation. - If your controller supports multiple namespaces, declare each of them in a separate
@Namespace
annotation. - Every namespace except the first requires a shorthand. The first namespace is the default. There's no need to use shorthands for that namespace anywhere.
- If a
@Node
annotation on a method doesn't include a shorthand, Sprox uses the default namespace set on the class. - If a
@Node
,@Attribute
or@Source
annotation on a parameter doesn't include a shorthand, Sprox uses the namespace set on the method.
Again this might look a bit complicated. Again it isn't. Sprox aims to do exactly what you expect it to.
Some XML structures are recursive in nature. OPML, for example. By default, Sprox doesn't understand recursive structures. Defining a controller that matches the OPML outline
node will just match the topmost node, and no more.
Given an Outline
class that holds a list of Element
s, with each Element
in turn holding a list of Element
s (and so on), the following controller will create an Outline
, containing all elements in an OPML file:
public class OutlineFactory {
@Node
public Outline opml(@Node String title, @Node DateTime dateCreated,
@Node DateTime dateModified, List<Element> elements) {
return new Outline(title, dateCreated, dateModified, elements);
}
@Recursive
@Node
public Element outline(@Attribute String text, Optional<List<Element>> elements) {
return new Element(text, elements);
}
}
Just two methods... The magic happens in the outline
method that is annotated with @Recursive
.
As you can see in the example, @Recursive
and Optional
typically go hand in hand: when creating an Element
, the list of Element
s below it is required. Recursively. The leaf elements don't have any children, so their list can be Optional
.
Because handling recursive structures is a bit tricky and a some additional overhead is involved, you have to explicitly enable it in Sprox.
The controllers in all the examples up to now had no exceptional cases. Either they are called by Sprox and work correctly, or they aren't called at all. What if your controllers do have exceptional cases? Then you can choose:
- You can throw any unchecked exception, without declaring it naturally. This unchecked exception is then the one that the
execute
method of theXmlProcessor
will throw. - You can throw a checked exception declared on the controller. This exception is then wrapped in an
XmlProcessorException
by Sprox, which is also checked.
Remember Item 58 of Effective Java (2nd Edition): "Use checked exceptions for recoverable conditions and runtime exceptions for programming errors". That's what Sprox itself does as well.
AKA: how do you acquire an instance of the XmlProcessorBuilderFactory
in your code?
To use Sprox in a Java 11 module, you have to do three things in your module-info.java
:
- Require Sprox.
- Make Sprox's
XmlProcessorBuilderFactory
available to your code. - Open up your package with controller classes to Sprox.
For example:
module mymodule {
requires nl.ulso.sprox;
uses nl.ulso.sprox.XmlProcessorBuilderFactory;
opens nl.ulso.sprox to mymodule.controllers;
}
The first requirement should be obvious. The second and third might not be.
Because Sprox's implementation is exposed only though the ServiceLoader mechanism (see next section), you have to provide the uses
clause. Otherwise the factory won't be available in your own code.
The opens
clause is needed to open up your controller classes (and thus the packages containing them) to Sprox because Sprox uses reflection to construct an XML processor from your annotated controllers. Also, it might use reflection to instantiate these same controllers. In Java 11 this only works if you open up your classes to Sprox.
The preferred way to get an instance of Sprox's factory is through Java's java.util.ServiceLoader
. (Actually in a Java 11 module with Sprox 4.x this is the only way.) For example:
ServiceLoader.load(XmlProcessorBuilderFactory.class)
.findFirst()
.orElseThrow(() -> new IllegalStateException("No factory available"));
In Sprox 3.x and below, next to the ServiceLoader, you can also construct an XmlProcessorBuilderFactory
yourself. For example:
public static <T> XmlProcessorBuilder<T> createXmlProcessorBuilder(Class<T> resultClass) {
return new StaxBasedXmlProcessorBuilderFactory().createXmlProcessorBuilder(resultClass);
}
This won't work in a Java 11 module, because the StaxBasedXmlProcessorBuilderFactory
is not exported by Sprox. Instead you have to use the ServiceLoader.
Up to version 3.x of Sprox you can deploy Sprox as a bundle in any OSGi 5+ container, after which a service of type XmlProcessorBuilderFactory
is available.
In Sprox 4.x, support for OSGi was dropped. No particular reason, other than that I couldn't get the Sprox BundleActivator
to work together with the Java 11 module. For now there should be no issue, since Sprox 3.x has feature parity with Sprox 4.x. I might bring back OSGi support if there's demand for it.
In the tutorial, controllers are either registered with a builder as singleton objects, or as classes instantiated by Sprox. There's a third option: using a ControllerFactory
. Controller factories provide you with the hook to create new instances of controllers for your processors that depend on objects outside of Sprox.
Controller methods are not ordered. Sprox uses Java's reflection API internally to discover and inspect the annotated controller methods. In that API the order of the methods in a class is unspecified. You cannot depend on one controller method having preference simply because it's above all others in your code!
If you do need some kind of ordering, the way to achieve that is to put your controller methods in different classes and then add these controllers to an XmlProcessorBuilder
in the right order, highest priority first.
Cases where you might need this are probably rare. For a far-fetched example, see class NodeAttributeTest
in the test cases.
Sprox itself uses a fixed amount of memory. When creating a processor, it creates an internal model from your controllers. The more complex your controllers, the more memory is needed. Still, it's not much, and it's constant. It's independent of the size of the XML being processed.
The amount of memory used during a processing run depends on your controllers. The more results you produce and the more parameters you inject, the more data Sprox needs to collect. So it's basically up to you!
Truth be told, Sprox hasn't been subjected to intensive load and stress testing yet. That's on the wish list.
Sprox internally uses a StAX parser. You can provide one yourself, but that's entirely optional. If you don't provide one, Sprox uses a parser provided by the platform. It configures this parser as securely as possible. DTDs are not supported, internal entity references are not replaced and external entity references are disabled. That means Sprox won't go out behind your back reading files from the local filesystem or downloading resources from the internet. Nor is Sprox susceptible to the Billion Laughs attack.
Unlike some other XML processors, Sprox is indifferent to attacks using deeply nested nodes. That's because Sprox doesn't actually build up a stack of any kind. There's no recursion in the control flow, nor are there internal data structures that reflect the hierarchy of the XML being processed.
Of course it is possible to make the JVM go out of memory. If you process a big XML and your controllers collect all data in it, that will certainly happen.
Sprox does not protect you from flaws in underlying libraries. If the StAX parser you use (probably the platform default) is insecure, then Sprox most likely is as well.
Sprox is one-way. You can use it only to pull data from XML, not for generating XML. That's a feature, not a limitation.
Libraries that go both ways are inherently complex. They always will be. There's no way to unify two completely different technologies in one easy-to-use API. Any which way that API cannot be a perfect fit. Requirements from the one will always bleed over to the other. Try reading data into an immutable object using JAXB, for example.
This is, of course, an opinion. Not a fact.
So what's the best way to generate XML, if DOM and object binding are so awful? Well, try a template engine like StringTemplate or FreeMarker. That will give you much more flexibility and speed while being much less hungry for memory.