8/25/2008

Using XInclude with Spring extensible XML authoring

When you use Spring as one of the possible containers which your Java application or your own framework should run on, you will possibly take in account the Spring feature called "extensible XML authoring" (refer to http://static.springframework.org/spring/docs/2.5.x/reference/extensible-xml.html for details).

With this very nice thing, you can embed your own XML stuff into the Spring bean definition so it will be up to you, to read out and to wire all those components with the container even based on your own XML stuff.

Without going deeper with the basic feature which is very well documented on the page behind the link, I would like to show how you can use XInclude to comfortably separate your own XML stuff from the Spring bean definition on the file base, so you can later reuse the file containing your own XML completely independently from Spring. So, the separation of concerns and reusability as well as technology independency keep on living.

Lets assume, you already have defined your XML schema having smth. like <myns:mytag> as the topmost XML element. In the "classic" Spring extensible XML authoring, you will probably have your bean definition in a file like that:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/
spring-beans-2.0.xsd
http://mycompany/myschema myschema.xsd"
xmlns:myns="http://mycompany/myschema">

<myns:mytag>
...
</myns:mytag>

</beans>

Having a bean definition like this constrains you to embed your own XML tags directly into it and to rewrite it again whenever it's used outside Spring. How can you keep your XML code reusable so it can be just referenced by the bean definition instead of beeing embedded?

The solutions comes with the XInclude specification. Using it, you can create such a reference. Take a look at the resulting bean definition file assuming we separated our own XML completely to a file called myxml.xml being completely valid according to our schema (keep in mind that we reference schemas either via their official URL or expect them to be in the same folder as a file for this example):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/
spring-beans-2.0.xsd
http://mycompany/myschema myschema.xsd
http://www.w3.org/2001/XInclude XInclude.xsd"
xmlns:xi="http://www.w3.org/2001/XInclude"
xmlns:myns="http://mycompany/myschema">

<xi:include href="myxml.xml"/>


</beans>

It now looks very friendly, doesn't it? Concerns have been separated completely and we didn't mix those XMLs at all! But...

The problem is that Spring doesn't support XInclude through its default implementation. In the depth of its code it creates an object of the javax.xml.parsers.DocumentBuilderFactory with the default XInclude awareness set to false (unset at all). That means, the XInclude reference doesn't get resolved and an exception gets thrown.

But as with all Spring components, there is a quite tricky but more or less elegant way to solve this issue.

First, we subclass the FileSystemXmlApplicationContext in order to override the protected method "loadBeanDefinitions":

public class MyFileSystemXmlApplicationContext extends FileSystemXmlApplicationContext {

public MyFileSystemXmlApplicationContext(String path) {
super(path);
}

@Override
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)
throws IOException {
// Create a new XmlBeanDefinitionReader for the given BeanFactory.
XmlBeanDefinitionReader beanDefinitionReader =
new XmlBeanDefinitionReader(beanFactory);

//use our own DocumentLoader here
DocumentLoader documentLoader = new MyDocumentLoader();

beanDefinitionReader.setDocumentLoader(documentLoader);

// Configure the bean definition reader with this context's
// resource loading environment.
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver( new ResourceEntityResolver(this));

// Allow a subclass to provide custom initialization of the reader,
// then proceed with actually loading the bean definitions.
initBeanDefinitionReader(beanDefinitionReader);
loadBeanDefinitions(beanDefinitionReader);
}
}

Relevant code modifications are highlighted red. Sure we now need the new class, MyDocumentLoader.

public class MyDocumentLoader extends DefaultDocumentLoader {
@Override
protected DocumentBuilderFactory createDocumentBuilderFactory(
int validationMode,
boolean namespaceAware)
throws ParserConfigurationException {

//let the parent to the job
DocumentBuilderFactory factory =
super.createDocumentBuilderFactory(validationMode, namespaceAware);

//now set all necessary Xerces settings
factory.setXIncludeAware(true);
factory.setNamespaceAware(true);
factory.setFeature(
"http://apache.org/xml/features/xinclude/fixup-base-uris", false);

return factory;
}
}

We override the method createDocumentBuilderFactory and set the XInclude awareness to true as well as the Namespace awareness. That's it. The feature being set at the end of the method is required for Xerces (default XML parser brought with the JDK) to avoid fixing base URL - that "feature" is well documented on the web - just google for it for further details.

From here you only need to use your new application context class to be able to reference per XInclude. For the ContextListener and the general extensible XML authoring stuff follow standard documentation and further subclassing.

No comments: