Chapter 14. Extending Smooks


14.1. APIs in Smooks

APIs

All existing Smooks functionality (Java Binding, EDI processing etc) is built through the extension of a number of well defined APIs.

The main extension points/APIs in Smooks are Reader and Visitor APIs:
Reader APIs
Those for processing Source/Input data (Readers) so as to make it consumable by other Smooks components as a series of well defined hierarchical events (based on the SAX event model) for all of the message fragments and sub-fragments.
Visitor APIs
Those for consuming the message fragment SAX Events produced by a Source/Input Reader.

14.2. Configuring Smooks Components

All Smooks components are configured in exactly the same way. When using the Smooks Core code, all Smooks components are resources which are configured using a SmooksResourceConfiguration instance.

14.3. Namespace-specific Configurations

Smooks provides mechanisms for constructing namespace (XSD) specific XML configurations for components. The most basic configuration (and the one that maps directly to the SmooksResourceConfiguration class) is the basic <resource-config> XML configuration from the base configuration namespace (http://www.milyn.org/xsd/smooks-1.1.xsd).

14.4. Namespace-specific Configuration Example

<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd">

    <resource-config selector="">
        <resource></resource>
        <param name=""></param>
    </resource-config>

</smooks-resource-list>
  • The selector attribute is the mechanism by which the resource is "selected" (for example, it can be an XPath for a Visitor implementation).
  • The resource element is the actual resource. This can be a Java Class name or some other form of resource such as a template. The resource is assumed to be a Java class name for the remainder for this section.
  • The param elements are configuration parameters for the resource defined in the resource element.

14.5. Runtime Representation

Smooks takes care of all the details of creating the runtime representation of the resource (for example, constructing the class named in the the resource element) and injects all the configuration parameters. It also works out what the resource type is, and from that, how to interpret things like the selector. (For example, if the resource is a Visitor instance, it knows the selector is an XPath, selecting a Source message fragment.)

14.6. Configuration Annotations

After your component has been created, you need to configure it with the <param> element details. This is done using the @ConfigParam and @Config annotations.

14.7. The @ConfigParam Annotation

The @ConfigParam annotation reflectively injects the named parameter from the <param> elements that have the same name as the annotated property itself. The name can be different but the default behavior matches against the name of the component property.

14.8. @ConficParam Benefits

This annotation eliminates excess code from your component because it:
  • Handles decoding of the <param> value before setting it on the annotated component property. Smooks provides DataDecoders for all of the main types (int, Double, File, Enums etc), but you can implement and use a custom DataDecoder where the out of the box decoders don't cover specific decoding requirements (for example, @ConfigParam(decoder = MyQuirkyDataDecoder.class)). Smooks will automatically use your custom decoder (that is, you won't need to define the decoder property on this annotation) if it is registered. See the DataDecoder Javadocs for details on registering a DataDecoder implementation such that Smooks will automatically locate it for decoding a specific data type.
  • Supports a choice constraint for the config property, generating a configuration exception where the configured value is not one of the defined choice values. For example, you may have a property which has a constrained value set of ON and OFF. You can use the choice property on this annotation to constrain the config, raise exceptions, and so on. (For example, @ConfigParam(choice = {"ON", "OFF"}).)
  • Can specify default config values e.g. @ConfigParam(defaultVal = "true").
  • Can specify whether or not the property config value is required or optional e.g. @ConfigParam(use = Use.OPTIONAL). By default, all properties are REQUIRED, but setting a defaultVal implicitly marks the property as being OPTIONAL.

14.9. Using the @ConfigParam Annotation

This example show the annotated component DataSeeder and its corresponding Smooks configuration:
public class DataSeeder 
{
   @ConfigParam
   private File seedDataFile;

   public File getSeedDataFile() 
   {
      return seedDataFile;
   }

   // etc...
}
<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd">
   <resource-config selector="dataSeeder">
      <resource>com.acme.DataSeeder</resource>
         <param name="seedDataFile">./seedData.xml</param>
      </resource-config>
</smooks-resource-list>

14.10. The @Config Annotation

The @Config annotation reflectively injects the full SmooksResourceConfiguration instance, associated with the component resource, onto the annotated component property. An error will result if this annotation is added to a component property that is not of type SmooksResourceConfiguration.

14.11. Using the @Config Annotation

public class MySmooksComponent 
{
    @Config
    private SmooksResourceConfiguration config;

