This is the fourth of four articles describing the ServingXML pipeline language.
This article looks at ServingXML
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 ServingXML
container does not instantiate
components directly, but rather supports an extensible component assembler module
that allows user defined components to be injected into the container.
The ServingXML
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:
Service service = (Service)resources.lookupServiceComponent(Service.class, "books");
The example below shows a resources script that declares some configuration and service
components, in particular, the sx:xsltConfiguration
configuration component
and the sx:service
service component named "books".
<sx:resources xmlns:sx="http://www.servingxml.com/core"> <!-- A configuration component --> <sx:xsltConfiguration> <sx:systemProperty name="javax.xml.transform.TransformerFactory" value="com.icl.saxon.TransformerFactoryImpl"/> <sx:outputProperty name="indent" value="no"/> <sx:outputProperty name="{http://icl.com/saxon}omit-meta-tag" value="yes"/> </sx:xsltConfiguration> <!-- A service component --> <sx:service id="books"> <sx:serialize> <sx:transform> <sx:content ref="style1"/> </sx:transform> </sx:serialize> </sx:service> <sx:xslt id="style1"><sx:urlSource url="styles/catalog.xsl"/></sx:xslt> <sx:xslt id="style2"><sx:urlSource url="styles/brochure.xsl"/></sx:xslt> ...
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.
<ioc:components xmlns:ioc="http://www.servingxml.com/ioc" xmlns:sx="http://www.servingxml.com/core"> <!-- Configuration Components --> <ioc:configurationComponent name="sx:xsltConfiguration" assemblerClass="com.servingxml.components.xsltconfig.XsltConfigurationAssembler"/> <!-- Abstract Components --> <ioc:abstractComponent name="sx:content" interface="com.servingxml.app.xmlpipeline.Content"/> <ioc:abstractComponent name="sx:stringable" interface="com.servingxml.components.string.StringFactory"/> <ioc:abstractComponent name="sx:task" interface="com.servingxml.components.task.Task"/> <!-- Service Components --> <ioc:serviceComponent name="sx:outputProperty" assemblerClass="com.servingxml.components.property.OutputPropertyFactoryAssembler"/> <ioc:serviceComponent name="sx:serialize" base="sx:task sx:stringable" assemblerClass="com.servingxml.components.transform.SerializedContentAssembler"/> <ioc:serviceComponent name="sx:xslt" base="sx:content" assemblerClass="com.servingxml.components.xslt.XsltFilterFactoryAssembler"/> <ioc:serviceComponent name="sx:systemProperty" assemblerClass="com.servingxml.components.property.SystemPropertyAssembler"/> <ioc:serviceComponent name="sx:transform" interface="com.servingxml.app.xmlpipeline.XmlPipelineFactory" assemblerClass="com.servingxml.components.transform.TransformedContentAssembler"/> ...
The code fragment below shows the injectComponent
methods on the serialize assembler that receive configuration and
service component instances. It is the IoC container's responsibility to call these
methods.
public class SerializedContentAssembler ... public void injectComponent(XsltConfiguration jaxpConfig) { this.jaxpConfig = jaxpConfig; } public void injectComponent(XmlFilterAppender 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 SerializedContentAssembler ... public Task assemble(ConfigurationContext context) { ... // Instantiate and return task using the configuration and service // component instances injected into the component.
The ServingXML distribution includes a template extension package servingxmlx
under the
extensions
directory. In the servingxmlx
directory you will find
the following files and directories.
You can place any extension to the system in the servingxmlx
package.
Alternatively, you can create a custom package name, using the servingxmlx
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
"ServingXML-Components" tells the ServingXML
implementation where to find the
components.
To build your package and make it part of the ServingXML
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
.
<sx:resources xmlns:sx="http://www.servingxml.com/core"> <sx:service id="myService"> <sx:transform> <sx:document><sx:urlSource url="document.xml"/></sx:document> <sx:xslt><sx:urlSource url="style.xsl"/></sx:xslt> <sx:xsltSerializer> <sx:fileSink file="output.xml"/> </sx:xsltSerializer> </sx:transform> </sx:service> </sx: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 sx:streamSink
family
is currently allowed. We want to write something like this.
<sx:resources xmlns:sx="http://www.servingxml.com/core" xmlns:sxx="servingxmlx"> ... <sx:service id="service"> <sx:serialize> <sx:xsltSerializer> <sxx:ftpSink remoteFile="output.xml"> <sxx:ftpClient host="xxx" user="xxx" password="xxx"/> </sxx:ftpSink> <sx:xsltSerializer>
Note that we've put our proposed new elements ftpSink
and ftpClient
in a separate namespace, to distinguish them
from the built-in ServingXML
elements.
We'll start by examining the framework components.xml
file,
which is where the sx:fileSink
element is registered. We see that it has an assembler class that
creates StreamSinkFactory
instances.
<ioc:components xmlns:ioc="http://www.servingxml.com/ioc" xmlns:sx="http://www.servingxml.com/core"> ... <ioc:abstractComponent name="sx:streamSink" interface="com.servingxml.components.streamsink.StreamSinkFactory"/> <ioc:serviceComponent name="sx:fileSink" base="sx:streamSink" assemblerClass="com.servingxml.components.streamsink.file.FileSinkFactoryAssembler"/>
The StreamSinkFactory
interface is defined as follows.
public interface StreamSinkFactory { StreamSink createStreamSink(ServiceContext context, Flow flow) ; }
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
.
ServingXML
container, which we'll do
by adding some entries to the local components.xml
file for the
servingxmlx
extension.
<ioc:components xmlns:ioc="http://www.servingxml.com/ioc" xmlns:sx="http://www.servingxml.com/core" xmlns:sxx="servingxmlx">... <ioc:serviceComponent name="sxx:ftpSink" base="sx:streamSink" assemblerClass="servingxmlx.components.streamsink.edtftpj.FtpSinkFactoryAssembler"/> <ioc:serviceComponent name="sxx:ftpClient" interface="servingxmlx.components.streamsink.FtpClient" assemblerClass="servingxmlx.components.connect.edtftpj.FtpClientAssembler"/>
We need an assembler class for building a StreamSinkFactory
.
public class FtpSinkFactoryAssembler ... public StreamSinkFactory assemble(ConfigurationContext context) { return new FtpSinkFactory(ftpClient,remoteFile); } }
We define setter methods to allow the ServingXML
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 injectComponent
method to allow the ServingXML
container
to inject an FtpClient
object into the builder object.
public class FtpSinkFactoryAssembler ... public void injectComponent(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) { 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.
<sx:resources xmlns:sx="http://www.servingxml.com/core" xmlns:sxx="servingxmlx"> <sxx:ftpClient name="myFtpClient" host="xxx" user="xxx" password="xxx"/> <sx:service id="service"> <sx:serialize> <sx:xsltSerializer> <sx:streamSink ref="myFtpSink"/> </sx:xsltSerializer> <sx:transform> <sx:document><sx:urlSource url="document.xml"/></sx:document> <sx:xslt><sx:urlSource url="style.xsl"/></sx:xslt> </sx:transform> </sx:serialize> </sx:service> <sxx:ftpSink name="myFtpSink" remoteFile="output.xml"> <sxx:ftpClient ref="myFtpClient"/> </sxx:ftpSink> </sx:resources>
Note that the ServingXML
container is responsible for resolving dependencies,
so we don't need to worry about the precise order of the sxx:ftpSink
and
sxx: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 QualifiedName("myFtpClient"); Name myFtpSink = new QualifiedName("myFtpSink"); IocContainer resources = appContext.getResources(); FtpClient ftpClient = (FtpClient)resources.lookupResource(FtpClient.class,myFtpClient); StreamSinkFactory streamSinkFactory = (StreamSinkFactory)resources.lookupResource( StreamSinkFactory.class,myFtpSink);