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 object would need to be populated over several calls to the
PullParserevent handler function: a new object would be created when the opening<customer>tag is encountered, and populated when the text nodes inside the<name>and<address>elements are met. Because it needs to survive across multiple function calls, theCustomerobject can't live as a local variable of the event handling function, but ends up polluting the instance variable space of the function's parent class. -
Another problem arises from
<name>being a child of both the<customer>and the<product>elements. To process the XML correctly the event handling function needs to maintain context data about where it is in the document. Again, this cannot be held local to the function, but must pollute the parent class.
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.