    // etc...

14.12. @Initialize and @Uninitialize

Sometimes your component needs more involved configuration for which we need to write some "initialization" code. For this, Smooks provides the @Initialize annotation.
Likewise, there are times when you need to undo work performed during initialization when the associated Smooks instance is being discarded (garbage collected) e.g. to release some resources acquired during initialization. For this, Smooks provides the @Uninitialize annotation.

14.13. A Basic Initialization/Un-initialization Sequence

This is a basic initialization/un-initialization sequence:
smooks = new Smooks(..);

// Initialize all annotated components
@Initialize

// Use the smooks instance through a series of filterSource invocations...
smooks.filterSource(...);
smooks.filterSource(...);
smooks.filterSource(...);
... etc ...

smooks.close();

// Uninitialize all annotated components
@Uninitialize

14.14. Using @Initialize and @Uninitialize

Overview

In this example, assume we have a component that opens multiple connections to a database on initialization and then needs to release all those database resources when we close the Smooks instance.

public class MultiDataSourceAccessor 
{
    @ConfigParam
    private File dataSourceConfig;

    Map<String, Datasource> datasources = new HashMap<String, Datasource>();

    @Initialize
    public void createDataSources() 
    {
        // Add DS creation code here....
        // Read the dataSourceConfig property to read the DS configs...
    }

    @Uninitialize
    public void releaseDataSources() 
    {
        // Add DS release code here....
    }

    // etc...
}
When using the @Initialize and @Uninitialize annotations above, the following should be noted:
  • The @Initialize and @Uninitialize methods must be public, zero-arg methods.
  • The @ConfigParam properties are all initialized before the first @Initialize method is called. Therefore, you can use the @ConfigParam component properties as input to the initialization process.
  • The @Uninitialize methods are all called in response to a call to the Smooks.close method.

14.15. Defining Custom Configuration Namespaces

Smooks supports a mechanism for defining custom configuration namespaces for components. This allows you to support custom, XSD-based, configurations for your components that can be validated instead of treating them all as generic Smooks resources using the <resource-config> base configuration.

14.16. Using Custom Configuration Namespaces

The basic process involves two steps:
  1. Writing an configuration XSD for your component that extends the base http://www.milyn.org/xsd/smooks-1.1.xsd configuration namespace. This XSD must be supplied on the classpath with your component. It must be located in the /META-INF/ folder and have the same path as the namespace URI. For example, if your extended namespace URI is http://www.acme.com/schemas/smooks/acme-core-1.0.xsd, then the physical XSD file must be supplied on the class-path in /META-INF/schemas/smooks/acme-core-1.0.xsd.
  2. Writing a Smooks configuration namespace mapping configuration file that maps the custom name-space configuration into a SmooksResourceConfiguration instance. This file must be named (by convention) based on the name of the name-space it is mapping and must be physically located on the class-path in the same folder as the XSD. Extending the above example, the Smooks mapping file would be /META-INF/schemas/smooks/acme-core-1.0.xsd-smooks.xml. Note the -smooks.xml postfix.

Note

The easiest way to get familiar with this mechanism is by looking at existing extended namespace configurations within the Smooks code itself. All Smooks components (including the Java Binding functionality) use this mechanism for defining their configurations. Smooks Core itself defines a number of extended configuration namespaces.

14.17. Implementing a Source Reader

You can implement a source reader for your custom data format which immediately opens all Smooks capabilities to that data format such as Java Binding, Templating, Persistence, Validation, Splitting, Routing etc. The only Smooks requirement is that the Reader implements the standard org.xml.sax.XMLReader interface from the Java JDK. However, if you want to be able to configure the Reader implementation, it needs to implement the org.milyn.xml.SmooksXMLReader interface. org.milyn.xml.SmooksXMLReader is an extension of org.xml.sax.XMLReader. You can easily use an existing org.xml.sax.XMLReader implementation, or implement a new one.

14.18. Implementing a Source Reader for use with Smooks

  1. You should first implement a basic reader class as shown below:
     public class MyCSVReader implements SmooksXMLReader 
    {
       // Implement all of the XMLReader methods...
    }
    
    Two methods from the org.xml.sax.XMLReader interface are of particular interest:
    1. setContentHandler(ContentHandler) is called by Smooks Core. It sets the org.xml.sax.ContentHandler instance for the reader. The org.xml.sax.ContentHandler instance methods are called from inside the parse(InputSource) method.
    2. parse(InputSource) : This is the method that receives the Source data input stream, parses it (i.e. in the case of this example, the CSV stream) and generates the SAX event stream through calls to the org.xml.sax.ContentHandler instance supplied in the setContentHandler(ContentHandler) method.
  2. Configure your CSV reader with the names of the fields associated with the CSV records. Configuring a custom reader implementation is the same for any Smooks component. See the example below:
    public class MyCSVReader implements SmooksXMLReader 
    {
    
