Joshua Marinacci wrote in 2004:
I really think that a complete XHTML renderer is a vital component of any modern programming toolkit.
I cannot do more but agree with him completely, much more so three years later.
I am not talking about trying to load native components into a Java application, but rather
an all Java XHTML + CSS renderer.
Unfortunately, I also have to agree with the two questions from the same blog entry:
Why are there so few renderers, and almost none that are opensource? Is it really that hard?
Fortunately, he set out to answer these questions and made the product open source:
Flying Saucer - The 100% Java XHTML/CSS2 RendererThis entry is a little bit of "Flying Saucer" evangelism, I wanted to share some interesting use case with you and some tuning tricks to make it do quite impressive things.
The base description of the use case I had at hand is relatively simple:
Create a panel that can display instant messages, in realtime, layouting them from an XHTML+CSS source that makes the look completely customizable. Messages arrive async and should be inserted into the actual document.
I first searched the mailing lists for some hints, but all I found was
somehow not very promising:
Everytime a new chat message is added we update the Document, the panel blanks out, re-renders, and then we scroll back to the bottom where the new message is added.
So I set out to try this myself, and I realized there were actually two performance problems:
- the setup time of the XHTMLPanel when loading the basic XHTML document:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<link rel="stylesheet" href="style.css" media="all" />
</head>
<body>
<div id="content">
</div>
</body>
</html>
- The time to insert message divs (with some structure; e.g. header, body etc.) in the content div.
And then, there is a related problem, which is basically the fact that we want to display emoticons using images, translating them and probably allowing different sets. Now without spoiling everything, here an image to give you an idea what we are talking about:

