This is the third of three articles describing the Serving XML pipeline language.
This article looks at Serving XML
as an "inversion of control" (IoC) container
that allows us to assemble components from a variety of projects - the Apache FOP project,
the Sun MSV project and others - and make them work together to process records and XML.
The term "inversion" conveys the idea that the Serving XML
container does not instantiate
components directly, but rather supports an extendible component assembler module
that allows user defined components to be injected into the container.
The Serving XML
container supports two types of components:
The difference between the two is in how they are identified.
In Java code, we can see the difference in how we look up component instances in the IoC container. This is how we look up a configuration component, passing only an object type to the lookup method:
XsltConfiguration config = (XsltConfiguration)resources.lookupConfigurationComponent(JaxpConfiguration.class);
This is how we look up a service component, passing both an object type and an instance name to the lookup method:
Name name = new NameImpl("books"); Service service = (Service)resources.lookupServiceComponent(Service.class,name);
The example below shows a resources script that declares some configuration and service
components, in particular, the px:xsltConfiguration
configuration component
and the px:service
service component named "books".
<px:resources xmlns:px="http://www.presentingxml.com/PresentingXML"> <!-- A configuration component --> <px:xsltConfiguration> <px:systemProperty key="javax.xml.transform.TransformerFactory" value="com.icl.saxon.TransformerFactoryImpl"/> <px:outputProperty key="indent" value="no"/> <px:outputProperty key="{http://icl.com/saxon}omit-meta-tag" value="yes"/> </px:xsltConfiguration> <!-- A service component --> <px:service name="books"> <px:serialize> <px:transform> <px:filter ref="style1"/> </px:transform> </px:serialize> </px:service> <px:style name="style1"><px:urlSource url="styles/catalog.xsl"/></px:style> <px:style name="style2"><px:urlSource url="styles/brochure.xsl"/></px:style> ...
The components declared in a resources script are assembled by the IoC container, using assemblers that are defined in a components file. The assemblers for the components shown above are defined as follows.
<pxi:components xmlns:pxi="http://www.presentingxml.com/PresentingXML/IoC" xmlns:px="http://www.presentingxml.com/PresentingXML"> <!-- Configuration Components --> <pxi:configurationComponent name="px:xsltConfiguration" assemblerClass="com.presentingxml.components.transformer.XsltConfigurationAssembler"/> <!-- Abstract Components --> <pxi:abstractComponent name="px:filter" interface="com.presentingxml.components.xmlpipeline.filter.XmlFilterFactory"/> <pxi:abstractComponent name="px:string" interface="com.presentingxml.components.string.StringFactory"/> <pxi:abstractComponent name="px:task" interface="com.presentingxml.components.task.Task"/> <!-- Service Components --> <pxi:serviceComponent name="px:outputProperty" assemblerClass="com.presentingxml.components.transformer.OutputPropertyAssembler"/> <pxi:serviceComponent name="px:serialize" base="px:task px:string" assemblerClass="com.presentingxml.components.xmlpipeline.filter.SerializePipelineAppenderAssembler"/> <pxi:serviceComponent name="px:style" base="px:filter" assemblerClass="com.presentingxml.components.xmlpipeline.filter.style.StyleFilterFactoryAssembler"/> <pxi:serviceComponent name="px:systemProperty" assemblerClass="com.presentingxml.components.systemproperty.SystemPropertyAssembler"/> <pxi:serviceComponent name="px:transform" interface="com.presentingxml.components.xmlpipeline.PipelineFactory" assemblerClass="com.presentingxml.components.xmlpipeline.filter.TransformPipelineAppenderAssembler"/> ...
The code fragment below shows the injectConfigurationComponent
and
injectServiceComponent
methods on the serialize task assembler that receive configuration and
service component instances. It is the IoC container's responsibility to call these
methods.
public class SerializePipelineAppenderAssembler ... public void injectConfigurationComponent(XsltConfiguration jaxpConfig) { this.jaxpConfig = jaxpConfig; } public void injectServiceComponent(PipelineAppender pipelineFactory) { this.pipelineFactory = pipelineFactory; }
Each assembler has a method assemble
that the IoC container, after it calls the
injectXXX
methods, calls to instantiate the component.
public class SerializePipelineAppenderAssembler ... public Task assemble(ConfigurationContext context) throws PresentingXmlException { ... // Instantiate and return task using the configuration and service // component instances injected into the component.
The Serving XML distribution includes a template extension package presentingxmlx
under the
extensions
directory. In the presentingxmlx
directory you will find
the following files and directories.
You can place any extension to the system in the presentingxmlx
package.
Alternatively, you can create a custom package name, using the presentingxmlx
package as a template, and place your extension in your own package. If you create a custom
package name, you will also need to go to the root directory of the servingxml
download
and add an entry to the build-extensions.xml
file.
The components supplied by an extension are registered in the extension's components.xml
file. This file is included in the component's jar file. An entry in the jar's manifest named
"PresentingXML-Components" tells the Serving XML framework where to find the
components.
To build your package and make it part of the Serving XML
distribution
build-extensions.xml
file to include your extension in the main build.
In the example below, we show a resources script that transforms a document with a style sheet
and writes the result to the local file output.xml
.
<px:resources xmlns:px="http://www.presentingxml.com/PresentingXML"> <px:service name="myService"> <px:serialize> <px:xmlEmitter> <px:fileSink file="output.xml"/> </xmlEmitter> <px:transform> <px:document><px:urlSource url="document.xml"/></px:document> <px:style><px:urlSource url="style.xsl"/></px:style> </px:transform> </px:serialize> </px:service> </px:resources>
Now, suppose that instead of writing the output to a local file, we want to send it
to a remote host using our favourite third party FTP tool. In particular, we want
to define a new element, say ftpSink
, that we can use anywhere
in a resources script where an element of the px:streamSink
family
is currently allowed. We want to write something like this.
<px:resources xmlns:px="http://www.presentingxml.com/PresentingXML" xmlns:pxx="presentingxmlx"> ... <px:service name="service"> <px:serialize> <px:xmlEmitter> <pxx:ftpSink remoteFile="output.xml"> <pxx:ftpClient host="xxx" user="xxx" password="xxx"/> </pxx:ftpSink> <px:xmlEmitter>
Note that we've put our proposed new elements ftpSink
and ftpClient
in a separate namespace, to distinguish them
from the built-in Serving XML
elements.
We'll start by examining the framework components.xml
file,
which is where the px:fileSink
element is registered. We see that it has an assembler class that
creates StreamSinkFactory
instances.
<pxi:components xmlns:pxi="http://www.presentingxml.com/PresentingXML/IoC" xmlns:px="http://www.presentingxml.com/PresentingXML"> ... <pxi:abstractComponent name="px:streamSink" interface="com.presentingxml.components.streamsink.StreamSinkFactory"/> <pxi:serviceComponent name="px:fileSink" base="px:streamSink" assemblerClass="com.presentingxml.components.streamsink.file.FileSinkFactoryAssembler"/>
The StreamSinkFactory
interface is defined as follows.
public interface StreamSinkFactory { StreamSink createStreamSink(ServiceContext context, Record parameters, Record contextRecord) throws PresentingXmlException; }
public interface StreamSink { OutputStream openStream() throws IOException; void closeStream(OutputStream os) throws IOException; }
We'll need StreamSinkFactory
and StreamSink
implementations.
Ultimatelty, the framework will call openStream
on a StreamSink
object,
and we'll want the content that is written to the output stream to be sent to a file on a remote host.
There are different ways of implementing this, for example, we could stream the content to the remote
file as it is written, or alternatively we could stream it to a local file, and then send the local file
to the remote host when the framework calls closeStream
.
Serving XML
container, which we'll do
by adding some entries to the local components.xml
file for the
presentingxmlx
extension.
<pxi:components xmlns:pxi="http://www.presentingxml.com/PresentingXML/IoC" xmlns:px="http://www.presentingxml.com/PresentingXML" xmlns:pxx="presentingxmlx">... <pxi:serviceComponent name="pxx:ftpSink" base="px:streamSink" assemblerClass="presentingxmlx.components.streamsink.edtftpj.FtpSinkFactoryAssembler"/> <pxi:serviceComponent name="pxx:ftpClient" interface="presentingxmlx.components.streamsink.FtpClient" assemblerClass="presentingxmlx.components.connect.edtftpj.FtpClientAssembler"/>
We need an assembler class for building a StreamSinkFactory
.
public class FtpSinkFactoryAssembler ... public StreamSinkFactory assemble(ConfigurationContext context) throws PresentingXmlException { return new FtpSinkFactory(ftpClient,remoteFile); } }
We define setter methods to allow the Serving XML
container to inject the attribute values
of the edt:ftpSink
element into the builder object.
public class FtpSinkFactoryAssembler ... public void setRemoteFile(String remoteFile) { this.remoteFile = remoteFile; }
We define an injectServiceComponent
method to allow the Serving XML
container
to inject an FtpClient
object into the builder object.
public class FtpSinkFactoryAssembler ... public void injectServiceComponent(FtpClient ftpClient) { this.ftpClient = ftpClient; }
We need another builder for the ftpClient. This builder class only requires setter methods.
public class FtpClientAssembler ... private int port = 21; public void setHost(String host) { this.host = host; } public void setPort(int port) { this.port = port; } public void setUser(String user) { this.user = user; } public void setPassword(String password) { this.password = password; } public FtpClient assemble(ConfigurationContext context) throws PresentingXmlException { return new FtpClient(host,port,user,password); } }
We can now rewite the resources script as proposed above, or alternatively, we can rewirte it using references like this.
<px:resources xmlns:px="http://www.presentingxml.com/PresentingXML" xmlns:pxx="presentingxmlx"> <pxx:ftpClient name="myFtpClient" host="xxx" user="xxx" password="xxx"/> <px:service name="service"> <px:serialize> <px:xmlEmitter> <px:streamSink ref="myFtpSink"/> </px:xmlEmitter> <px:transform> <px:document><px:urlSource url="document.xml"/></px:document> <px:style><px:urlSource url="style.xsl"/></px:style> </px:transform> </px:serialize> </px:service> <pxx:ftpSink name="myFtpSink" remoteFile="output.xml"> <pxx:ftpClient ref="myFtpClient"/> </pxx:ftpSink> </px:resources>
Note that the Serving XML
container is responsible for resolving dependencies,
so we don't need to worry about the precise order of the pxx:ftpSink
and
pxx:ftpClient
elements, or references to them, in the script.
If we wanted to access the objects we've just defined in Java code, we could do so as follows.
Name myFtpClient = new NameImpl("myFtpClient"); Name myFtpSink = new NameImpl("myFtpSink"); ResourceContainer resources = appContext.getResources(); FtpClient ftpClient = (FtpClient)resources.lookupResource(FtpClient.class,myFtpClient); StreamSinkFactory streamSinkFactory = (StreamSinkFactory)resources.lookupResource( StreamSinkFactory.class,myFtpSink);