        private ContentHandler contentHandler;
    
        @ConfigParam
        private String[] fields; // Auto decoded and injected from the "fields" <param> on the reader config.
    
        public void setContentHandler(ContentHandler contentHandler) {
            this.contentHandler = contentHandler;
        }
    
        public void parse(InputSource csvInputSource) throws IOException, SAXException {
            // TODO: Implement parsing of CSV Stream...
        }
    
        // Other XMLReader methods...
    }
    
  3. Now that you have the basic Reader implementation stub, you can start writing unit tests to test the new reader implementation. To do this you will need something with CSV input. Observe the example below featuring a simple list of names in a file with the name names.csv:
    Tom,Jones
    Mike,Jones
    Mark,Jones
  4. Use a test Smooks configuration to configure Smooks with your MyCSVReader. As stated before, everything in Smooks is a resource and can be configured with the basic <resource-config> configuration. While this works fine, it's a little noisy, so Smooks provides a basic <reader> configuration element specifically for the purpose of configuring a reader. The configuration for the test looks like the following, in the mycsvread-config.xml:
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd">
       <reader class="com.acme.MyCSVReader">
          <params>
             <param name="fields">firstname,lastname</param>
          </params>
       </reader>
    </smooks-resource-list>
    
  5. Implement the JUnit test class:
    public class MyCSVReaderTest extends TestCase 
    {
        
        public void test() {
            Smooks smooks = new Smooks(getClass().getResourceAsStream("mycsvread-config.xml"));
            StringResult serializedCSVEvents = new StringResult();
    
            smooks.filterSource(new StreamSource(getClass().getResourceAsStream("names.csv")), serializedCSVEvents);
    
            System.out.println(serializedCSVEvents);
    
            // TODO: add assertions etc
        }
    }
    
  6. Implement the parse method:
    public class MyCSVReader implements SmooksXMLReader 
    {
        private ContentHandler contentHandler;
    
        @ConfigParam
        private String[] fields; // Auto decoded and injected from the "fields" <param> on the reader config.
    
        public void setContentHandler(ContentHandler contentHandler) 
        {
            this.contentHandler = contentHandler;
        }
    
        public void parse(InputSource csvInputSource) throws IOException, SAXException 
        {
            BufferedReader csvRecordReader = new BufferedReader(csvInputSource.getCharacterStream());
            String csvRecord;
    
            // Send the start of message events to the handler...
            contentHandler.startDocument();
            contentHandler.startElement(XMLConstants.NULL_NS_URI, "message-root", "", new AttributesImpl());
    
            csvRecord = csvRecordReader.readLine();
            while(csvRecord != null) 
            {
                String[] fieldValues = csvRecord.split(",");
            
                // perform checks...
    
                // Send the events for this record...
                contentHandler.startElement(XMLConstants.NULL_NS_URI, "record", "", new AttributesImpl());
                for(int i = 0; i < fields.length; i++) 
                {
                    contentHandler.startElement(XMLConstants.NULL_NS_URI, fields[i], "", new AttributesImpl());
                    contentHandler.characters(fieldValues[i].toCharArray(), 0, fieldValues[i].length());
                    contentHandler.endElement(XMLConstants.NULL_NS_URI, fields[i], "");            
                }
                contentHandler.endElement(XMLConstants.NULL_NS_URI, "record", "");            
    
                csvRecord = csvRecordReader.readLine();    
            }
    
            // Send the end of message events to the handler...
            contentHandler.endElement(XMLConstants.NULL_NS_URI, "message-root", "");
            contentHandler.endDocument();
        }
    
        // Other XMLReader methods...
    }
    
  7. Run the unit test class to see the following output on the console (formatted):
    <message-root>
        <record>
            <firstname>Tom</firstname>
            <lastname>Jones</lastname>
        </record>
        <record>
            <firstname>Mike</firstname>
            <lastname>Jones</lastname>
        </record>
        <record>
            <firstname>Mark</firstname>
            <lastname>Jones</lastname>
        </record>
    </message-root>
    