1. Setup time and solving them with some Java URL Tricks
The initialization of the
XHTMLPanel was a weird puzzle from the very beginning. In fact, it was kind of unpredictable how much time it was taking, but most of the time it was simply too much. Now, after guessing a while I was working without a network connection and I suddenly realized what the problem was:
http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtdHacking some debug statements into the flying saucer source revealed it clearly, a complete set of documents was loaded over the network, the DTD and the Entities.
Probably I have taken a strange route, but it resolved the problem and it was at the same time an elegant solution for the task with the emoticons.
Instead of hacking the flying saucer code I remembered some URL acrobatics that had helped me out in various situations during my time with Java: custom URL handler implementations. Now, instead of getting into details, I will just
point you to a document that does a very good job on this topic. With a little bit of code you will end up with a custom handler and be able to use something like the following URL to replace the DTD:
resource:net/coalevo/empp/client/resources/xhtml1-strict.dtdAll the resources can be embedded into your JAR, which makes it easy to deploy. Finally, this approach allows you to plug in a simple cache (most of the time a LRU map as provided by the Java class library is just fine), and get max performance for repetitive resources....like, yes, I suppose you guessed it: emoticons! :) It can't get simpler than that, and if your resource loader is well built, you may just swap the images on the fly....:
<img src="resource:net/coalevo/empp/client/resources/images/emoticons/smile.png" />Before this works however, you need to null out the base uri:
SharedContext ctx = m_HTMLPane.getSharedContext();
ctx.getUac().setBaseURL(null);
UPDATE: I have tweaked R6
NaiveUserAgent.java to allow this. At the beginning of the
public String resolveURI(String uri) method, I added:
if(baseURL == null || baseURL.length()==0) {
return uri;
}
2. Inserting Messages
With the first problem and the additional task solved, what was left was to insert messages into the preloaded document.
First step: We need to parse the previously prepared XHTML base document into a
Document. That's an easy part, flying saucer actually offers some utility to do this (see the FAQ). I preferred to grab a
StringReader and wrap it into a new
InputSource, which in turn can be used by
DocumentBuilder.parse().
new InputSource(new StringReader(documentContents));
However, this isn't a good idea for the actual messages, less if we need performance. More later.
Second Step: we need to insert the message at the correct node. Actually this turns out to be rather easy to achieve:
m_InsertNode = m_Document.getElementById("content");
Third Step: Parsing messages from a String quickly into the actual document.
It's a bit more tricky. One needs to know some XML acrobatics... :) I came up with a helper class that solves the problem: a document fragment insertion utility class that parses the incoming message strings directly as new nodes into the document.
package net.coalevo.empp.client.impl;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
class DocumentFragmentInserter {
protected final SAXParser m_Parser;
protected Document m_Document;
protected Node m_InsertNode;
protected Handler m_DocumentHandler;
public DocumentFragmentInserter()
throws SAXException, ParserConfigurationException {
this(SAXParserFactory.newInstance().newSAXParser());
}//DocumentFragmentInserter
public DocumentFragmentInserter(SAXParser parser) {
this.m_Parser = parser;
}//DocumentFragementBuilder
/**
* Treats the xml parameter as a well-formed XML document to be inserted
* into the given document.
*/
public void parse(Document doc, Node insert, String xml)
throws SAXException {
try {
parse(doc, insert, new ByteArrayInputStream(xml.getBytes("UTF-8")));
}
catch (IOException ex) {
}
}//parse
/**
* Treats the xml parameter as a well-formed XML document to be inserted
* into m_Document.
*/
public void parse(Document doc, Node insert, InputStream xml)
throws SAXException, IOException {
m_Document = doc;
m_InsertNode = insert;
m_Parser.parse(xml, getDocumentHandler());
}//parse
protected DefaultHandler getDocumentHandler() {
if (m_DocumentHandler == null) {
m_DocumentHandler = new Handler();
}
return m_DocumentHandler;
}//getDocumentHandler
protected class Handler extends DefaultHandler {
protected Node m_Current = m_InsertNode;
public void characters(char [] ch, int start, int length) {
m_Current.appendChild
(m_Document.createTextNode(new String(ch, start, length)));
}//characters
public void endElement(String uri, String localName, String qName)
throws SAXException {
m_Current = m_Current.getParentNode();
}//endElement
public void processingInstruction(String target, String data) {
m_Current.appendChild
(m_Document.createProcessingInstruction(target, data));
}//processingInstruction
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
//System.out.println(qName);
Element e = m_Document.createElement(qName);
m_Current.appendChild(e);
m_Current = e;
int nAtts = attributes.getLength();
for (int i = 0; i < nAtts; i++) {
String qn = attributes.getQName(i);
String val = attributes.getValue(i);
e.setAttribute(qn, val);
if("id".equals(qn)) {
e.setIdAttribute(qn,true);
}
}
}//startElement
}//class Handler
}//DocumentFragmentInserter
The code for inserting a message now looks similar to this:
m_DocumentInserter.parse(m_Document, m_InsertNode, msg);
m_XHTMLPane.relayout();
Clean. Works. Everything looks good, except....
Fourth Step:
Now, we really don't want an infinite number of messages being displayed, because it may simply kill performance after a while. Fortunately it's realtively easy to also remove a child node message from the
Document. So all you actually need is a counter, a max history variable and a conditional that removes the oldest node (e.g. the first child of our insert node) from the moment the max number is reached.
m_DocumentInserter.parse(m_Document, m_InsertNode, msg);
if(m_MessageCount >= m_MaxHistory) {
m_InsertNode.removeChild(m_InsertNode.getFirstChild());
} else {
m_MessageCount++;
}
m_XHTMLPane.relayout();
Sixth Step:
Finally we want the window to scroll properly to the bottom. It took me quite a while to find, but a solution that seems to work
is described here, as ReverseScrollBar.
Now, altogether it works out nicely. On my PowerBook G4 Alu (1.67Ghz) it runs smoothly, no blanking out of the panel and almost no delay for displaying messages. I think that this should at least give you some pointers and ensure you that
it is possible. If you are stuck, don't hesitate to contact me, also, it is likely that the code will soon be in the Coalevo SVN repository.