Arkles - JavaFX XML made easy

Arkles is a new library that takes advantage of JavaFX Script's declarative syntax, combining it with a subset of the XPath notation, to create a more natural and manageable way to process XML documents in your JavaFX applications.

When PullParser gets its mitts on any XML data it turns each opening tag, closing tag, loose text element, etc. into a linear stream of Event objects. These events are fed to the callback function plugged into PullParser, one by one. Compared to other XML processing systems (SAX or DOM) the pull method's beauty is in its simplicity. But that simplicity comes at a price.

Take, as an example, this simple XML document:

<?xml version="1.0" ?>
<shop>
    <version>1</version>
    <title>Shop example</title>
    <customers>
        <customer>
            <name>Joe Smith</name>
            <address>123 Java Street</address>
        </customer>
        <customer>
            <name>Fred Bloggs</name>
            <address>456 Duke Road</address>
        </customer>
    </customers>
    <products>
        <product>
            <name>Widget Maker</name>
            <price>20 groats</price>
        </product>
        <product>
            <name>Chocolate Fireguard</name>
            <price>99 credits</price>
        </product>
    </products>
</shop>

Suppose we want to extract each of the customers in the XML and transfer their data (name and address) to a Customer object. This simple task presents two problems:

The first way to use Arkles (although not the quickest) is to provide it with a structure that mirrors the XML document being processed, with event handling functions for given parts of the XML placed at their corresponding points in the structure. We can use JavaFX Script's declarative syntax to create such a structure.

Consider the code below.

import javafx.data.pull.Event;
import com.jfxia.arkles.EventNode;
import com.jfxia.arkles.Handler;


// These classes will help store the data we extract from the XML
class Customer {
    var name:String;
    var address:String;
}
class Product {
    var name:String;
    var price:String;
}

// There are the actual variables we'll populate from the XML
var title:String;
var customers:Customer[];
var products:Product[];

// This is the event tree, mirroring the XML file structure
def shopEventTree = EventNode {
    name: "shop"
    onStart: function(ev:Event) { println("Start <shop>"); }
    content: [
        EventNode {
            name: "title"
            onText: function(ev:Event) { title=ev.text; }
        } ,
        EventNode {
            name: "customers"
            content: [
                EventNode {
                    name: "customer"
                    var c:Customer;
                    onStart: function(ev:Event) { c=Customer{}; }
                    content: [
                        EventNode {
                            name: "name"
                            onText: function(ev:Event) {  c.name=ev.text; }
                        } ,
                        EventNode {
                            name: "address"
                            onText: function(ev:Event) {  c.address=ev.text; }
                        }
                    ]
                    onEnd: function(ev:Event) { insert c into customers; }
                }
            ]
        } ,
        EventNode {
            name: "products"
            content: [
                EventNode {
                    name: "product"
                    var p:Product;
                    onStart: function(ev:Event) { p=Product{}; }
                    content: [
                        EventNode {
                            name: "name"
                            onText: function(ev:Event) {  p.name=ev.text; }
                        } ,
                        EventNode {
                            name: "price"
                            onText: function(ev:Event) {  p.price=ev.text; }
                        }
                    ]
                    onEnd: function(ev:Event) { insert p into products; }
                }
            ]
        }
    ]
    onEnd: function(ev:Event) { println("End <shop>"); }
}

def xh:Handler = Handler {
    rootEventNode: shopEventTree;
    input: (new java.net.URL("{__DIR__}test.xml")).openStream();
}
xh.parse();

The shopEventTree variable provides a hierarchical structure of EventNodes, mirroring the structure of the XML. Each EventNode has an onStart, onText, and onEnd callback, into which functions can be plugged to process PullParser events for that part of the tree.

We no longer have to worry about confusing <name> elements from different parts of the document, because the event handling code is now clearly separated into different functions for each part of the document. Also, the temporary Customer and Product objects are created within the scope of the part of the hierarchy they relate to — they do not pollute the rest of the class.

The Handler class takes the EventNode tree and an XML input, and uses a PullParser to read the XML and direct the resulting Events to the appropriate function in the EventNode structure.

This solution works for simple XML documents, but becomes unmanagable for larger documents. Fortunately Arkles provides an answer.

Modelling the whole XML document with an EventNode tree is a pain, particularly if we're only interested in specific sections of the data. Wouldn't it be nice to be able to model parts of the structure, then direct the Handler class to activate them when a given part of the document is reached?

Consider the XML below:

<?xml version="1.0" ?>
<shop>
    <version>1</version>
    <title>Shop example</title>

    <customerLists>
        <customers type="shop">
            <customer>
                <name>Joe Smith</name>
                <address>123 Java Street</address>
            </customer>
            <customer>
                <name>Fred Bloggs</name>
                <address>456 Duke Road</address>
            </customer>
        </customers>

        <customers type="online">
            <customer>
                <name>Jane Smith</name>
                <address>88 Java Street</address>
            </customer>
            <customer>
                <name>Freda Bloggs</name>
                <address>99 Duke Road</address>
            </customer>
        </customers>
    </customerLists>
</shop>

The structure is a little bit deeper than the first XML document we saw. The EventNode structure would also grow to quite long and deep if we had to model the whole structure. Fortunately Arkles provides a way of avoiding that. See the code below.

import javafx.data.pull.Event;

import com.jfxia.arkles.EventNode;
import com.jfxia.arkles.Trigger;
import com.jfxia.arkles.Handler;


def trigger1:Trigger = Trigger {
    path: "/shop/customerLists/customers";
    eventNode: EventNode {
        name: "customer"
        onStart: function(ev:Event) { println("Start <customer>"); }
        content: [
            EventNode {
                name: "name"
                onText: function(ev:Event) { println("  name={ev.text}"); }
            } ,
            EventNode {
                name: "address"
                onText: function(ev:Event) { println("  address={ev.text}"); }
            }
        ]
        onEnd: function(ev:Event) { println("End <customer>"); }
    }
}

def trigger2:Trigger = Trigger {
    path: "/shop/customerLists/customers[@type='online']";
    eventNode: EventNode {
        name: "customer"
        onStart: function(ev:Event) { println("Start online <customer>"); }
        content: [
            EventNode {
                name: "name"
                onText: function(ev:Event) { println("  online name={ev.text}"); }
            } ,
            EventNode {
                name: "address"
                onText: function(ev:Event) { println("  online address={ev.text}"); }
            }
        ]
        onEnd: function(ev:Event) { println("End online <customer>"); }
    }
}


def xh:Handler = Handler {
    triggers: [ trigger2 , trigger1 ];
    input: (new java.net.URL("{__DIR__}test2.xml")).openStream();
}
xh.parse();

Here we see two fragments of the document modelled by EventNodes, both handling the <customer> element and its content. But notice that rather than being plugged directly into the Handler, they are plugged into Trigger objects that also contain an XPath. This XPath will determine when the tree of event handlers will take effect. The first object (trigger1) will be used when the Handler is inside shop->customerLists->customers, while the second (trigger2) uses the same path, but also specifies that the customers element must have a type attribute set to online.

A list of triggers is given to the Handler object; as the XML document is walked the Handler scans this list from first to last looking for a Trigger that matches, When it finds a match it processes all child elements using the corresponding EventNode tree.

Note: because trigger2 has a more specific XPath than trigger1, it must be placed into the list first. If trigger1 was first it would swallow all the <customer> elements, and trigger2 would never get used.