    After this, it is a case of expanding the tests, hardening the reader implementation code, and so on. Then you can use your reader to perform all sorts of operations supported by Smooks.

14.19. Configuring the Reader with java-binding-config.xml Example

The following configuration (java-binding-config.xml) can be used to bind the names into a List of PersonName objects:
<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:jb="http://www.milyn.org/xsd/smooks/javabean-1.4.xsd">
    <reader class="com.acme.MyCSVReader">
        <params>
         <param name="fields">firstname,lastname</param>
        </params>
    </reader>
    <jb:bean beanId="peopleNames" class="java.util.ArrayList" createOnElement="message-root">
        <jb:wiring beanIdRef="personName" />
    </jb:bean>
    <jb:bean beanId="personName" class="com.acme.PersonName" createOnElement="message-root/record">
        <jb:value property="first" data="record/firstname" />
        <jb:value property="last" data="record/lastname" />
    </jb:bean>
</smooks-resource-list>
Here is a test for that configuration:
public class MyCSVReaderTest extends TestCase 
{
    public void test_java_binding() 
    {
        Smooks smooks = new Smooks(getClass().getResourceAsStream("java-binding-config.xml"));
        JavaResult javaResult = new JavaResult();

        smooks.filterSource(new StreamSource(getClass().getResourceAsStream("names.csv")), javaResult);

        List<PersonName> peopleNames = (List<PersonName>) javaResult.getBean("peopleNames");

        // TODO: add assertions etc
    }
}

14.20. Tips for Using a Reader

  • Reader instances are never used concurrently. Smooks Core will create a new instance for every message, or, will pool and reuse instances as per the readerPoolSizeFilterSettings property.
  • If your Reader requires access to the Smooks ExecutionContext for the current filtering context, your Reader needs to implement the org.milyn.xml.SmooksXMLReader interface.
  • If your Source data is a binary data stream your Reader must implement the org.milyn.delivery.StreamReader interface.
  • You can configure your reader within your source code (e.g. in your unit tests) using a GenericReaderConfigurator instance, which you then set on the Smooks instance.
  • While the basic <reader> configuration is fine, it is possible to define a custom configuration namespace (XSD) for your custom CSV Reader implementation. This topic is not covered here. Review the source code to see the extended configuration namespace for the Reader implementations supplied with Smooks, e.g. the EDIReader, CSVReader, JSONReader etc. From this, you should be able to work out how to do this for your own custom Reader.

14.21. Binary Source Readers

A binary source reader is a reader for a binary data source. Your reader should implement the org.milyn.delivery.StreamReader interface. This is just a marker interface that tells the Smooks runtime to ensure that an InputStream is supplied.
The binary Reader implementation is essentially the same as a non-binary Reader implementation (see above), except that the implementation of the the parse method should use the InputStream from the InputSource (i.e. call InputSource..getByteStream() instead of InputSource.getCharacterStream()) and generate the XML events from the decoded binary data.

14.22. Implementing a Binary Source Reader

  1. To implement a binary source reader, observe the following parse method implementation:
    public static class BinaryFormatXXReader implements SmooksXMLReader, StreamReader 
    {
        @ConfigParam
        private String xProtocolVersion;
    
        @ConfigParam
        private int someOtherXProtocolConfig;
    
        // etc...
    
        public void parse(InputSource inputSource) throws IOException, SAXException {
            // Use the InputStream (binary) on the InputSource...
            InputStream binStream = inputSource.getByteStream();
    
            // Create and configure the data decoder... 
            BinaryFormatXDecoder xDecoder = new BinaryFormatXDecoder();
            xDecoder.setProtocolVersion(xProtocolVersion);
            xDecoder.setSomeOtherXProtocolConfig(someOtherXProtocolConfig);
            xDecoder.setXSource(binStream);
    
            // Generate the XML Events on the contentHandler...
            contentHandler.startDocument();
    
            // Use xDecoder to fire startElement, endElement etc events on the contentHandler (see previous section)...
    
            contentHandler.endDocument();
        }
    
        // etc....
    }
    
  2. Configure the BinaryFormatXXReader reader in your Smooks configuration as you would any other reader:
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd">
    
        <reader class="com.acme.BinaryFormatXXReader">
            <params>
             <param name="xProtocolVersion">2.5.7</param>
             <param name="someOtherXProtocolConfig">1</param>
                ... etc... 
            </params>
        </reader>
    
        ... Other Smooks configurations e.g. <jb:bean> configs for binding the binary data into Java objects... 
    
    </smooks-resource-list>
    
  3. Run the Smooks execution code (note the InputStream supplied to the StreamSource). In this case, two results are generated: XML and Java objects.
    StreamResult xmlResult = new StreamResult(xmlOutWriter);
    JavaResult javaResult = new JavaResult();
    
