Every which way

A new approach to XML – Part 2: Features of the XMLBeam library

SvenEwald
library.1

In the second part of this special series, Sven Ewald takes you on a dynamic, external and bidirectional XML journey.

This article is part of a series. Before you read this, we recommend checking out ‘Part 1: Data projection with XMLBeam’ to bring you up to speed. 

This time around, we take a closer look at the features of the XMLBeam library. We’ll show you how to parametrize projection targets, explain the use of external documents, and take a first look at writing projections.

In the first part of this series we learned about the concept of data projection and the function of sub-projections. By adding XPath expressions to method declarations, XMLBeam is an amazingly versatile tool. Today we are using this Java library with even more versatility by enhancing projection methods with parameters. These Parameters can influence the target of a projection method even at runtime. Let’s start with an example from the real world:

Example 1: Eclipse Code Formatter Configuration (Extract)
<profiles>
<profile name="Some Profile" version="8">
<setting id="org.eclipse.jdt.core.formatter..." value="false" />
                ...
</profile>
<profile name="Another Profile" version="8">
<setting id="org.eclipse.jdt.core.formatter..." value="true" />
         ...
</profile>
</profiles>

This XML file contains multiple configuration profiles. We define a projection interface to get a list of all profile names and all settings for one selected profile.

Example 1: Projection interface
@XBDocURL("resource://eclipsecodeformatprofile.xml")
public interface EclipseFormatterConfigFile {
interface Setting {
@XBRead("@id")
String getName();
@XBRead("@value")
String getValue();
}
@XBRead("//profile/@name")
List<String> getProfileNames();
@XBRead("//profile[@name="{0}"]/setting")
List<Setting> getAllSettingsForProfile(String profileName);
}

For now, we leave the first line containing the @XBDocURL aside because we are going to come back to that later.

The inner interface Setting, functioning as a sub-projection, returns the corresponding value for each configuration entry. The first requirement, providing a list of profile names, is met with the method getProfileNames().

The corresponding XPath expression selects all attributes name of all elements profile. As in the examples of the first part of this series, we don’t care about the XML structure. The simple XPath expression just does its’ job. The inner interface Setting is our sub-projection to access all values of a profiles settings.

The second method “getAllSettingsForProfile(String)” defines a String parameter which fills the placeholder “{0}” in the XPath expression when called.

This works because all parameters of projection methods are applied via the class java.text.MessageFormat to their XPath expression. So it’s possible for the specification of the effective target to have multiple parameters. To distinguish this concept from static data projection, we call this a dynamic projection.

The extensive syntax of the class MessageFormat is described in [2]. Our first example just takes a single String:

Example 1: Source code
EclipseFormatterConfigFile configFile = new XBProjector().io().fromURLAnnotation(EclipseFormatterConfigFile.class);

System.out.println("Profile names:" + configFile.getProfileNames());

for (Setting setting:configFile.getAllSettingsForProfile("Some Profile")) {
    System.out.println(setting.getName()+" -> "+setting.getValue());
}

Having defined the projection interface, it just takes four lines of code to print out the list of profiles and all settings for one profile.

Now we come back to the first usage of the annotation @XBDocURL. It assigns the URL of the target document to the projection, so the document can be loaded from a declared origin. This happens when the method projector.io().fromURLAnnotation(…) is called.

