Skip to content

XHTML Builder Example

Ben Fagin edited this page Aug 17, 2013 · 14 revisions

Builders are well suited for working with nested operations, and Flapi provides for this with its concept of blocks. Let's build a descriptor which can be used to generate an XML document. In Java this uses the w3c Document and Element classes primarily.

The Descriptor

Descriptor builder = Flapi.builder()
    .setPackage("unquietcode.tools.flapi.examples.xhtml.builder")
    .setStartingMethodName("createDocument")
    .setDescriptorName("XHTML")

    .addMethod("addComment(String comment)").any()
    .addMethod("done()").last(Document.class)

    .startBlock("Element", "startElement(String tagName)").any()
        .addMethod("setValue(String value)").atMost(1)
        .addMethod("addAttribute(String key, String value)").any()
        .addMethod("addComment(String comment)").any()
        .addMethod("endElement()").last()
        .addBlockReference("Element", "startElement(String tagName)").any()
    .endBlock()
.build();

We have one block, called Element. Each element can have any number of elements inside of it, just as in XML. We can provide text values of the element once, any number of attributes, and any number of comments.

The Helpers

Implementing the XHTMLHelper isn't so bad in this case, because our actions more or less mirror what we would call on the normal objects. All we need to do is keep track of our top level document, and add elements and comments as needed.

public class XHTMLHelperImpl implements XHTMLHelper {
    final Document document;

    public XHTMLHelperImpl() {
        try {
                document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
    }

    @Override
    public Document done() {
        return document;
    }

    @Override
    public void startElement(String tagName, ObjectWrapper<ElementHelper> _helper1) {
        Element element = document.createElement(tagName);
        document.appendChild(element);
        _helper1.set(new ElementHelperImpl(element));
    }

    @Override
    public void addComment(String comment) {
        document.appendChild(document.createComment(comment));
    }
}

Likewise, the ElementHelper can be implemented simply by keeping track of an Element object and calling the appropriate methods on it. This is the ideal case for a builder—wrapping an existing object's functionality.

public class ElementHelperImpl implements ElementHelper {
    private final Element element;

    ElementHelperImpl(Element element) {
        this.element = element;
    }

    @Override
    public void endElement() {
        // nothing
    }

    @Override
    public void startElement(String tagName, ObjectWrapper<ElementHelper> _helper1) {
        Element newElement = element.getOwnerDocument().createElement(tagName);
        element.appendChild(newElement);
        _helper1.set(new ElementHelperImpl(newElement));
    }

    @Override
    public void addAttribute(String key, String value) {
        element.setAttribute(key, value);
    }

    @Override
    public void addComment(String comment) {
        Comment commentNode = element.getOwnerDocument().createComment(comment);
        element.appendChild(commentNode);
    }

    @Override
    public void setValue(String value) {
        element.setTextContent(value);
    }
}

Usage

Here, a sample usage building an XML representation of a book library:

Document doc = XHTMLGenerator.createDocument(new XHTMLHelperImpl())
    .addComment("This is a list of books in my library.")
    .startElement("books")
        .startElement("book")
            .addAttribute("ISBN", "978-0375703768")
            .setValue("House of Leaves")
        .endElement()

        .startElement("book")
            .addAttribute("ISBN", "978-0399533457")
            .setValue("The Cloudspotter's Guide")
        .endElement()
    .endElement()
.done();

Which corresponds to the following output when the document is written to a stream:

<!--This is a list of books in my library.-->
<books>
    <book ISBN="978-0375703768">House of Leaves</book>
    <book ISBN="978-0399533457">The Cloudspotter's Guide</book>
</books>

Now, it is certainly not required that each method be called in a chain. Every method returns a typed value, and so it is possible to break up the chain to perform repeated operations. In the next usage, I'm iterating a list and creating elements each time, then going back to finish the document.

Document doc;
ElementBuilder_setValue<XHTMLBuilder<Document>> movies
    = XHTMLGenerator.createDocument(new XHTMLHelperImpl())
    .addComment("This is a list of movies in my library.")
    .startElement("movies");

for (Movie movie : getMovieList()) {
    movies.startElement(movie.mediaType)
          .setValue(movie.title)
          .endElement();
}

doc = movies.endElement().done();
printDocument(doc);

...which creates this document:

<!--This is a list of movies in my library.-->
<movies>
    <DVD>Hedwig and the Angry Inch</DVD>
    <DVD>Best in Show</DVD>
    <VHS>Blow Dry</VHS>
</movies>

The full source is located under src/test in the examples package, accessible here.

Clone this wiki locally