    InputStream xBinaryInputStream = getXByteStream();
    
    smooks.filterSource(new StreamSource(xBinaryInputStream), xmlResult, javaResult);
    
    // etc... Use the beans in the javaResult...
    

14.23. Visitor Implementations

Visitor implementations are the workhorses of Smooks. Most of the out-of-the-box functionality in Smooks (Java Binding, Templating, Persistence, and so on) was created by using one or more Visitor implementations. Visitor implementations often collaborate through the ExecutionContext and ApplicationContext context objects, accomplishing a common goal by working together.

14.24. Supported Visitor Implementations

  1. SAX-based implementations based on the org.milyn.delivery.sax.SAXVisitor sub-interfaces.
  2. DOM-based implementations based on the org.milyn.delivery.dom.DOMVisitor sub-interfaces.

14.25. SAX and DOM Visitor Implementations

Your implementation can support both SAX and DOM, but Red Hat recommends implementing a SAX only Visitor. SAX-based implementations are usually easier to create and perform faster.

Important

All Visitor implementations are treated as stateless objects. A single Visitor instance must be usable concurrently across multiple messages, that is, across multiple concurrent calls to the Smooks.filterSource method. All state associated with the current Smooks.filterSource execution must be stored in the ExecutionContext.

14.26. The SAX Visitor API

The SAX Visitor API is made up of a number of interfaces. These interfaces are based on the org.xml.sax.ContentHandler SAX events that a SAXVisitor implementation can capture and processes. Depending on the use case being solved with the SAXVisitor implementation, you may need to implement one or all of these interfaces.

14.27. SAX Visitor API Interfaces

org.milyn.delivery.sax.SAXVisitBefore
Captures the startElement SAX event for the targeted fragment element:
public interface SAXVisitBefore extends SAXVisitor 
{
    void visitBefore(SAXElement element, ExecutionContext executionContext) 
                                          throws SmooksException, IOException;
}
org.milyn.delivery.sax.SAXVisitChildren
Captures the character based SAX events for the targeted fragment element, as well as Smooks generated (pseudo) events corresponding to the startElement events of child fragment elements:
public interface SAXVisitChildren extends SAXVisitor 
{
    void onChildText(SAXElement element, SAXText childText, ExecutionContext 
                        executionContext) throws SmooksException, IOException;

    void onChildElement(SAXElement element, SAXElement childElement, 
         ExecutionContext executionContext) throws SmooksException, IOException;
}
org.milyn.delivery.sax.SAXVisitAfter
Captures the endElement SAX event for the targeted fragment element:
 public interface SAXVisitAfter extends SAXVisitor 
{
    void visitAfter(SAXElement element, ExecutionContext executionContext) 
                                          throws SmooksException, IOException;
}

14.28. SAX Visitor API Example

Illustrating API events using XML

This pulls together three interfaces into a single interface in the org.milyn.delivery.sax.SAXElementVisitor interface:

<message>
    <target-fragment>      <!-- SAXVisitBefore.visitBefore -->
        Text!!             <!-- SAXVisitChildren.onChildText -->  
        <child>            <!-- SAXVisitChildren.onChildElement -->
        </child>
    </target-fragment>     <!-- SAXVisitAfter.visitAfter -->
</message>
The above is an illustration of a Source message event stream as XML. It could be EDI, CSV, JSON, or some other format. Consider it to be a Source message event stream, serialized as XML for easy reading.
As can be seen from the above SAX interfaces, the org.milyn.delivery.sax.SAXElement type is passed in all method calls. This object contains details about the targeted fragment element, including attributes and their values. It also contains methods for managing text accumulation, as well as accessing the Writer associated with any StreamResult instance that may have been passed in the Smooks.filterSource(Source, Result) method call.

14.29. Text Accumulation with SAX

SAX is a stream based processing model. It doesn't create a Document Object Model (DOM) or "accumulate" event data in any way. This is why it is a suitable processing model for processing huge message streams.

14.30. org.milyn.delivery.sax.SAXElement

The org.milyn.delivery.sax.SAXElement will always contain attribute data associated with the targeted element, but will not contain the fragment child text data, whose SAX events ( SAXVisitChildren.onChildText ) occur between the SAXVisitBefore.visitBefore and SAXVisitAfter.visitAfter events. The text events are not accumulated on the SAXElement because that could result in a significant performance drain. The downside to this is that if the SAXVisitor implementation needs access to the text content of a fragment, you need to explicitly tell Smooks to accumulate text for the targeted fragment. This is done by calling the SAXElement.accumulateText method from inside the SAXVisitBefore.visitBefore method implementation of your SAXVisitor.

14.31. Text Accumulation Example

public class MyVisitor implements SAXVisitBefore, SAXVisitAfter 
{
   public void visitBefore(SAXElement element, ExecutionContext executionContext)
                                             throws SmooksException, IOException 
   {
      element.accumulateText();
   }