Apart from the standard protocols supported by the class java.net.URL, the projector accepts a special identifier “resource” defining a location within the class path. The class loader of the current projection interface is used to load the document. So you just need to place your document beside the projection interface to have it found by the projector. After successfully loading and parsing the document, the URL is set as the BaseURI in the DOM-Tree. Of course, there is no need to declare the URL in the interface. Callingprojector.io().url(“resource://eclipsecodeformatprofile.xml”).read(…) will do as well.

There is a second use case for the annotation @XBDocURL. You can directly use it on projection methods so that the target document of the XPath-evaluation is no longer the document attached to the projection interface, but an external document. In this way it’s possible to access multiple documents within one Java object. A simple example:

Example 2: Projection interface with external documents
public interface ExternalDocumentsDemo {
@XBDocURL("http://somewhere.examle/fooDocument.xml")
@XBRead("//foo")
String getFoo();

@XBDocURL("http://somewhere.examle/barDocument.xml")
@XBRead("//bar")
String getBar();
}

The interface ExternalDocumentsDemo defines two projection methods for two different external documents. When such a method is called, the assigned XPath expression isn’t applied to the document of the projection method anymore. It’s even possible to leave the document for the projection empty.

Example 2: Using external documents
ExternalDocumentsDemo projection = new XBProjector().projectEmptyDocument(ExternalDocumentsDemo.class);
System.out.println("Content of foo:"+projection.getFoo());
System.out.println("Content of bar:"+projection.getBar());

Each method call results in loading the target document, evaluating the XPath expression and returning the result converted into the desired Java type. This can even be combined with dynamic projections, because the parameters fill placeholders also in the document URL.

Example 3: A generic XPath evaluator on external documents
public interface DynamicExternalDemo {
@XBDocURL("{0}")
@XBRead("{1}")
String evaluateXPathfromURL(String url, String xpath);
}

Example 3 demonstrates the expressiveness of XMLBeam’s data projection. Creating a generic tool to extract data from a document at a given URL is really simple using XMLBeam.

Before we get too distracted by external or dynamic projection gimmicks, we take a look at another subject: Bidirectional projections.

In the first part of this series we already learned that projections are views onto DOM-trees. Now we learn that these views are not limited to reading elements or attributes. Creating, reading, updating, and deleting of any DOM nodes is supported. However, the XPath syntax has to be limited for all writing operations. E.g. functions must not be used and the target of the XPath selector must be distinct. When the expression complies with these formalities, reading and writing are symmetric operations. This means you read the same data you wrote previously, and writing data you read before does not change the document.

Example 4: A bidirectional projection
public interface Bidirectional {
@XBRead("/a/b/c/foo")
String getFoo();

@XBWrite("/a/b/c/foo")
void setFoo(String value)

@XBDelete("/a/b/c/foo")
void deleteFoo();
}

Declaring a writing projection method is similar to declaring a reading one: Instead of @XBRead, the annotation @XBWrite has to be used and the method needs a parameter for the value to be written. The parameter type may be chosen freely as the method toString() is used. Passing a null value removes the value from the projection target leaving behind an empty attribute or element, respectively. To delete entire attributes or elements, the annotation @XBDelete is used.

To this point we have seen dynamic projections with parameters for the XPath expression and writing projections with a parameter for the value. But how do we combine writing a projection with a dynamic XPath target? This is shown in the next example.

Example 5: A dynamic bidirectional projection
public interface DynamicBidirectional {
@XBWrite("/a/{1}/foo")
void setFoo(String value,String parentElementName)

@XBWrite("/a/b/foo[@key="{0}"]/@value")
void setValue(String key,@Value int value);
}

The method setFoo() gets another parameter which is used in the XPath expression. The first one will be written as a value to the element “foo”. For the second method “setValue(…)” we decide to create our API the other way around. By just annotating this parameter with @Value we change the second parameter into the value parameter. We are going to come back to writing projections when we explain the creation of complete documents in the fourth part of this series

In the third part of this series, we are going to illuminate the usage of XMLBeam with name spaces, show the configuration abilities of the projector, and explain how to store the XPath expressions apart from the Java code.

About the author

Sven Ewald (Twitter handle:@Cfx) is the author of the library XMLBeam (@XMLBeam). He has been creating Java solutions for 15 years with no end in sight. Currently he works in the field of domain-specific languages for the automotive industry

Links & Literature

[1] http://xmlbeam.org

[2] http://docs.oracle.com/javase/7/docs/api/java/text/MessageFormat.html

[3] http://www.w3schools.com/dom/prop_attr_baseuri.asp

Author
Comments
comments powered by Disqus