   public void visitAfter(SAXElement element, ExecutionContext executionContext) 
                                             throws SmooksException, IOException 
   {
      String fragmentText = element.getTextContent();

      // ... etc ...
   }
}

14.32. The @TextConsumer Annotation

The @TextConsumer annotation can be used to annotate your SAXVisitor implementation instead of using the SAXVisitBefore.visitBefore method.

14.33. @TextConsumer Example

@TextConsumer
public class MyVisitor implements SAXVisitAfter 
{
   public void visitAfter(SAXElement element, ExecutionContext executionContext) 
                                             throws SmooksException, IOException 
   {
      String fragmentText = element.getTextContent();

        // ... etc ...
    }
}

Note

The entirity of the fragment text will not be available until after the SAXVisitAfter.visitAfter event.

14.34. StreamResult Writing/Serialization

Overview

The Smooks.filterSource(Source, Result) method can take one or more of a number of different Result type implementations, one of which is the javax.xml.transform.stream.StreamResult class. Smooks streams the Source in and back out again through the StreamResult instance.

By default, Smooks will always serialize the full Source event stream as XML to any StreamResult instance provided to the Smooks.filterSource(Source, Result) method. If the Source provided to the Smooks.filterSource(Source, Result) method is an XML stream and a StreamResult instance is provided as one of the Result instances, the Source XML will be written out to the StreamResult unmodified, unless the Smooks instance is configured with one or more SAXVisitor implementations that modify one or more fragments.

14.35. Configuring StreamResult Writing/Serialization

  1. To turn the default serialization behavior on or off, access the filter settings and configure them to do so.
  2. To modify the serialized form of one of the message fragments, implement a SAXVisitor to perform the transformation and target it at the message fragment using an XPath-like expression.
  3. To modify the serialized form of a message fragment, use one of the provided templating components. These components are also SAXVisitor implementations.

14.36. Implementing the SAXVisitor

  1. To implement a SAXVisitor geared towards transforming the serialized form of a fragment, program Smooks so the SAXVisitor implementation will be writing to the StreamResult. This is because Smooks supports targeting of multiple SAXVisitor implementations at a single fragment, but only one SAXVisitor is allowed to write to the StreamResult, per fragment.
  2. If a second SAXVisitor attempts to write to the StreamResult, a SAXWriterAccessException will result and you will need to modify your Smooks configuration.
  3. To specify the StreamResult to write, the SAXVisitor needs to "acquire ownership" of the Writer to the StreamResult. It does this by making a call to the SAXElement.getWriter(SAXVisitor) method from inside the SAXVisitBefore.visitBefore methods implementation, passing this as the SAXVisitor parameter.

14.37. SAXVisitor Implementation Example

public class MyVisitor implements SAXElementVisitor 
{
   public void visitBefore(SAXElement element, ExecutionContext executionContext) 
                                             throws SmooksException, IOException 
   {
      Writer writer = element.getWriter(this);

      // ... write the start of the fragment...
   }

   public void onChildText(SAXElement element, SAXText childText, 
                                    ExecutionContext executionContext) 
                                       throws SmooksException, IOException 
   {
      Writer writer = element.getWriter(this);

      // ... write the child text...
   }

   public void onChildElement(SAXElement element, SAXElement childElement, 
                                    ExecutionContext executionContext) 
                                       throws SmooksException, IOException 
   {
   
   }

   public void visitAfter(SAXElement element, ExecutionContext executionContext) 
                                             throws SmooksException, IOException 
   {
      Writer writer = element.getWriter(this);
      // ... close the fragment...
   }
}

14.38. The SAXElement.setWriter

The SAXElement.setWriter lets you control the serialization of sub-fragments you need to reset the Writer instance so it diverts serialization of the sub-fragments.
Sometimes you know that the target fragment you are serializing/transforming will never have sub-fragments. In this situation, it is inefficient implement the SAXVisitBefore.visitBefore method just to make a call to the SAXElement.getWriter method to acquire ownership of the Writer. For this reason, we have the @StreamResultWriter annotation. Used in combination with the @TextConsumer annotation, it is only necessary to implement the SAXVisitAfter.visitAfter method.

14.39. StreamResultWriter Example

@StreamResultWriter
public class MyVisitor implements SAXVisitAfter 
{
   public void visitAfter(SAXElement element, ExecutionContext executionContext) 
                                                throws SmooksException, IOException 
   {
      Writer writer = element.getWriter(this);

      // ... serialize to the writer ...
   }
}

14.40. SAXToXMLWriter

Smooks provides the SAXToXMLWriter class. It simplifies the process of serializing SAXElement data as XML. This class allows you to write SAXVisitor implementations.

14.41. SAXToXMLWriter Example

@StreamResultWriter
public class MyVisitor implements SAXElementVisitor 
{
   private SAXToXMLWriter xmlWriter = new SAXToXMLWriter(this, true);

   public void visitBefore(SAXElement element, ExecutionContext executionContext) 
                                             throws SmooksException, IOException 
   {
      xmlWriter.writeStartElement(element);
   }

   public void onChildText(SAXElement element, SAXText childText, ExecutionContext 
                              executionContext) throws SmooksException, IOException 
   {
      xmlWriter.writeText(childText, element);
   }

   public void onChildElement(SAXElement element, SAXElement childElement, 
      ExecutionContext executionContext) throws SmooksException, IOException 
   {
   
   }

   public void visitAfter(SAXElement element, ExecutionContext executionContext) 
                                             throws SmooksException, IOException 
   {
      xmlWriter.writeEndElement(element);
   }
}

14.42. Configuring the SAXToXMLWriter

  1. When writing a SAXVisitor implementation with the SAXToXMLWriter, set the SAXToXMLWriter constructor to a boolean. This is the encodeSpecialChars arg and it should be set based on the rewriteEntities filter setting.
  2. Move the @StreamResultWriter annotation from the class and onto the SAXToXMLWriter instance declaration. This results in Smooks creating the SAXToXMLWriter instance which is then initialized with the rewriteEntities filter setting for the associated Smooks instance:
    @TextConsumer
    public class MyVisitor implements SAXVisitAfter 
    {
       @StreamResultWriter
       private SAXToXMLWriter xmlWriter;
    
       public void visitAfter(SAXElement element, ExecutionContext executionContext) 
                                                 throws SmooksException, IOException 
       {
          xmlWriter.writeStartElement(element);
          xmlWriter.writeText(element);
          xmlWriter.writeEndElement(element);
       }
    }
    

14.43. Visitor Configuration

The SAXVisitor configuration is useful for testing purposes and works in exactly the same way as any other Smooks component. When configuring Smooks Visitor instances, the configuration selector is interpreted in a similar manner as an XPath expression. Visitor instances can be configured within program code on a Smooks instance.

14.44. Example Visitor Configuration

This example will use the SAXVisitor implementation as follows:
@TextConsumer
public class ChangeItemState implements SAXVisitAfter 
{
   @StreamResultWriter
   private SAXToXMLWriter xmlWriter;

   @ConfigParam
   private String newState;

   public void visitAfter(SAXElement element, ExecutionContext executionContext) 
                                             throws SmooksException, IOException 
   {
      element.setAttribute("state", newState);

      xmlWriter.writeStartElement(element);
      xmlWriter.writeText(element);
      xmlWriter.writeEndElement(element);
   }
}
Declaratively configuring ChangeItemState to fire on <order-item> fragments having a status of OK is shown below:
<smooks-resource-list 
   xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd">
 
   <resource-config selector="order-items/order-item[@status = 'OK']">
      <resource>com.acme.ChangeItemState </resource>
      <param name="newState">COMPLETED</param>
   </resource-config>
 
</smooks-resource-list>
Custom configuration namespaces can be used to define a cleaner and more strongly typed configuration for the ChangeItemState component. A custom configuration namespace component is configured as follows:
<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" 
   xmlns:order="http://www.acme.com/schemas/smooks/order.xsd">
 
   <order:changeItemState itemElement="order-items/order-item[@status = 'OK']" 
      newState="COMPLETED" />
 
</smooks-resource-list>
This Visitor could also be configured in source code as follows:
Smooks smooks = new Smooks();

smooks.addVisitor(new ChangeItemState().setNewState("COMPLETED"), 
                                 "order-items/order-item[@status = 'OK']");

smooks.filterSource(new StreamSource(inReader), new StreamResult(outWriter));

14.45. The ExecutionLifecycleCleanable

Visitor components implementing the ExecutionLifecycleCleanable life-cycle interface will be able to perform post Smooks.filterSource life-cycle operations. See the example below:
public interface ExecutionLifecycleCleanable extends Visitor 
{
   public abstract void executeExecutionLifecycleCleanup(
                                          ExecutionContext executionContext);
}
The basic call sequence can be described as follows (note the executeExecutionLifecycleCleanup calls):
smooks = new Smooks(..);

smooks.filterSource(...);
// ** VisitorXX.executeExecutionLifecycleCleanup **
smooks.filterSource(...);
// ** VisitorXX.executeExecutionLifecycleCleanup **
smooks.filterSource(...);
// ** VisitorXX.executeExecutionLifecycleCleanup **
 ... etc ...
This life-cycle method allows you to ensure that resources scoped around the Smooks.filterSource execution life-cycle can be cleaned up for the associated ExecutionContext.

14.46. The VisitLifecycleCleanable

Visitor components implementing the VisitLifecycleCleanable life-cycle interface will be able to perform post SAXVisitAfter.visitAfter life-cycle operations.
public interface VisitLifecycleCleanable extends Visitor 
{
   public abstract void executeVisitLifecycleCleanup(ExecutionContext executionContext);
}
The basic call sequence can be described as follows (note the executeVisitLifecycleCleanup calls):
smooks.filterSource(...);
    
    <message>
        <target-fragment>      < --- VisitorXX.visitBefore
            Text!!                       < --- VisitorXX.onChildText  
            <child>                      < --- VisitorXX.onChildElement
            </child>                 
        </target-fragment>     < --- VisitorXX.visitAfter
        ** VisitorXX.executeVisitLifecycleCleanup **
        <target-fragment>      < --- VisitorXX.visitBefore
            Text!!                       < --- VisitorXX.onChildText  
            <child>                      < --- VisitorXX.onChildElement
            </child>                 
        </target-fragment>     < --- VisitorXX.visitAfter
        ** VisitorXX.executeVisitLifecycleCleanup **
    </message>
    VisitorXX.executeExecutionLifecycleCleanup

smooks.filterSource(...);
    
    <message>
        <target-fragment>      < --- VisitorXX.visitBefore
            Text!!                       < --- VisitorXX.onChildText  
            <child>                      < --- VisitorXX.onChildElement
            </child>                 
        </target-fragment>     < --- VisitorXX.visitAfter
        ** VisitorXX.executeVisitLifecycleCleanup **
        <target-fragment>      < --- VisitorXX.visitBefore
            Text!!                       < --- VisitorXX.onChildText  
            <child>                      < --- VisitorXX.onChildElement
            </child>                 
        </target-fragment>     < --- VisitorXX.visitAfter
        ** VisitorXX.executeVisitLifecycleCleanup **
    </message>
    VisitorXX.executeExecutionLifecycleCleanup
This life-cycle method allows you to ensure that resources scoped around a single fragment execution of a SAXVisitor implementation can be cleaned up for the associated ExecutionContext.

14.47. The ExecutionContext

The ExecutionContext is a context object for the storing of state information. It is scoped specifically around a single execution of a Smooks.filterSource method. All Smooks Visitor implementations must be stateless within the context of a single Smooks.filterSource execution, allowing the Visitor implementation to be used across multiple concurrent executions of the Smooks.filterSource method. All data stored in an ExecutionContext instance will be lost on completion of the Smooks.filterSource execution. The ExecutionContext is supplied in all Visitor API message event calls.

14.48. The ApplicationContext

The ApplicationContext is a context object for storing of state information. It is scoped around the associated Smooks instance, that is, only one ApplicationContext instance exists per Smooks instance. This context object can be used to store data that needs to be maintained and accessible across multiple Smooks.filterSource executions. Components (including SAXVisitor components) can gain access to their associated ApplicationContext instance by declaring an ApplicationContext class property and annotating it with the @AppContext annotation. See the example below:
public class MySmooksComponent 
{
    @AppContext
    private ApplicationContext appContext;

    // etc...
}
Red Hat logoGithubRedditYoutubeTwitter

Learn

Try, buy, & sell

Communities

About Red Hat Documentation

We help Red Hat users innovate and achieve their goals with our products and services with content they can trust.

Making open source more inclusive

Red Hat is committed to replacing problematic language in our code, documentation, and web properties. For more details, see the Red Hat Blog.

About Red Hat

We deliver hardened solutions that make it easier for enterprises to work across platforms and environments, from the core datacenter to the network edge.

© 2024 Red Hat, Inc.