Red Hat Training

A Red Hat training course is available for JBoss Enterprise SOA Platform

Smooks User Guide

JBoss Enterprise SOA Platform 5

This guide is for developers wishing to use Smooks transformations as ESB pipeline actions.

Edition 5.3.1

David Le Sage

Red Hat Engineering Content Services

Suzanne Dorfield

Red Hat Engineering Content Services

Abstract

This document will guide the developer through using Smooks transformations on messages.

Preface

Chapter 1. Preface

1.1. Business Integration

In order to provide a dynamic and competitive business infrastructure, it is crucial to have a service-oriented architecture in place that enables your disparate applications and data sources to communicate with each other with minimum overhead.
The JBoss Enterprise SOA Platform is a framework capable of orchestrating business services without the need to constantly reprogram them to fit changes in business processes. By using its business rules and message transformation and routing capabilities, JBoss Enterprise SOA Platform enables you to manipulate business data from multiple sources.

1.2. What is a Service-Oriented Architecture?

Introduction

A Service Oriented Architecture (SOA) is not a single program or technology. Think of it, rather, as a software design paradigm.

As you may already know, a hardware bus is a physical connector that ties together multiple systems and subsystems. If you use one, instead of having a large number of point-to-point connectors between pairs of systems, you can simply connect each system to the central bus. An enterprise service bus (ESB) does exactly the same thing in software.
The ESB sits in the architectural layer above a messaging system. This messaging system facilitates asynchronous communications between services through the ESB. In fact, when you are using an ESB, everything is, conceptually, either a service (which, in this context, is your application software) or a message being sent between services. The services are listed as connection addresses (known as end-points references.)
It is important to note that, in this context, a "service" is not necessarily always a web service. Other types of applications, using such transports as File Transfer Protocol and the Java Message Service, can also be "services."

Note

At this point, you may be wondering if an enterprise service bus is the same thing as a service-oriented architecture. The answer is, "Not exactly." An ESB does not form a service-oriented architecture of itself. Rather, it provides many of the tools than can be used to build one. In particular, it facilitates the loose-coupling and asynchronous message passing needed by a SOA. Always think of a SOA as being more than just software: it is a series of principles, patterns and best practices.

1.3. Key Points of a Service-Oriented Architecture

These are the key components of a service-oriented architecture:
  1. the messages being exchanged
  2. the agents that act as service requesters and providers
  3. the shared transport mechanisms that allow the messages to flow back and forth.

1.4. What is the JBoss Enterprise SOA Platform?

The JBoss Enterprise SOA Platform is a framework for developing enterprise application integration (EAI) and service-oriented architecture (SOA) solutions. It is made up of an enterprise service bus (JBoss ESB) and some business process automation infrastructure. It allows you to build, deploy, integrate and orchestrate business services.

1.5. The Service-Oriented Architecture Paradigm

The service-oriented architecture (SOA) consists of three roles: requester, provider, and broker.
Service Provider
A service provider allows access to services, creates a description of a service and publishes it to the service broker.
Service Requester
A service requester is responsible for discovering a service by searching through the service descriptions given by the service broker. A requester is also responsible for binding to services provided by the service provider.
Service Broker
A service broker hosts a registry of service descriptions. It is responsible for linking a requester to a service provider.

1.6. Core and Components

The JBoss Enterprise SOA Platform provides a comprehensive server for your data integration needs. On a basic level, it is capable of updating business rules and routing messages through an Enterprise Service Bus.
The heart of the JBoss Enterprise SOA Platform is the Enterprise Service Bus. JBoss (ESB) creates an environment for sending and receiving messages. It is able to apply “actions” to messages to transform them and route them between services.
There are a number of components that make up the JBoss Enterprise SOA Platform. Along with the ESB, there is a registry (jUDDI), transformation engine (Smooks), message queue (HornetQ) and BPEL engine (Riftsaw).

1.7. Components of the JBoss Enterprise SOA Platform

  • A full Java EE-compliant application server (the JBoss Enterprise Application Platform)
  • an enterprise service bus (JBoss ESB)
  • a business process management system (jBPM)
  • a business rules engine (JBoss Rules)
  • support for the optional JBoss Enterprise Data Services (EDS) product.

1.8. JBoss Enterprise SOA Platform Features

The JBoss Enterprise Service Bus (ESB)
The ESB sends messages between services and transforms them so that they can be processed by different types of systems.
Business Process Execution Language (BPEL)
You can use web services to orchestrate business rules using this language. It is included with SOA for the simple execution of business process instructions.
Java Universal Description, Discovery and Integration (jUDDI)
This is the default service registry in SOA. It is where all the information pertaining to services on the ESB are stored.
Smooks
This transformation engine can be used in conjunction with SOA to process messages. It can also be used to split messages and send them to the correct destination.
JBoss Rules
This is the rules engine that is packaged with SOA. It can infer data from the messages it receives to determine which actions need to be performed.

1.9. Features of the JBoss Enterprise SOA Platform's JBossESB Component

The JBoss Enterprise SOA Platform's JBossESB component supports:
  • Multiple transports and protocols
  • A listener-action model (so that you can loosely-couple services together)
  • Content-based routing (through the JBoss Rules engine, XPath, Regex and Smooks)
  • Integration with the JBoss Business Process Manager (jBPM) in order to provide service orchestration functionality
  • Integration with JBoss Rules in order to provide business rules development functionality.
  • Integration with a BPEL engine.
Furthermore, the ESB allows you to integrate legacy systems in new deployments and have them communicate either synchronously or asynchronously.
In addition, the enterprise service bus provides an infrastructure and set of tools that can:
  • Be configured to work with a wide variety of transport mechanisms (such as e-mail and JMS),
  • Be used as a general-purpose object repository,
  • Allow you to implement pluggable data transformation mechanisms,
  • Support logging of interactions.

Important

There are two trees within the source code: org.jboss.internal.soa.esb and org.jboss.soa.esb. Use the contents of the org.jboss.internal.soa.esb package sparingly because they are subject to change without notice. By contrast, everything within the org.jboss.soa.esb package is covered by Red Hat's deprecation policy.

1.10. Task Management

JBoss SOA simplifies tasks by designating tasks to be performed universally across all systems it affects. This means that the user does not have to configure the task to run separately on each terminal. Users can connect systems easily by using web services.
Businesses can save time and money by using JBoss SOA to delegate their transactions once across their networks instead of multiple times for each machine. This also decreases the chance of errors ocurring.

1.11. Integration Use Case

Acme Equity is a large financial service. The company possesses many databases and systems. Some are older, COBOL-based legacy systems and some are databases obtained through the acquisition of smaller companies in recent years. It is challenging and expensive to integrate these databases as business rules frequently change. The company wants to develop a new series of client-facing e-commerce websites, but these may not synchronise well with the existing systems as they currently stand.
The company wants an inexpensive solution but one that will adhere to the strict regulations and security requirements of the financial sector. What the company does not want to do is to have to write and maintain “glue code” to connect their legacy databases and systems.
The JBoss Enterprise SOA Platform was selected as a middleware layer to integrate these legacy systems with the new customer websites. It provides a bridge between front-end and back-end systems. Business rules implemented with the JBoss Enterprise SOA Platform can be updated quickly and easily.
As a result, older systems can now synchronise with newer ones due to the unifying methods of SOA. There are no bottlenecks, even with tens of thousands of transactions per month. Various integration types, such as XML, JMS and FTP, are used to move data between systems. Any one of a number of enterprise-standard messaging systems can be plugged into JBoss Enterprise SOA Platform providing further flexibility.
An additional benefit is that the system can now be scaled upwards easily as more servers and databases are added to the existing infrastructure.

1.12. Utilising the JBoss Enterprise SOA Platform in a Business Environment

Cost reduction can be achieved due to the implementation of services that can quickly communicate with each other with less chance of error messages occurring. Through enhanced productivity and sourcing options, ongoing costs can be reduced.
Information and business processes can be shared faster because of the increased connectivity. This is enhanced by web services, which can be used to connect clients easily.
Legacy systems can be used in conjunction with the web services to allow different systems to "speak" the same language. This reduces the amount of upgrades and custom code required to make systems synchronise.

Chapter 2. Introduction

2.1. Intended Audience

This book is aimed at developers who wish to understand Smooks and how to utilize it. It contains an introduction to using the product for beginners as well as topics for advanced programming.

2.2. Aim of the Guide

This guide aims to teach developers about Smooks, its features and methods of implementation. Users will learn how to configure Smooks for different tasks and how to perform extended tasks.

2.3. Back Up Your Data

Warning

Red Hat recommends that you back up your system settings and data before undertaking any of the configuration tasks mentioned in this book.

2.4. Red Hat Documentation Site

Red Hat's official documentation site is at https://access.redhat.com/knowledge/docs/. There you will find the latest version of every book, including this one.

Chapter 3. Basics

3.1. Smooks

Smooks is a fragment-based data transformation and analysis tool. It is a general purpose processing tool capable of interpreting fragments of a message. It uses visitor logic to accomplish this. It allows you implement your transformation logic in XSLT or Java and provides a management framework through which you can centrally manage the transformation logic for your message-set.

3.2. Visitor Logic in Smooks

Smooks uses visitor logic. A "visitor" is Java code that performs a specific action on a specific fragment of a message. This enables Smooks to perform actions on message fragments.

3.3. Message Fragment Processing

Smooks supports these types of message fragment processing:
  • Templating: Transform message fragments with XSLT or FreeMarker
  • Java Binding: Bind message fragment data into Java objects
  • Splitting: Split messages fragments and rout the split fragments over multiple transports and destinations
  • Enrichment: "Enrich" message fragments with data from databases
  • Persistence: Persist message fragment data to databases
  • Validation: Perform basic or complex validation on message fragment data

3.4. Basic Processing Model

The following is a list of different transformations you can perform with Smooks:
  • XML to XML
  • XML to Java
  • Java to XML
  • Java to Java
  • EDI to XML
  • EDI to Java
  • Java to EDI
  • CSV to XML

3.5. Supported Models

Simple API for XML (SAX)
The SAX event model is based on the hierarchical SAX events you can generate from an XML source. These include the startElement and endElement. Apply it to other structured and hierarchical data sources like EDI, CSV and Java files.
Document Object Model (DOM)
Use this object model to map the message source and its final result.

Note

The most important events have visitBefore and visitAfter in their titles.

3.6. FreeMarker

FreeMarker is a template engine. You can use it to create and use a NodeModel as the domain model for a template operation. Smooks adds the ability to perform fragment-based template transformations to this functionality, as well as the power to apply the model to huge messages.

3.7. Example of Using SAX

Prerequisites

  • Requires an implemented SAXVisitor interface. (Choose an interface that corresponds to the events of the process.)
  • This example uses the ExecutionContext name. It is a public interface which extends the BoundAttributeStore class.

Procedure 3.1. Task

  1. Create a new Smooks configuration. This will be used to apply the visitor logic at the <xxx> element's visitBefore and visitAfter events.
  2. Apply the logic at the visitBefore and visitAfter events in a specific element of the overall event stream. The visitor logic is applied to the events in the <xxx> element.
  3. Use Smooks with FreeMarker to perform an XML-to-XML transformation on a huge message.
  4. Insert the following source format:
    <order id='332'>
        <header>
            <customer number="123">Joe</customer>
        </header>
        <order-items>
            <order-item id='1'>
                <product>1</product>
                <quantity>2</quantity>
                <price>8.80</price>
            </order-item>
    Â 
            <!-- etc etc -->
    Â 
        </order-items>
    </order>
    
  5. Insert this target format:
    <salesorder>
        <details>
            <orderid>332</orderid>
            <customer>
                <id>123</id>
                <name>Joe</name>
            </customer>
        <details>
        <itemList>
            <item>
                <id>1</id>
                <productId>1</productId>
                <quantity>2</quantity>
                <price>8.80</price>
            <item>        
     
            <!-- etc etc -->
     
        </itemList>
    </salesorder>
    
  6. Use this Smooks configuration:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"
                          xmlns:ftl="http://www.milyn.org/xsd/smooks/freemarker-1.1.xsd">
     
        <!--
        Filter the message using the SAX Filter (i.e. not DOM, so no
        intermediate DOM for the "complete" message - there are "mini" DOMs
        for the NodeModels below)....
        -->
        <params>
            <param name="stream.filter.type">SAX</param>
            <param name="default.serialization.on">false</param>
        </params>
     
        <!--
        Create 2 NodeModels.  One high level model for the "order"
        (header etc) and then one per "order-item".
     
        These models are used in the FreeMarker templating resources
        defined below.  You need to make sure you set the selector such
        that the total memory footprint is as low as possible.  In this
        example, the "order" model will contain everything accept the
        <order-item> data (the main bulk of data in the message).  The
        "order-item" model only contains the current <order-item> data
        (i.e. there's max 1 order-item in memory at any one time).
        -->
        <resource-config selector="order,order-item">
            <resource>org.milyn.delivery.DomModelCreator</resource>
        </resource-config>
     
        <!--
        Apply the first part of the template when we reach the start
        of the <order-items> element.  Apply the second part when we
        reach the end.
     
        Note the <?TEMPLATE-SPLIT-PI?> Processing Instruction in the
        template.  This tells Smooks where to split the template,
        resulting in the order-items being inserted at this point.
        -->
        <ftl:freemarker applyOnElement="order-items">
            <ftl:template><!--<salesorder>
        <details>
            <orderid>${order.@id}</orderid>
            <customer>
                <id>${order.header.customer.@number}</id>
                <name>${order.header.customer}</name>
            </customer>
        </details>
        <itemList>
            <?TEMPLATE-SPLIT-PI?>
        </itemList>
    </salesorder>--></ftl:template>
        </ftl:freemarker>
     
        <!--
        Output the <order-items> elements.  This will appear in the
        output message where the <?TEMPLATE-SPLIT-PI?> token appears in the
        order-items template.
        -->
        <ftl:freemarker applyOnElement="order-item">
            <ftl:template><!--        <item>
                <id>${.vars["order-item"].@id}</id>
                <productId>${.vars["order-item"].product}</productId>
                <quantity>${.vars["order-item"].quantity}</quantity>
                <price>${.vars["order-item"].price}</price>
            </item>
            --></ftl:template>
        </ftl:freemarker>
     
    </smooks-resource-list>
    
  7. Use this code to execute:
    Smooks smooks = new Smooks("smooks-config.xml");
    try {
        smooks.filterSource(new StreamSource(new FileInputStream("input-message.xml")), new StreamResult(System.out));
    } finally {
        smooks.close();
    }
    
  8. An XML-to-XML transformation occurs as a result.

3.8. Cartridges

A cartridge is a Java archive (JAR) file that contains reusable content handlers. In most cases, you will not need to write large quantities of Java code for Smooks because some modules of functionality are included as cartridges. You can create new cartridges of your own to extend the smooks-core's basic functionality. Each cartridge provides ready-to-use support for either a transformation process or a specific form of XML analysis.

3.9. Supplied Cartridges

These are the cartridges supplied with Smooks:
  • Calc:"milyn-smooks-calc"
  • CSV: "milyn-smooks-csv"
  • Fixed length reader: "milyn-smooks-fixed-length"
  • EDI: "milyn-smooks-edi"
  • Javabean: "milyn-smooks-javabean"
  • JSON: "milyn-smooks-json"
  • Routing: "milyn-smooks-routing"
  • Templating: "milyn-smooks-templating"
  • CSS: "milyn-smooks-css"
  • Servlet: "milyn-smooks-servlet"
  • Persistence: "milyn-smooks-persistence"
  • Validation: "milyn-smooks-validation"

3.10. Selectors

Smooks resource selectors tell Smooks which messages fragments to apply visitor logic. They also serve as simple look-up values for non-visitor logic. When a resource is a visitor implementation (like <jb:bean> or <ftl:freemarker>), Smooks treats the resource selector as an XPath selector. Resources include the Java Binding Resource and FreeMarker Template Resource.

3.11. Using Selectors

The following points apply when using the selectors:
  • Configurations are both "strongly typed" and domain-specific for legibility.
  • Configurations are XSD-based. This provides you with auto-completion support when using an integrated development environment.
  • The actual handler doesn't need to be defined for the given resource type (such as the BeanPopulator class for Java bindings).

3.12. Declaring Namespaces

Procedure 3.2. Task

  • Configure namespace prefix-to-URI mappings through the core configuration namespace and modify the following XML code to include the namespaces you wish to use:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"
        xmlns:core="http://www.milyn.org/xsd/smooks/smooks-core-1.3.xsd">
    
        <core:namespaces>
            <core:namespace prefix="a" uri="http://a"/>
            <core:namespace prefix="b" uri="http://b"/>
            <core:namespace prefix="c" uri="http://c"/>
            <core:namespace prefix="d" uri="http://d"/>
        </core:namespaces>
    
        <resource-config selector="c:item[@c:code = '8655']/d:units[text() = 1]">
            <resource>com.acme.visitors.MyCustomVisitorImpl</resource>
        </resource-config>
    
    </smooks-resource-list>
    

3.13. Filtering Process Selection

This is how Smooks selects a filtering process:
  • The DOM processing model is selected automatically if only the DOM visitor interface is applied ( DOMElementVisitor and SerializationUnit).
  • If all visitor resources use only the SAX visitor interface ( SAXElementVisitor), the SAX processing model is selected automatically.
  • If the visitor resources use both the DOM and SAX interfaces, the DOM processing model is selected by default unless you specify SAX in the Smooks resource configuration file. (This is done using <core:filterSettings type="SAX" />.)
Visitor resources do not include non-element visitor resources such as readers.

3.14. Example of Setting the Filter Type to SAX in Smooks 1.3

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

    <core:filterSettings type="SAX" />

</smooks-resource-list>

3.15. DomModelCreator

The DomModelCreator is a class that you can use in Smooks to create models for message fragments.

3.16. Mixing the DOM and SAX Models

  • Use the DOM (Document Object Model) for node traversal (that is, seinding information between nodes) and pre-existing scripting/template engines.
  • Use the DomModelCreator visitor class to mix SAX and DOM models. When used with SAX filtering, this visitor will construct a DOM fragment from the visited element. It allows you to use DOM utilities within a streaming environment.
  • When more than one model is nested, the outer models will never contain data from the inner models (that is, the same fragment will never co-exist inside two models):
    <order id="332">
        <header>
            <customer number="123">Joe</customer>
        </header>
        <order-items>
            <order-item id='1'>
                <product>1</product>
                <quantity>2</quantity>
                <price>8.80</price>
            </order-item>
            <order-item id='2'>
                <product>2</product>
                <quantity>2</quantity>
                <price>8.80</price>
            </order-item>
            <order-item id='3'>
                <product>3</product>
                <quantity>2</quantity>
                <price>8.80</price>
            </order-item>
        </order-items>
    </order>
    

3.17. Configuring the DomModelCreator

  1. Configure the DomModelCreator from within Smooks to create models for the order and order-item message fragments. See the following example:
    <resource-config selector="order,order-item">
        <resource>org.milyn.delivery.DomModelCreator</resource>
    </resource-config>
    
  2. Configure the in-memory model for the order as shown:
    <order id='332'>
         <header>
             <customer number="123">Joe</customer>
         </header>
         <order-items />
    </order>
    

    Note

    Each new model overwrites the previous one so there will never be more than one order-item model in memory at once.

3.18. Further Information about the DomModelCreator

3.19. The Bean Context

The bean context contains objects for Smooks to access when filtering occurs. One bean context is created per execution context (using the Smooks.filterSource operation). Every bean the cartridge creates is filed according to its beanId.

3.20. Configuring Bean Contexts

  1. To have the contents of the bean context returned at the end of a Smooks.filterSource process, supply a org.milyn.delivery.java.JavaResult object in the call to the Smooks.filterSource method. This example shows you how:
     //Get the data to filter
    StreamSource source = new StreamSource(getClass().getResourceAsStream("data.xml"));
    
    //Create a Smooks instance (cachable)
    Smooks smooks = new Smooks("smooks-config.xml");
    
    //Create the JavaResult, which will contain the filter result after filtering
    JavaResult result = new JavaResult();
    
    //Filter the data from the source, putting the result into the JavaResult
    smooks.filterSource(source, result);
    
    //Getting the Order bean which was created by the Javabean cartridge
    Order order = (Order)result.getBean("order");
    
  2. To access the bean contexts at start-up, specify this in the BeanContext object. You can retrieve it from the ExecutionContext via the getBeanContext() method.
  3. When adding or retrieving objects from the BeanContext make sure you first retrieve a beanId object from the beanIdStore. (The beanId object is a special key that ensures higher performance than string keys, although string keys are also supported.)
  4. You must retrieve the beanIdStore from the ApplicationContext using the getbeanIdStore() method.
  5. To create a beanId object, call the register("beanId name") method. (If you know that the beanId is already registered, then you can retrieve it by calling the getbeanId("beanId name") method.)
  6. beanId objects are ApplicationContext-scoped objects. Register them in your custom visitor implementation's initialization method and then put them in the visitor object as properties. You can then use them in the visitBefore and visitAfter methods. (The beanId objects and the beanIdStore are thread-safe.)

3.21. Pre-Installed Beans

The following Beans come pre-installed:
  • PUUID: UniqueId bean. This bean provides unique identifiers for the filtering ExecutionContext.
  • PTIME: Time bean. This bean provides time-based data for the filtering ExecutionContext.
These examples show you how to use these beans in a FreeMarker template:
  • Unique ID of the ExecutionContext (message being filtered): $PUUID.execContext
  • Random Unique ID: $PUUID.random
  • Message Filtering start time (in milliseconds): $PTIME.startMillis
  • Message Filtering start time (in nanoseconds): $PTIME.startNanos
  • Message Filtering start time (Date): $PTIME.startDate
  • Time now (in milliseconds): $PTIME.nowMillis
  • Time now (in nanoseconds): $PTIME.nowNanos
  • Time now (Date): $PTIME.nowDate

3.22. Multiple Outputs/Results

Smooks produces output in these ways:
  • Through in-result instances. These are returned in the result instances passed to the Smooks.filterSource method.
  • During the filtering process. This is achieved through output generated and sent to external endpoints (such as ESB services, files, JMS destinations and databases) during the filtering process. Message fragment events trigger automatic routing to external endpoints.

Important

Smooks can generate output in the above ways in a single filtering pass of a message stream. It does not need to filter a message stream multiple times to generate multiple outputs.

3.23. Creating "In-Result" Instances

  • Supply Smooks with multiple result instances as seen in the API:
    public void filterSource(Source source, Result... results) throws SmooksException
    

    Note

    Smooks does not support capturing result data from multiple result instances of the same type. For example, you can specify multiple StreamResult instances in the Smooks.filterSource method call, but Smooks will only output to one of these StreamResult instances (the first one).

3.24. Supported Result Types

Smooks can work with standard JDK StreamResult and DOMResult result types, as well as these specialist ones:
  • JavaResult: use this result type to capture the contents of the Smooks Java Bean context.
  • ValidationResult: use this result type to capture outputs.
  • Simple Result type: use this when writing tests. This is a StreamResult extension wrapping a StringWriter.

3.25. Event Stream Results

When Smooks processes a message, it produces a stream of events. If a StreamResult or DOMResult is supplied in the Smooks.filterSource call, Smooks will, by default, serialize the event stream (produced by the Source) to the supplied result as XML. (You can apply visitor logic to the event stream before serialization.)

Note

This is the mechanism used to perform a standard 1-input/1-xml-output character based transformation.

3.26. During the Filtering Process

Smooks generates different types of output during the Smooks.filterSource process. (This occurs during the message event stream, before the end of the message is reached.) An example of this is when it is used to split and route message fragments to different types of endpoints for execution by other processes.
Smooks does not "batch up" the message data and produce all of the outputs after filtering the complete message. This is because performance would be impacted and also because it allows you to utilize the message event stream to trigger the fragment transformation and routing operations. Large messages are sent by streaming the process.

3.27. Checking the Smooks Execution Process

  1. To obtain an execution report from Smooks you must configure the ExecutionContext class to produce one. (Smooks will publish events as it processes messages.) The following sample code shows you how to configure Smooks to generate a HTML report:
    Smooks smooks = new Smooks("/smooks/smooks-transform-x.xml");
    ExecutionContext execContext = smooks.createExecutionContext();
    
    execContext.setEventListener(new HtmlReportGenerator("/tmp/smooks-report.html"));
    smooks.filterSource(execContext, new StreamSource(inputStream), new StreamResult(outputStream));
    
  2. Use the HtmlReportGenerator feature to assist you when debugging.

    Note

    You can see a sample report on this web page: http://www.milyn.org/docs/smooks-report/report.html

    Note

    Alternatively, you can create a custom ExecutionEventListener implementation.

3.28. Terminating the Filtering Process

  1. To terminate the Smooks filtering process before the end of the message is reached, add the <core:terminate> configuration to the Smooks settings. (This works for SAX and is not needed for DOM.)
    Here is an example configuration that terminates filtering at the end of the message's customer fragment:
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" 
       xmlns:core="http://www.milyn.org/xsd/smooks/smooks-core-1.3.xsd">
    
        <!-- Visitors... -->
        <core:terminate onElement="customer" />
    
    </smooks-resource-list>
  2. To terminate at the beginning of a message (on the visitBefore event), use this code:
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" 
       xmlns:core="http://www.milyn.org/xsd/smooks/smooks-core-1.3.xsd">
    
       <!-- Visitors... -->
    
       <core:terminate onElement="customer" terminateBefore="true" />
    
    </smooks-resource-list>

3.29. Global Configuration Settings

Default Properties
Default Properties specify the default values for <resource-config> attributes. These properties are automatically applied to the SmooksResourceConfiguration class when the corresponding <resource-config> does not specify a value for the attribute.
Global parameters
You can specify <param> elements in every <resource-config>. These parameter values will either be available at runtime through the SmooksResourceConfiguration or, if not, they will be injected through the @ConfigParam annotation.
Global configuration parameters are defined in one place. Every runtime component can access them by using the ExecutionContext.

3.30. Global Configuration Parameters

  1. Global parameters are specified in a <params> element as shown:
    <params>
        <param name="xyz.param1">param1-val</param>
    </params>
    
  2. Access the global parameters via the ExecutionContext:
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" 
        xmlns:xsl="http://www.milyn.org/xsd/smooks/xsl-1.1.xsd" 
        default-selector="order">
    
        <resource-config>
            <resource>com.acme.VisitorA</resource>
            ...
        </resource-config>
    
        <resource-config>
            <resource>com.acme.VisitorB</resource>
            ...
        </resource-config>
    
    <smooks-resource-list>
    

3.31. Default Properties

Default properties can be set on the root element of a Smooks configuration which then applies them applied the resource configurations in the smooks-conf.xml file. If all of the resource configurations have the same selector value, you can specify a default-selector=order. This means you don't have to specify the selector on every resource configuration.

3.32. Default Properties Example Configuration

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

    <resource-config>
        <resource>com.acme.VisitorA</resource>
        ...
    </resource-config>

    <resource-config>
        <resource>com.acme.VisitorB</resource>
        ...
    </resource-config>

<smooks-resource-list>

3.33. Default Property Options

default-selector
This is applied to all of the resource-config elements in the Smooks configuration file if no other selector has been defined.
default-selector-namespace
This is the default selector namespace. It is used if no other namespace is defined.
default-target-profile
This is the default target profile. It is applied to all of the resources in the Smooks configuration file when no other target-profile has been defined.
default-condition-ref
This refers to a global condition by the conditions identifier. This condition is applied to resources that define an empty condition element (in other words, <condition/>) that does not reference a globally-defined condition.

3.34. Filter Settings

  • To set filtering options, use the smooks-core configuration namespace. See the following example:
    ;smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" 
       xmlns:core="http://www.milyn.org/xsd/smooks/smooks-core-1.3.xsd">
       <core:filterSettings type="SAX" defaultSerialization="true" 
          terminateOnException="true" readerPoolSize="3" closeSource="true" 
          closeResult="true" rewriteEntities="true" />
    
          .. Other visitor configs etc...
    
    </smooks-resource-list>
    

3.35. Filter Options

type
This determines the type of processing model that will be used out of either SAX or DOM. (The default is DOM.)
defaultSerialization
This determines if default serialization should be switched on. The default value is true. Turning it on tells Smooks to locate a StreamResult (or DOMResult) in the result objects provided to the Smooks.filterSource method and to, by default, serialize all events to that result.
You can turn this behaviour off via the global configuration parameter or you can override it on a per-fragment basis by targeting a visitor implementation at that fragment that either takes ownership of the result writer (when using SAX filtering) or modifies the DOM (when using DOM filtering).
terminateOnException
Use this to determine whether an exception should terminate processing. The default setting is true.
closeSource
This closes source instance streams passed to the Smooks.filterSource method (the default is true). The exception here is System.in, which will never be closed.
closeResult
This closes result streams passed to the Smooks.filterSource method (the default istrue). The exceptions here are System.out and System.err, which are never closed.
rewriteEntities
Use this to rewrite XML entities when reading and writing (default serialization) XML.
readerPoolSize
This sets the reader pool size. Some reader implementations are very expensive to create. Pooling reader instances (in other words, reusing them) can result in significant performance improvement, especially when processing a multitude of small messages. The default value for this setting is 0 (in other words, not pooled: a new reader instance is created for each message).
Configure this to be in line with your applications threading model.

Chapter 4. Consuming Input Data

4.1. Stream Readers

A stream reader is a class that implements the XMLReader interface (or the SmooksXMLReader interface). Smooks uses a stream reader to generate a stream of SAX events from the source message data stream. XMLReaderFactory.createXMLReader() is the default XMLReader. It can be configured to read non-XML data sources by configuring a specialist XML reader.

4.2. XMLReader Configurations

This is an example of how to configure the XML to use handlers, features and parameters:
<reader class="com.acme.ZZZZReader">
    <handlers>
        <handler class="com.X" />
        <handler class="com.Y" />
    </handlers>
    <features>
        <setOn feature="http://a" />
        <setOn feature="http://b" />
        <setOff feature="http://c" />
        <setOff feature="http://d" />
    </features>
    <params>
        <param name="param1">val1</param>
        <param name="param2">val2</param>
    </params>
</reader>

4.3. Setting Features on the XML Reader

  • By default, Smooks reads XML data. To set features on the default XML reader, omit the class name from the configuration:
    <reader>
        <features>
            <setOn feature="http://a" />
            <setOn feature="http://b" />
            <setOff feature="http://c" />
            <setOff feature="http://d" />
        </features>
    </reader>
    

4.4. Configuring the CSV Reader

  1. Use the http://www.milyn.org/xsd/smooks/csv-1.2.xsd configuration namespace to configure the reader.
    Here is a basic configuration:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:csv="http://www.milyn.org/xsd/smooks/csv-1.2.xsd">
     
        <!--
        Configure the CSV to parse the message into a stream of SAX events.
        -->
        <csv:reader fields="firstname,lastname,gender,age,country" separator="|" quote="'" skipLines="1" />
     
    </smooks-resource-list>
    
  2. You will see the following event stream:
    <csv-set>
        <csv-record>
            <firstname>Tom</firstname>
            <lastname>Fennelly</lastname>
            <gender>Male</gender>
            <age>21</age>
            <country>Ireland</country>
        </csv-record>
        <csv-record>
            <firstname>Tom</firstname>
            <lastname>Fennelly</lastname>
            <gender>Male</gender>
            <age>21</age>
            <country>Ireland</country>
        </csv-record>
    </csv-set>
    

4.5. Defining Configurations

  1. To define fields in XML configurations you must use a comma-separated list of names in the fields attribute.
  2. Make sure the field names follow the same naming rules as XML element names:
    • they can contain letters, numbers, and other characters
    • they cannot start with a number or punctuation character
    • they cannot start with the letters xml (or XML or Xml, etc)
    • they cannot contain spaces
  3. Set the rootElementName and recordElementName attributes so you can modify the csv-set and csv-record element names. The same rules apply for these names.
  4. You can define string manipulation functions on a per-field basis. These functions are executed before the data is converted into SAX events. Define them after the field name, separating the two with a question mark:
    
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:csv="http://www.milyn.org/xsd/smooks/csv-1.2.xsd">
     
        <csv:reader fields="lastname?trim.capitalize,country?upper_case" />
     
    </smooks-resource-list>
    
    
  5. To get Smooks to ignore fields in a CSV record, you must specify the $ignore$ token as the field's configuration value. Specify the number of fields to be ignored simply by following the $ignore$ token with a number (so use $ignore$3 to ignore the next three fields.) Use $ignore$+ to ignore all of the fields to the end of the CSV record.
    
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:csv="http://www.milyn.org/xsd/smooks/csv-1.2.xsd">
     
        <csv:reader fields="firstname,$ignore$2,age,$ignore$+" />
     
    </smooks-resource-list>
    
    

4.6. Binding CSV Records to Java Objects

  1. Read the following to learn how to CSV records to Java objects. In this example, we will use CSV records for people:
    Tom,Fennelly,Male,4,Ireland
    Mike,Fennelly,Male,2,Ireland
    
  2. Input this code to bind the record to a person:
    
    public class Person {
        private String firstname;
        private String lastname;
        private String country;
        private Gender gender;
        private int age;
    }
     
    public enum Gender {
        Male, 
        Female;
    }
    
    
  3. Input the following code and modify it to suit your task:
    
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:csv="http://www.milyn.org/xsd/smooks/csv-1.2.xsd">
     
        <csv:reader fields="firstname,lastname,gender,age,country">
            <!-- Note how the field names match the property names on the Person class. -->
            <csv:listBinding beanId="people" class="org.milyn.csv.Person" />
        </csv:reader>
     
    </smooks-resource-list>
    
  4. To execute the configuration, use this code:
    
    Smooks smooks = new Smooks(configStream);
    JavaResult result = new JavaResult();
     
    smooks.filterSource(new StreamSource(csvStream), result);
     
    List<Person> people = (List<Person>) result.getBean("people");
    
  5. You can create Maps from the CSV record set:
    
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:csv="http://www.milyn.org/xsd/smooks/csv-1.2.xsd">
     
        <csv:reader fields="firstname,lastname,gender,age,country">
            <csv:mapBinding beanId="people" class="org.milyn.csv.Person" keyField="firstname" />
        </csv:reader>
     
    </smooks-resource-list>
    
    
  6. The configuration above produces a map of person instances, keyed to the firstname value of each person. This is how it is executed:
    
    Smooks smooks = new Smooks(configStream);
    JavaResult result = new JavaResult();
     
    smooks.filterSource(new StreamSource(csvStream), result);
     
    Map<String, Person> people = (Map<String, Person>) result.getBean("people");
     
    Person tom = people.get("Tom");
    Person mike = people.get("Mike");
    
    
    Virtual models are also supported, so you can define the class attribute as a java.util.Map and bind the CSV field values to map instances which are, in turn, added to a list or map.

4.7. Configuring the CSV Reader for Record Sets

  1. To configure a Smooks instance with a CSV reader to read a person record set, use the code below. It will bind the records to a list of person instances.
    
    Smooks smooks = new Smooks();
     
    smooks.setReaderConfig(new CSVReaderConfigurator("firstname,lastname,gender,age,country")
                      .setBinding(new CSVBinding("people", Person.class, CSVBindingType.LIST)));
     
    JavaResult result = new JavaResult();
    smooks.filterSource(new StreamSource(csvReader), result);
     
    List<Person> people = (List<Person>) result.getBean("people");
    
    

    Note

    You can also optionally configure the Java Bean. The Smooks instance could instead (or additionally) be configured programmatically to use other visitor implementations to process the CSV record set.
  2. To bind the CSV's records to a list or map of a Java type that reflects the data in your CSV records, use the CSVListBinder or CSVMapBinder classes:
    
    // Note: The binder instance should be cached and reused...
    CSVListBinder binder = new CSVListBinder("firstname,lastname,gender,age,country", Person.class);
     
    List<Person> people = binder.bind(csvStream);
    
    CSVMapBinder:
    
    // Note: The binder instance should be cached and reused...
    CSVMapBinder binder = new CSVMapBinder("firstname,lastname,gender,age,country", Person.class, "firstname");
     
    Map<String, Person> people = binder.bind(csvStream);
    
    
    If you need more control over the binding process, revert back to using the lower-level APIs.

4.8. Configuring the Fixed-Length Reader

  1. To configure the fixed-length reader, modify the http://www.milyn.org/xsd/smooks/fixed-length-1.3.xsd configuration namespace as shown below:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:fl="http://www.milyn.org/xsd/smooks/fixed-length-1.3.xsd">
     
        <!--
        Configure the Fixed length to parse the message into a stream of SAX events.
        -->
        <fl:reader fields="firstname[10],lastname[10],gender[1],age[2],country[2]" skipLines="1" />
     
    </smooks-resource-list>
    
    
    Here is an example input file:
    #HEADER
    Tom       Fennelly  M 21 IE
    Maurice  Zeijen     M 27 NL
    
    Here is the event stream that will be generated:
    
    <set>
        <record>
            <firstname>Tom       </firstname>
            <lastname>Fennelly  </lastname>
            <gender>M</gender>
            <age> 21</age>
            <country>IE</country>
        </record>
        <record>
            <firstname>Maurice  </firstname>
            <lastname>Zeijen     </lastname>
            <gender>M</gender>
            <age>27</age>
            <country>NL</country>
        </record>
    </set>
    ]]>
    
  2. Define the string manipulation functions as shown below:
    
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:fl="http://www.milyn.org/xsd/smooks/fixed-length-1.3.xsd">
     
        <!--
        Configure the fixed length reader to parse the message into a stream of SAX events.
        -->
        <fl:reader fields="firstname[10]?trim,lastname[10]trim.capitalize,gender[1],age[2],country[2]" skipLines="1" />
     
    </smooks-resource-list>
    
    
  3. You can also ignore these fields if you choose:
    
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:fl="http://www.milyn.org/xsd/smooks/fixed-length-1.3.xsd">
     
        <fl:reader fields="firstname,$ignore$[2],age,$ignore$[10]" />
     
    </smooks-resource-list>
    
    

4.9. Configuring Fixed-Length Records

  1. To bind fixed-length records to a person, see the configuration below. In this example we will use these sample records:
    Tom       Fennelly  M 21 IE
    Maurice  Zeijen     M 27 NL
    
    This is how you bind them to a person:
    [
    public class Person {
        private String firstname;
        private String lastname;
        private String country;
        private String gender;
        private int age;
    }
    
    
  2. Configure the records so they look like this:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"  xmlns:fl="http://www.milyn.org/xsd/smooks/fixed-length-1.3.xsd">
     
        <fl:reader fields="firstname[10]?trim,lastname[10]?trim,gender[1],age[3]?trim,country[2]">
            <!-- Note how the field names match the property names on the Person class. -->
            <fl:listBinding BeanId="people" class="org.milyn.fixedlength.Person" />
        </fl:reader>
     
    </smooks-resource-list>
    
    
  3. Execute it as shown:
    Smooks smooks = new Smooks(configStream);
    JavaResult result = new JavaResult();
     
    smooks.filterSource(new StreamSource(fixedLengthStream), result);
     
    List<Person> people = (List<Person>) result.getBean("people");
    
    
  4. Optionally, use this configuration to create maps from the fixed-length record set:
    
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"  xmlns:fl="http://www.milyn.org/xsd/smooks/fixed-length-1.3.xsd">
     
        <fl:reader fields="firstname[10]?trim,lastname[10]?trim,gender[1],age[3]?trim,country[2]">
            <fl:mapBinding BeanId="people" class="org.milyn.fixedlength.Person" keyField="firstname" />
        </fl:reader>
    
  5. This is how you execute the map of person instances that is produced:
    Smooks smooks = new Smooks(configStream);
    JavaResult result = new JavaResult();
     
    smooks.filterSource(new StreamSource(fixedLengthStream), result);
     
    Map<String, Person> people = (Map<String, Person>) result.getBean("people");
     
    Person tom = people.get("Tom");
    Person mike = people.get("Maurice");
    
    Virtual Models are also supported, so you can define the class attribute as a java.util.Map and bind the fixed-length field values to map instances, which are in turn added to a list or a map.

4.10. Configuring the Fixed-Length Reader Programmatically

  1. Use this code to configure the fixed-length reader to read a person record set, binding the record set into a list of person instances:
    Smooks smooks = new Smooks();
     
    smooks.setReaderConfig(new FixedLengthReaderConfigurator("firstname[10]?trim,lastname[10]?trim,gender[1],age[3]?trim,country[2]")
                      .setBinding(new FixedLengthBinding("people", Person.class, FixedLengthBindingType.LIST)));
     
    JavaResult result = new JavaResult();
    smooks.filterSource(new StreamSource(fixedLengthStream), result);
     
    List<Person> people = (List<Person>) result.getBean("people");
    
    Configuring the Java binding is not mandatory. You can instead programmatically configure the Smooks instance to use other visitor implementations to carry out various forms of processing on the fixed-length record set.
  2. To bind fixed-length records directly to a list or map of a Java type that reflects the data in your fixed-length records, use either the FixedLengthListBinder or the FixedLengthMapBinder classes:
    // Note: The binder instance should be cached and reused...
    FixedLengthListBinder binder = new FixedLengthListBinder("firstname[10]?trim,lastname[10]?trim,gender[1],age[3]?trim,country[2]", Person.class);
     
    List<Person> people = binder.bind(fixedLengthStream);
    
    FixedLengthMapBinder:
    
    // Note: The binder instance should be cached and reused...
    FixedLengthMapBinder binder = new FixedLengthMapBinder("firstname[10]?trim,lastname[10]?trim,gender[1],age[3]?trim,country[2]", Person.class, "firstname");
     
    Map<String, Person> people = binder.bind(fixedLengthStream);
    
    If you need more control over the binding process, revert back to the lower level APIs.

4.11. EDI Processing

  1. To utilize EDI processing in Smooks, access the http://www.milyn.org/xsd/smooks/edi-1.2.xsd configuration namespace.
  2. Modify this configuration to suit your needs:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:edi="http://www.milyn.org/xsd/smooks/edi-1.2.xsd">    
     <!--
        Configure the EDI Reader to parse the message stream into a stream of SAX events.
        -->
        <edi:reader mappingModel="edi-to-xml-order-mapping.xml" validate="false"/>
    </smooks-resource-list>
    

4.12. EDI Processing Terms

  • mappingModel: This defines the EDI mapping model configuration for converting the EDI message stream to a stream of SAX events that can be processed by Smooks.
  • validate: This attribute turns the data-type validation in the EDI Parser on and off. (Validation is on by default.) To avoid redundancy, turn data-type validation off on the EDI reader if the EDI data is being bound to a Java object model (using Java bindings a la jb:bean).

4.13. EDI to SAX

The EDI to SAX event mapping process is based on a mapping model supplied to the EDI reader. (This model must always use the http://www.milyn.org/xsd/smooks/edi-1.2.xsd schema. From this schema, you can see that segment groups are supported, including groups within groups, repeating segments and repeating segment groups.)
The medi:segment element supports two optional attributes, minOccurs and maxOccurs. (There is a default value of one in each case.) Use these attributes to control the characteristics of a segment. A maxOccurs value of -1 indicates that the segment can repeat any number of times in that location of the (unbound) EDI message.
You can add segment groups by using the segmentGroup element. A segment group is matched to the first segment in the group. They can contain nested segmentGroup elements, but the first element in a segmentGroup must be a segment. segmentGroup elements support minOccurs and maxOccurs cardinality. They also support an optional xmlTag attribute which, if present, will result in the XML generated by a matched segment group to be inserted into an element that has the name of the xmlTag attribute value.

4.14. EDI to SAX Event Mapping

When mapping EDI to SAX events, segments are matched in either of these ways:
  • by an exact match on the segment code (segcode).
  • by a regex pattern match on the full segment, where the segcode attribute defines the regex pattern (for instance, segcode="1A\*a.*").
  • required: field, component and sub-component configurations support a "required" attribute, which flags that field, component or sub-component as requiring a value.
  • by default, values are not required (fields, components and sub-components).
  • truncatable: segment, field and component configurations support a "truncatable" attribute. For a segment, this means that parser errors will not be generated when that segment does not specify trailing fields that are not "required" (see "required" attribute above). Likewise for fields/components and components/sub-components.
  • By default, segments, fields, and components are not truncatable.
So, a field, component or a sub-component can be present in a message in one of the following states:
  • present with a value (required="true")
  • present without a value (required="false")
  • absent (required="false" and truncatable="true")

4.15. Segment Definitions

It is possible to reuse segment definitions. Below is a configuration that demonstrates the importation feature:
    
<?xml version="1.0" encoding="UTF-8"?>
<medi:edimap xmlns:medi="http://www.milyn.org/schema/edi-message-mapping-1.2.xsd">
 
    <medi:import truncatableSegments="true" truncatableFields="true" truncatableComponents="true" resource="example/edi-segment-definition.xml" namespace="def"/>
 
    <medi:description name="DVD Order" version="1.0"/>
 
    <medi:delimiters segment="
" field="*" component="^" sub-component="~" escape="?"/>
 
    <medi:segments xmltag="Order">
        <medi:segment minOccurs="0" maxOccurs="1" segref="def:HDR" segcode="HDR" xmltag="header"/>
        <medi:segment minOccurs="0" maxOccurs="1" segref="def:CUS" segcode="CUS" xmltag="customer-details"/>
        <medi:segment minOccurs="0" maxOccurs="-1" segref="def:ORD" segcode="ORD" xmltag="order-item"/>
    </medi:segments>
 
</medi:edimap> 

4.16. Segment Terms

Segments and segments containing child segments can be separated into another file for easier future reuse.
  • segref: This contains a namespace:name referencing the segment to import.
  • truncatableSegments: This overrides the truncatableSegments specified in the imported resource mapping file.
  • truncatableFields: This overrides the truncatableFields specified in the imported resource mapping file.
  • truncatableComponents: This overrides the truncatableComponents specified in the imported resource mapping file.

4.17. The Type Attribute

The example below demonstrates support for the type attribute.
 
<medi:edimap xmlns:medi="http://www.milyn.org/schema/edi-message-mapping-1.2.xsd">
 
    <medi:description name="Segment Definition DVD Order" version="1.0"/>
 
    <medi:delimiters segment="
" field="*" component="^" sub-component="~" escape="?"/>
 
    <medi:segments xmltag="Order">
 
        <medi:segment segcode="HDR" xmltag="header">
            <medi:field xmltag="order-id"/>
            <medi:field xmltag="status-code" type="Integer"/>
            <medi:field xmltag="net-amount" type="BigDecimal"/>
            <medi:field xmltag="total-amount" type="BigDecimal"/>
            <medi:field xmltag="tax" type="BigDecimal"/>
            <medi:field xmltag="date" type="Date" typeParameters="format=yyyyHHmm"/>
        </medi:segment>
 
    </medi:segments>
 
</medi:edimap>
You can use type system for different things, including:
  • field validation
  • Edifact Java Compilation

4.18. The EDIReaderConfigurator

  • Use the EDIReaderConfigurator to programmatically configure the Smooks instance to use the EDIReader as shown in the code below:
    Smooks smooks = new Smooks();
     
    // Create and initialise the Smooks config for the parser...
    smooks.setReaderConfig(new EDIReaderConfigurator("/edi/models/invoice.xml"));
     
    // Use the smooks as normal
    smooks.filterSource(....);
    

4.19. The Edifact Java Compiler

The Edifact Java Compiler simplifies the process of going from EDI to Java. It generates the following:
  • a Java object model for a given EDI mapping model.
  • a Smooks Java binding configuration to populate the Java Object model from an instance of the EDI message described by the EDI mapping model.
  • a factory class to use the Edifact Java Compiler to bind EDI data to the Java object model.

4.20. Edifact Java Compiler Example

The Edifact Java Compiler allows you to write simple Java code such as the following:
// Create an instance of the EJC generated Factory class.  This should normally be cached and reused...
OrderFactory orderFactory = OrderFactory.getInstance();
 
// Bind the EDI message stream data into the EJC generated Order model...
Order order = orderFactory.fromEDI(ediStream);
 
// Process the order data...
Header header = order.getHeader();
Name name = header.getCustomerDetails().getName();
List<OrderItem> orderItems = order.getOrderItems();

4.21. Executing the Edifact Java Compiler

  • To execute the Edifact Java Compiler through Maven, add the plug-in in your POM file:
    <build>
        <plugins>
            <plugin>
                <groupId>org.milyn</groupId>
                <artifactId>maven-ejc-plugin</artifactId>
                <version>1.2</version>
                <configuration>
                    <ediMappingFile>edi-model.xml</ediMappingFile>
                    <packageName>com.acme.order.model</packageName>
                </configuration>
                <executions>
                    <execution><goals><goal>generate</goal></goals></execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    

4.22. Maven Plug-in Parameters for the Edifact Java Compiler

The plug-in has three configuration parameters:
  • ediMappingFile: the path to the EDI mapping model file within the Maven project. (It is optional. The default is src/main/resources/edi-model.xml).
  • packageName:the Java package the generated Java artifacts are placed into (the Java object model and the factory class).
  • destDir: the directory in which the generated artifacts are created and compiled. (This is optional. The default is target/ejc).

4.23. Executing the Edifact Java Compiler with Ant

  • Create and execute the EJC task as shown below:
    <target name="ejc">
     
        <taskdef resource="org/milyn/ejc/ant/anttasks.properties">
            <classpath><fileset dir="/smooks-1.2/lib" includes="*.jar"/></classpath>
        </taskdef>
     
        <ejc edimappingmodel="src/main/resources/edi-model.xml"
             destdir="src/main/java"
             packagename="com.acme.order.model"/>
     
        <!-- Ant as usual from here on... compile and jar the source... -->
     
    </target>
    

4.24. UN/EDIFACT Message Interchanges

The easiest way to learn more about the Edifact Java Compiler is to check out the EJC example, UN/EDIFACT.
Smooks provides out-of-the-box support for UN/EDIFACT message interchanges by way of these means:
  • pre-generated EDI mapping models generated from the official UN/EDIFACT message definition ZIP directories. These allow you to convert a UN/EDIFACT message interchange into a more readily consumable XML format.
  • pre-generated Java bindings for easy reading and writing of UN/EDIFACT interchanges using pure Java

4.25. Using UN/EDIFACT Interchanges with the edi:reader

  • <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:unedifact="http://www.milyn.org/xsd/smooks/unedifact-1.4.xsd">
     
        <unedifact:reader mappingModel="urn:org.milyn.edi.unedifact:d03b-mapping:v1.4" ignoreNewLines="true" />
     
    </smooks-resource-list>
    
    The mappingModel attribute defines an URN that refers to the mapping model ZIP set's Maven artifact, which is used by the reader.

4.26. Configuring Smooks to Consume a UN/EDIFACT Interchange

  1. To programmatically configure Smooks to consume a UN/EDIFACT interchange (via, for instance, an UNEdifactReaderConfigurator), use the code below:
    Smooks smooks = new Smooks();
     
    smooks.setReaderConfig(new UNEdifactReaderConfigurator("urn:org.milyn.edi.unedifact:d03b-mapping:v1.4"));
    
  2. Insert the following on the containing application's classpath:
    • the requisite EDI mapping models
    • the Smooks EDI cartridge
  3. There may be some Maven dependancies your configuration will require. See the example below:
    <dependency>
        <groupId>org.milyn</groupId>
        <artifactId>milyn-smooks-edi</artifactId>
        <version>1.4</version>
    </dependency>
     
    <!-- Required Mapping Models -->
    <dependency>
        <groupId>org.milyn.edi.unedifact</groupId>
        <artifactId>d93a-mapping</artifactId>
        <version>v1.4</version>
    </dependency>
    <dependency>
        <groupId>org.milyn.edi.unedifact</groupId>
        <artifactId>d03b-mapping</artifactId>
        <version>v1.4</version>
    </dependency>
    
  4. Once an application has added an EDI mapping model ZIP set to its classpath, you can configure Smooks to use this model by simply referencing the Maven artifact using a URN as the unedifact:reader configuration's mappingModel attribute value:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:unedifact="http://www.milyn.org/xsd/smooks/unedifact-1.4.xsd"
     
        <unedifact:reader mappingModel="urn:org.milyn.edi.unedifact:d03b-mapping:v1.4" ignoreNewLines="true" />
     
    </smooks-resource-list>
    

4.27. The mappingModel

The mappingModel attribute can define multiple, comma-separated EDI Mapping Models URNs . By doing so, it facilitates the UN/EDIFACT reader process interchanges which deal with multiple UN/EDIFACT messages defined in different directories.
Mapping model ZIP sets are available for all of the UN/EDIFACT directories. Obtain them from the MavenSNAPSHOT and Central repositories and add them to your application by using standard Maven dependency management.

4.28. Configuring the mappingModel

  1. To add the D93A mapping model ZIP set to your application classpath, set the following dependency to your application's POM file:
    <!-- The mapping model sip set for the D93A directory... -->
    <dependency>
        <groupId>org.milyn.edi.unedifact</groupId>
        <artifactId>d93a-mapping</artifactId>
        <version>v1.4</version>
    </dependency>
    
  2. Configure Smooks to use this ZIP set by adding the unedifact:reader configuration to your Smooks configuration as shown below:
    <unedifact:reader mappingModel="urn:org.milyn.edi.unedifact:d93a-mapping:v1.4" />
    
    Note how you configure the reader using a URN derived from the Maven artifac's dependency information.
  3. You can also add multiple mapping model ZIP sets to your application's classpath. To do so, add all of them to your unedifact:reader configuration by comma-separating the URNs.
  4. Pre-generated Java binding model sets are provided with the tool (there is one per mapping model ZIP set). Use these to process UN/EDIFACT interchanges using a very simple, generated factory class.

4.29. Processing a D03B UN/EDIFACT Message Interchange

  1. To process a D03B UN/EDIFACT message interchange, follow the example below:
    
    Reading:
    
    // Create an instance of the EJC generated factory class... cache this and reuse !!!
    D03BInterchangeFactory factory = D03BInterchangeFactory.getInstance();
     
    // Deserialize the UN/EDIFACT interchange stream to Java...
    UNEdifactInterchange interchange = factory.fromUNEdifact(ediInStream);
     
    // Need to test which interchange syntax version.  Supports v4.1 at the moment...
    if(interchange instanceof UNEdifactInterchange41) {
        UNEdifactInterchange41 interchange41 = (UNEdifactInterchange41) interchange;
     
        for(UNEdifactMessage41 message : interchange41.getMessages()) {
            // Process the messages...
     
            Object messageObj = message.getMessage();
     
            if(messageObj instanceof Invoic) {
                // It's an INVOIC message....
                Invoic invoic = (Invoic) messageObj;
                ItemDescription itemDescription = invoic.getItemDescription();
                // etc etc....
            } else if(messageObj instanceof Cuscar) {
                // It's a CUSCAR message...
            } else if(etc etc etc...) {
                // etc etc etc...
            }
        }
    }
    
    
    Writing:
    
    factory.toUNEdifact(interchange, ediOutStream);
    
    
  2. Use Maven to add the ability to process a D03B message interchange by adding the binding dependency for that directory (you can also use pre-generated UN/EDIFACT Java object models distributed via the MavenSNAPSHOT and Central repositories):
    <dependency>
        <groupId>org.milyn.edi.unedifact</groupId>
        <artifactId>d03b-binding</artifactId>
        <version>v1.4</version>
    </dependency>
    

4.30. Processing JSON Data

  1. To process JSON data, you must configure a JSON reader:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:json="http://www.milyn.org/xsd/smooks/json-1.1.xsd">
     
        <json:reader/>
     
    </smooks-resource-list>
    
  2. Set the XML names of the root, document and array elements by using the following configuration options:
    • rootName: this is the name of the root element. The default is yaml.
    • elementName: this is the name of a sequence element. The default is element.
  3. You may wish to use characters in the key name that are not allowed in the XML element name. The reader offers multiple solutions to this problem. It can search and replace white spaces, illegal characters and the number in key names that start with a number. You can also use it to replace one key name with a completely different one. The following sample code shows you how to do this:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:json="http://www.milyn.org/xsd/smooks/json-1.1.xsd">
     
        <json:reader keyWhitspaceReplacement="_" keyPrefixOnNumeric="n" illegalElementNameCharReplacement=".">
            <json:keyMap>
                <json:key from="some key">someKey</json:key>
                <json:key from="some&key" to="someAndKey" />
            </json:keyMap>
        </json:reader>
     
    </smooks-resource-list>
    
    • keyWhitspaceReplacement: this is the replacement character for white spaces in a JSON map key. By default this is not defined, so the reader does not automatically search for white spaces.
    • keyPrefixOnNumeric: this is the prefix character to add if the JSON node name starts with a number. By default, this is not defined, so the reader does not search for element names that start with a number.
    • illegalElementNameCharReplacement: if illegal characters are encountered in a JSON element name then they are replaced with this value.
  4. You can also configure these optional settings:
    • nullValueReplacement: this is the replacement string for JSON null values. The default is an empty string.
    • encoding: this is the default encoding of any JSON message InputStream processed by the reader. The default encoding is UTF-8.

      Note

      This feature is deprecated. Instead, you should now manage the JSON streamsource character encoding by supplying a java.io.Reader to the Smooks.filterSource() method.
  5. To configure Smooks programmatically to read a JSON configuration, use the JSONReaderConfiguratorclass:
    Smooks smooks = new Smooks();
     
    smooks.setReaderConfig(new JSONReaderConfigurator()
            .setRootName("root")
            .setArrayElementName("e"));
     
    // Use Smooks as normal...
    

4.31. Using Characters Not Allowed in XML when Processing JSON Data

To use characters in the key name that are not allowed in the XML element name, use the reader to search and replace white spaces, illegal characters and the number in key names that start with a number. You can also use it to replace one key name with a completely different one. The following sample code shows you how to do this:
<?xml version="1.0"?>
<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:json="http://www.milyn.org/xsd/smooks/json-1.1.xsd">
 
    <json:reader keyWhitspaceReplacement="_" keyPrefixOnNumeric="n" illegalElementNameCharReplacement=".">
        <json:keyMap>
            <json:key from="some key">someKey</json:key>
            <json:key from="some&key" to="someAndKey" />
        </json:keyMap>
    </json:reader>
 
</smooks-resource-list>
  • keyWhitspaceReplacement: this is the replacement character for white spaces in a JSON map key. By default this is not defined, so the reader does not automatically search for white spaces.
  • keyPrefixOnNumeric: this is the prefix character to add if the JSON node name starts with a number. By default, this is not defined, so the reader does not search for element names that start with a number.
  • illegalElementNameCharReplacement: if illegal characters are encountered in a JSON element name then they are replaced with this value.
These settings are optional:
  • nullValueReplacement: this is the replacement string for JSON null values. The default is an empty string.
  • encoding: this is the default encoding of any JSON message InputStream processed by the reader. The default encoding is UTF-8.

    Note

    This feature is deprecated. Instead, you should now manage the JSON streamsource character encoding by supplying a java.io.Reader to the Smooks.filterSource() method.

4.32. Configuring YAML Streams

Procedure 4.1. Task

  1. Configure your reader to process YAML files as shown:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:yaml="http://www.milyn.org/xsd/smooks/yaml-1.4.xsd">
     
        <yaml:reader/>
     
    </smooks-resource-list>
    
  2. Configure the YAML stream to contain multiple documents. The reader handles this by adding a document element as a child of the root element. An XML-serialized YAML stream with one empty YAML document looks like this:
    <yaml>
        <document>
        </document>
    </yaml>
    
  3. Configure Smooks programmatically to read a YAML configuration by exploiting the YamlReaderConfigurator class:
    Smooks smooks = new Smooks();
     
    smooks.setReaderConfig(new YamlReaderConfigurator()
            .setRootName("root")
            .setDocumentName("doc")
            .setArrayElementName("e"))
            .setAliasStrategy(AliasStrategy.REFER_RESOLVE)
            .setAnchorAttributeName("anchor")
            .setAliasAttributeName("alias");
     
    // Use Smooks as normal...
    

4.33. Supported Result Types

Smooks can work with standard JDK StreamResult and DOMResult result types, as well as these specialist ones:
  • JavaResult: use this result type to capture the contents of the Smooks Java Bean context.
  • ValidationResult: use this result type to capture outputs.
  • Simple Result type: use this when writing tests. This is a StreamResult extension wrapping a StringWriter.

4.34. Using Characters Not Allowed in XML when Processing YAML Data

  • You can use characters in the key name that are not allowed in the XML element name. The reader offers multiple solutions to this problem. It can search and replace white spaces, illegal characters and the number in key names that start with a number. You can configure it to replace one key name with a completely different one, as shown below:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:yaml="http://www.milyn.org/xsd/smooks/yaml-1.4.xsd">
     
        <yaml:reader keyWhitspaceReplacement="_" keyPrefixOnNumeric="n" illegalElementNameCharReplacement=".">
            <yaml:keyMap>
                <yaml:key from="some key">someKey</yaml:key>
                <yaml:key from="some&key" to="someAndKey" />
            </yaml:keyMap>
        </yaml:reader>
     
    </smooks-resource-list>
    

4.35. Options for Replacing XML in YAML

  • keyWhitspaceReplacement: This is the replacement character for white spaces in a YAML map key. By default this not defined.
  • keyPrefixOnNumeric: Add this prefix if the YAML node name starts with a number. By default this is not defined.
  • illegalElementNameCharReplacement: If illegal characters are encountered in a YAML element name, they are replaced with this value. By default this is not defined.

4.36. Anchors and Aliases in YAML

The YAML reader can handle anchors and aliases via three different strategies. Define your strategy of choice via the aliasStrategy configuration option. This option can have one of the following values:
  • REFER: The reader creates reference attributes on the element that has an anchor or an alias. The element with the anchor obtains the id attribute containing the name from the anchor as the attribute value. The element with the alias gets the ref attribute also containing the name of the anchor as the attribute value. You can define the anchor and alias attribute names by setting the anchorAttributeName and aliasAttributeName properties.
  • RESOLVE: The reader resolves the value or the data structure of an anchor when its alias is encountered. This means that the SAX events of the anchor are repeated as child events of the alias element. When a YAML document contains a lot of anchors or anchors and a substantial data structure this can lead to memory problems.
  • REFER_RESOLVE: This is a combination of REFER and RESOLVE. The anchor and alias attributes are set but the anchor value or data structure is also resolved. This option is useful when the name of the anchor has a business meaning.
The YAML reader uses the REFER strategy by default.

4.37. Java Object Graph Transformation

  1. Smooks can transform one Java object graph into another. To do this, it uses the SAX processing model, which means no intermediate object model is constructed. Instead, the source Java object graph is turned directly into a stream of SAX events, which are used to populate the target Java object graph.
    If you use the HTML Smooks Report Generator tool, you will see that the event stream produced by the source object model is as follows:
    <example.srcmodel.Order>
        <header>
            <customerNumber>
                </customerNumber>
               <customerName>
           </customerName>;
        </header>
        <orderItems>
            <example.srcmodel.OrderItem>
                <productId>
               </productId>
                <quantity>
                >/quantity>
                <price>
                </price>
            </example.srcmodel.OrderItem>
        </orderItems>
    </example.srcmodel.Order>
    
  2. Aim the Smooks Java bean resources at this event stream. The Smooks configuration for performing this transformation (smooks-config.xml) is as follows:
    <?xml version="1.0"?>
    <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">
     
        <jb:bean BeanId="lineOrder" class="example.trgmodel.LineOrder" createOnElement="example.srcmodel.Order">
            <jb:wiring property="lineItems" BeanIdRef="lineItems" />
            <jb:value property="customerId" data="header/customerNumber" />
            <jb:value property="customerName" data="header/customerName" />
        </jb:bean<
     
        <jb:bean BeanId="lineItems" class="example.trgmodel.LineItem[]" createOnElement="orderItems">
            <jb:wiring BeanIdRef="lineItem" />
        </jb:bean>
     
     
        <jb:bean BeanId="lineItem" class="example.trgmodel.LineItem" createOnElement="example.srcmodel.OrderItem">
            <jb:value property="productCode" data="example.srcmodel.OrderItem/productId" />
            <jb:value property="unitQuantity" data="example.srcmodel.OrderItem/quantity" />
            <jb:value property="unitPrice" data="example.srcmodel.OrderItem/price" />
        </jb:bean>
     
    </smooks-resource-list>
    
  3. The source object model is provided to Smooks via a org.milyn.delivery.JavaSource object. Create this object by passing the constructor the source model's root object. The resulting Java Source object is used in the Smooks#filter method. Here is the resulting code:
    protected LineOrder runSmooksTransform(Order srcOrder) throws IOException, SAXException {
        Smooks smooks = new Smooks("smooks-config.xml");
        ExecutionContext executionContext = smooks.createExecutionContext();
     
        // Transform the source Order to the target LineOrder via a
        // JavaSource and JavaResult instance...
        JavaSource source = new JavaSource(srcOrder);
        JavaResult result = new JavaResult();
     
        // Configure the execution context to generate a report...
        executionContext.setEventListener(new HtmlReportGenerator("target/report/report.html"));
     
        smooks.filterSource(executionContext, source, result);
     
        return (LineOrder) result.getBean("lineOrder");
    }
    

4.38. String Manipulation on Input Data

The CSV and fixed-length readers allow you to execute string manipulation functions on the input data before the data is converted into SAX events. The following functions are available:
  • upper_case: this returns the upper case version of the string.
  • lower_case: this returns the lower case version of the string.
  • cap_first: this returns the string with the very first word capitalized.
  • uncap_first: this returns the string with the very first word un-capitalized. It is the opposite of cap_first.
  • capitalize: this returns the string with all words capitalized.
  • trim: this returns the string without leading and trailing white-spaces.
  • left_trim: this returns the string without leading white-spaces.
  • right_trim: this returns the string without trailing white-spaces.
You can chain functions via the point separator. Here is an example: trim.upper_case
How you define the functions per field depends on the reader you are using.

Chapter 5. Validation

5.1. Rules in Smooks

In Smooks, rules are a general concept, not something specific to a particular cartridge.
You can configure and reference a RuleProvider from other components.

Note

The only cartridge that uses rules functionality is the validation cartridge.
Rules are centrally defined through ruleBases. A single Smooks configuration can refer to many ruleBase definitions. A rulesBase configuration has a name, a rule src and a rule provider.
The format of the rule source is entirely dependent on the provider implementation. The only requirement is that the individual rules be uniquely named (within the context of a single source) so they can be referenced.

5.2. Configuring Rules in Smooks

Here is an example ruleBase configuration:
<?xml version="1.0"?>
<smooks-resource-list  xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"  xmlns:rules="http://www.milyn.org/xsd/smooks/rules-1.0.xsd">
 
    <rules:ruleBases>
        <rules:ruleBase name="regexAddressing" src="/org/milyn/validation/address.properties" provider="org.milyn.rules.regex.RegexProvider" />
        <rules:ruleBase name="order" src="/org/milyn/validation/order/rules/order-rules.csv" provider="org.milyn.rules.mvel.MVELProvider"/>
    </rules:ruleBases>
 
</smooks-resource-list>

5.3. Mandatory Configurations for the rules:ruleBase Configuration Element

  • name: this is used by other components to refer to this rule.
  • src: this can be a file or anything else that is meaningful to the RuleProvider.
  • provider: This is the actual provider implementation. In the configuration above, there is one RuleProvider that uses regular expressions but you can specify multiple ruleBase elements and have as many RuleProviders as you need. .

5.4. Rule Providers

Rule providers implement the org.milyn.rules.RuleProviderinterface.
Smooks comes pre-configured to support two RuleProvider implementations:
  • RegexProvider
  • MVELProvider
You can also create your own RuleProvider implementations.

5.5. The RegexProvider

The RegexProvider utilises regular expressions. It allows you to define low-level rules specific to the format of selected data fields in the message being filtered. For example, it may be applied to a particular field to validate the syntax to make sure the right e-mail address is being used.

5.6. Configuring a Regex ruleBase

  1. Use this example code to configure a Regex ruleBase:
    <?xml version="1.0"?>
    <smooks-resource-list  xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"  xmlns:rules="http://www.milyn.org/xsd/smooks/rules-1.0.xsd">
     
        <rules:ruleBases>
            <rules:ruleBase name="customer" src="/org/milyn/validation/order/rules/customer.properties" provider="org.milyn.rules.regex.RegexProvider"/>
        </rules:ruleBases>
     
    </smooks-resource-list>
    
  2. Define the Regex expressions in a standard .properties file format. The following customer.properties Regex rule definition file example shows you how:
    # Customer data rules...
    customerId=[A-Z][0-9]{5}
    customerName=[A-Z][a-z]*, [A-Z][a-z]
    

5.7. The MVEL Provider

The MVEL provider allows you to define rules as MVEL expressions. These expressions are executed over the contents of the Smooks Javabean context. You should to bind data from the message being filtered into Java objects in the Smooks bean context.
MVEL allows you to define more complex rules on message fragments. (Such as "Is the product in the targeted order item fragment within the age eligibility constraints of the customer specified in the order header details?")

5.8. Configuring an MVEL ruleBase

  1. To configure an MVEL ruleBase, see the code below:
    <?xml version="1.0"?>
    <smooks-resource-list  xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"  xmlns:rules="http://www.milyn.org/xsd/smooks/rules-1.0.xsd">
     
        <rules:ruleBases>
            <rules:ruleBase name="order" src="/org/milyn/validation/order/rules/order-rules.csv" provider="org.milyn.rules.mvel.MVELProvider"/>
        </rules:ruleBases>
     
    </smooks-resource-list>
    
  2. You must store your MVEL rules in CSV files. The easiest way to edit these files is through a spreadsheet application such as LibreOffice Calc or Gnumeric. Each rule record contains a rule name and an MVEL expression.
  3. If you wish to create comment and header rows, prefix the first field with a hash (#) character.

5.9. The Smooks Validation Cartridge

The Smooks validation cartridge works with the rules cartridge to provide rules-based fragment validation.
This allows you to perform detailed validation on message fragments. As with everything in Smooks, the validation functionality is available across all supported data formats. This means you can perform strong validation on not just XML data, but also on EDI, JSON, CSV and so on.
Validation configurations are defined by the http://www.milyn.org/xsd/smooks/validation-1.0.xsd configuration namespace.
Smooks supports a number of different rule provider types and these can all be used by the Validation Cartridge. Each of these provide a different level of validation but they are all configured in exactly the same way. The Smooks Validation Cartridge sees a rule provider as an abstract resource that it can aim at message fragments in order to validate them.

5.10. Configuring Validation Rules

To configure a validation rule you need to specify the following:
  • executeOn: this is the fragment on which the rule is to be executed.
  • excecuteOnNS: this is the fragment namespace to which the executeOn belongs.
  • name: this is the name of the rule to be applied. This is a composite rule name that refers to a ruleBase and ruleName combination in a dot delimited format (in other words ruleBaseName.ruleName).
  • onFail: this determines the severity of a failed match.
Here is a sample validation rule configuration:
    <validation:rule executeOn="order/header/email" name="regexAddressing.email" onFail="ERROR" />

5.11. Configuring Validation Exceptions

  1. You can set a maximum number of validation failures per Smooks filter operation. (An exception will be thrown if this maximum is exceeded.) Validations configured with OnFail.FATAL will always throw an exception and stop processing.
    To configure the maximum validation failures, add this code to your Smooks configuration:
    <params>
        <param name="validation.maxFails">5</param>
    </params>
    
  2. The onFail attribute in the validation configuration specifies what action is to be taken. This determines how validation failures are to be reported. To utilize it, modify the following options to suit your needs:
    • OK: Use this to save the validation as "okay". By calling ValidationResults.getOks all validation warnings will be returned. This option is useful for content-based routing.
    • WARN: Use this to save the validation as a warning. By calling ValidationResults.getWarnings all validation warnings will be returned.
    • ERROR: Use this to save the validation as an error. By calling ValidationResults.getErrors you will return all validation errors.
    • FATAL: Use this to throw a ValidationException as soon as a validation failure occurs. If you call ValidationResults.getFatal you will see the fatal validation failure.

5.12. Rule Bases

  • Use a composite rule name in the following format for a rule base:
    <ruleProviderName>.<ruleName>
    
    • ruleProviderName identifies the rule provider and maps to the name attribute in the ruleBase element.
    • ruleName identifies a specific rule the rule provider knows about. This could be a rule defined in the src file.

5.13. Smooks.filterSource

The Smooks.filterSource captures the validation results. When the filterSource method returns, the ValidationResult instance will contain all validation data.
This code shows how to make Smooks perform message fragment validation:
ValidationResult validationResult = new ValidationResult();
 
smooks.filterSource(new StreamSource(messageInStream), new StreamResult(messageOutStream), validationResult);
 
List<OnFailResult> errors = validationResult.getErrors();
List<OnFailResult> warnings = validationResult.getWarnings();
As you can see, individual warning and error validation results are made available from the ValidationResult object in the form of OnFailResult instances, each of which provide details about an individual failure.
The Validation Cartridge also allows you to specify localized messages relating to validation failures. You can define these messages in standard Java ResourceBundle files (which use the .properties format).

Note

The base name of the validation message bundle is derived from the rule source ("src") by dropping the rule source file extension and adding an extra folder named i18n. For example, if you have an MVEL ruleBase source of /org/milyn/validation/order/rules/order-rules.csv, the corresponding validation message bundle base name will be /org/milyn/validation/order/rules/i18n/order-rules.

5.14. The Validation Cartridge and Messages

The validation cartridge lets you apply FreeMarker templates to the localized messages, allowing messages to contain contextual data from the bean context, as well as data about the actual rule failure. You must prefix FreeMarker-based messages with ftl: and reference the contextual data using standard FreeMarker notation. The beans from the bean context can be referenced directly, while you can refer to the RuleEvalResult and rule failure path through the ruleResult and path beans.
Here is an example that uses RegexProvider rules which shows how Smooks can be used to perform validation of message fragment data:
customerId=ftl:Invalid customer number '${ruleResult.text}' at '${path}'.  Customer number must match pattern '${ruleResult.pattern}'.

5.15. Types of Validation

Smooks performs two types of validation using two different kinds of validation rules:
  • message field value/format validation using regular expressions defined in a .properties file RuleBase. This, for example, can be to validate a field as being a valid e-mail address.
  • business rules validation using MVEL expressions defined in a .csv file RuleBase. This can, for example, be validating that the total price of an order item on an order (price * quantity) does not breach some predefined business rule.

5.16. Running Validation Rules

  • To run validaton rules, go to the example root folder and execute:
    1. mvn clean install
    2. mvn exec:java

5.17. RuleBase Example

In this example, there is an XML message containing a collection of order items. (This functionality works similarly for all other data formats supported by Smooks.):
<Order>
	<header>
        <orderId>A188127</orderId>
        <username>user1</username>
        <name>
            <firstname>Bob</firstname>
            <lastname>Bobington</lastname>
        </name>
        <email>bb@awesomemail.com</email>
        <state>Queensland</state>
    </header>
    <order-item>
        <quantity>1</quantity>
        <productId>364b</productId>
        <title>A Great Movie</title>
        <price>29.95</price>
    </order-item>
    <order-item>
        <quantity>2</quantity>
        <productId>299</productId>
        <title>A Terrible Movie</title>
        <price>29.95</price>
    </order-item>
</Order>

5.18. Message Data Validation

  1. When processing an order message, you should perform a number of validations. First, check that the supplied username follows a format of an upper case character, followed by five digits (for example, S12345 or G54321). To perform this validation, you should use regular expression.
  2. Next, check that the supplied e-mail address is in a valid format. Use a regular expression to check it.
  3. Confirm that each order item's productId field follows a format of exactly three digits (such as 123). Use a regular expression to do this.
  4. Finally, you need to confirm that the total for each order item does not exceed 50.00 (price * quantity is not greater than 50.00). Perform this validation using an MVEL expression.

5.19. Using an MVEL Expression

  1. To use an MVEL expression on a rule set, divide the Regex rules and place them in two separate .properties files.
  2. Drop these rules into the example rules directory.
  3. Put the MVEL expression in a .csv file, also in the rules directory.
    The customer-related Regex rules that go in the customer.properties file look like this:
    # Customer data rules...
    customerId=[A-Z][0-9]{5}
     
    # Email address...
    email=^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$
    
  4. Insert the product-related Regex rule in the product.properties file:
    # Product data rules...
    productId=[0-9]{3}
    
  5. Insert the MVEL expression for performing the order item total check into the order-rules.csv file.

    Note

    The easiest way to edit a .csv file is through using a spreadsheet application like LibeOffice Calc or Gnumeric.
  6. Create resource bundle .properties files for each of the rule source files.

    Note

    The names of these files are derived from the names of their corresponding rule files.
    The message bundle for the rules defined in rules/customer.properties is located in the rules/i18n/customer.properties file:
    customerId=ftl:Invalid customer number '${ruleResult.text}' at '${path}'.  
    <!--  Customer number must begin with an uppercase character, followed by 5 digits. -->
    email=ftl:Invalid email address '${ruleResult.text}' at '${path}'.  
    <!--  Email addresses match pattern '${ruleResult.pattern}'. -->
    
    The message bundle for the rule defined in rules/product.properties is located in the rules/i18n/product.properties file:
    # Product data rule messages...
    productId=ftl:Invalid product ID '${ruleResult.text}' at '${path}'. 
    <!--  Product ID must match pattern '${ruleResult.pattern}'. -->
    
    The message bundle for the rule defined in rules/order-rules.csv is located in the rules/i18n/order-rules.properties file:
    # <!--  Order item rule messages. The "orderDetails" and "orderItem" beans are populated by Smooks bindings - see config in following section. -->
    order_item_total=ftl:Order ${orderDetails.orderId} 
    <!-- contains an order item for product ${orderItem.productId} with a quantity of ${orderItem.quantity} and a unit price of ${orderItem.price}. This exceeds the permitted per order item total. -->
    
  7. Apply this validation to the rules:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"
                          xmlns:rules="http://www.milyn.org/xsd/smooks/rules-1.0.xsd"
                          xmlns:validation="http://www.milyn.org/xsd/smooks/validation-1.0.xsd"
                          xmlns:jb="http://www.milyn.org/xsd/smooks/javabean-1.2.xsd">
     
        <params>
            <!-- Generate a ValidationException if we get more than 5 validation failures... -->
            <param name="validation.maxFails">5</param>
        </params>
     
        <!-- Define the ruleBases that are used by the validation rules... -->
        <rules:ruleBases>
            <!-- Field value rules using regex... -->
            <rules:ruleBase name="customer" src="rules/customer.properties" provider="org.milyn.rules.regex.RegexProvider"/>
            <rules:ruleBase name="product" src="rules/product.properties" provider="org.milyn.rules.regex.RegexProvider"/>
     
            <!-- Order business rules using MVEL expressions... -->
            <rules:ruleBase name="order" src="rules/order-rules.csv" provider="org.milyn.rules.mvel.MVELProvider"/>
        </rules:ruleBases>
     
        <!-- Capture some data into the bean context - required by the business rule validations... -->
        <jb:bean beanId="orderDetails" class="java.util.HashMap" createOnElement="header">
            <jb:value data="header/*"/>
        </jb:bean>
        <jb:bean beanId="orderItem" class="java.util.HashMap" createOnElement="order-item">
            <jb:value data="order-item/*"/>
        </jb:bean>
     
        <!-- Target validation rules... -->
        <validation:rule executeOn="header/username" name="customer.customerId" onFail="ERROR"/>
        <validation:rule executeOn="email" name="customer.email" onFail="WARN"/>
        <validation:rule executeOn="order-item/productId" name="product.productId" onFail="ERROR"/>
     
        <validation:rule executeOn="order-item" name="order.order_item_total" onFail="ERROR"/>
     
    </smooks-resource-list>
    
  8. Execute from the example's Main class using this code:
    protected static ValidationResult runSmooks(final String messageIn) throws IOException, SAXException, SmooksException {
        // Instantiate Smooks with the config...
        final Smooks smooks = new Smooks("smooks-config.xml");
     
        try {
            // Create an exec context - no profiles....
            final ExecutionContext executionContext = smooks.createExecutionContext();
            final ValidationResult validationResult = new ValidationResult();
     
            // Configure the execution context to generate a report...
            executionContext.setEventListener(new HtmlReportGenerator("target/report/report.html"));
     
            // Filter the input message...
            smooks.filterSource(executionContext, new StringSource(messageIn), validationResult);
     
            return validationResult;
        }
        finally {
            smooks.close();
        }
    }
    

Chapter 6. Producing Output Data

6.1. The Smooks Javabean Cartridge

You can use the Smooks Javabean Cartridge to create and populate Java objects from your message data. It can be used purely as a Java binding framework for XML, EDI, CSV and so forth. However, Smooks' Java binding capabilities are also the cornerstone of many other capabilities. This is because Smooks makes the Java objects it creates (and to which it binds data) available through the BeanContext class. This class is essentially a Java bean context that is made available to any Smooks visitor implementation via the Smooks ExecutionContext.

6.2. Javabean Cartridge Features

  • Templating: This usually involves applying a template to the objects in the BeanContext.
  • Validation: Business rules validation normally involves applying a rule to the objects in the BeanContext.
  • Message splitting and routing: This works by generating split messages from the objects in the BeanContext, either by using the objects themselves and routing them, or by applying a template to them and routing the result of that operation to a new file.
  • Persistence: These features depend on the Java binding functions for creating and populating the Java objects (such as entities) that are to be committed to the database. Data read from a database will normally be bound to the BeanContext.
  • Message enrichment: Enrichment data (read, for example from a database) is normally bound to the BeanContext, from where it is available to all of Smooks' other features, including the Java binding functionality itself (making it available for expression-based bindings.) This allows you to enrich messages generated by Smooks.

6.3. Javabean Cartridge Example

The following example is based on this XML:
<order>
    <header>
        <date>Wed Nov 15 13:45:28 EST 2006</date>
        <customer number="123123">Joe</customer>
    </header>
    <order-items>
        <order-item>
            <product>111</product>
            <quantity>2</quantity>
            <price>8.90</price>
        </order-item>
        <order-item>
            <product>222</product>
            <quantity>7</quantity>
            <price>5.20</price>
        </order-item>
    </order-items>
</order>
The Javabean Cartridge is used via the http://www.milyn.org/xsd/smooks/javabean-1.4.xsd configuration namespace. (Install the schema in your IDE to avail yourself of the latter's auto-complete functionality.)
Here is an example configuration:
<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">
 
    <jb:bean BeanId="order" class="example.model.Order" createOnElement="#document" />
 
</smooks-resource-list>
This configuration creates an instance of the example.model.Order class and binds it to the bean context under the BeanId called order. The instance is created at the very start of the message on the #document element (in other words, at the start of the root order element).
The configuration shown above creates the example.model.Order bean instance and binds it to the bean context.

6.4. Javabean Elements

  • beanId: this is the bean's identifier.
  • class: this is the bean's fully-qualified class name.
  • createOnElement: use this attribute to control when the bean instance is to be created. You can control the population of the bean properties through the binding configurations (which are child elements of the jb:bean element).
  • createOnElementNS: you can specify the namespace of the createOnElement via this attribute.

6.5. Javabean Conditions

The Javabean Cartridge sets the following conditions to Java beans:
  • There is a public no-argument constructor.
  • There are public property setter methods. These do not need to follow any specific name formats, but it would be better if they do follow those for the standard property setter method names.
  • You cannot set Java bean properties directly.

6.6. Javabean Cartridge Data Bindings

These are the three types of data bindings the Javabean Cartridge allows for:
  • jb:value: use this to bind data values from the source message event stream to the target bean.
  • jb:wiring: use this to "plug" another bean instance from the bean context into a bean property on the target bean. You can use this configuration to construct an object graph (as opposed to a loose collection of Java object instances). You can plug beans in based on their beanId, their Java class type or their annotation.
  • jb:expression: use this configuration to bind a value calculated from an expression (in the MVEL language). A simple example is the ability to bind an order item total value to an OrderItem bean (based on the result of an expression that calculates price * quantity). Use the execOnElement attribute expression to define the element on which the expression is to be evaluated and to which the result will be bound. (If you do not define it, the expression is executed based on the value of the parent jb:bean createOnElement.) The value of the targeted element is available in the expression as a string variable under the name _VALUE (note the underscore).

6.7. Binding Data

  1. Using the Order XML message, look at the full XML-to-Java binding configuration. Here are the Java objects that you must populate from that XML message (the "getters" and "setters" are not shown):
    public class Order {
        private Header header;
        private List<OrderItem> orderItems;
    }
     
    public class Header {
        private Date date;
        private Long customerNumber;
        private String customerName;
        private double total;
    }
     
    public class OrderItem {
        private long productId;
        private Integer quantity;
        private double price;
    }
    
  2. Use this configuration to bind the data from the order XML to the object model:
    <?xml version="1.0"?>
    <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">
     
    (1)   <jb:bean beanId="order" class="com.acme.Order" createOnElement="order">
    (1.a)     <jb:wiring property="header" beanIdRef="header" />
    (1.b)     <jb:wiring property="orderItems" beanIdRef="orderItems" />
          </jb:bean>
     
    (2)   <jb:bean beanId="header" class="com.acme.Header" createOnElement="order">
    (2.a)     <jb:value property="date" decoder="Date" data="header/date">
                  <jb:decodeParam name="format">EEE MMM dd HH:mm:ss z yyyy</jb:decodeParam>
              </jb:value>
    (2.b)     <jb:value property="customerNumber" data="header/customer/@number" />
    (2.c)     <jb:value property="customerName" data="header/customer" />
    (2.d)     <jb:expression property="total" execOnElement="order-item" >
                  += (orderItem.price * orderItem.quantity);
              </jb:expression>
          </jb:bean>
     
    (3)   <jb:bean beanId="orderItems" class="java.util.ArrayList" createOnElement="order">
    (3.a)     <jb:wiring beanType="com.acme.OrderItem" /> <!-- Could also wire using beanIdRef="orderItem" -->
          </jb:bean>
     
    (4)   <jb:bean beanId="orderItem" class="com.acme.OrderItem" createOnElement="order-item">
    (4.a)     <jb:value property="productId" data="order-item/product" />
    (4.b)     <jb:value property="quantity" data="order-item/quantity" />
    (4.c)     <jb:value property="price" data="order-item/price" />
          </jb:bean>
     
    </smooks-resource-list>
    

6.8. Binding Data Configurations

Configuration (1) defines the creation rules for the com.acme.Order bean instance (the top level bean). See the following configurations for details:
  • You should create each of the beans instances ((1), (2), (3) but not (4)) at the very start of the message (on the order element). Do this because there will only ever be a single instance of these beans in the populated model.
  • Configurations (1.a) and (1.b) define the wiring configuration for wiring the Header and ListOrderItem bean instances ((2) and (3)) into the order bean instance (see the beanIdRef attribute values and how the reference the beanId values defined on (2) and (3)). The property attributes on (1.a) and (1.b) define the Order bean properties on which the wirings are to be made.
    Note also that beans can also be wired into an object based on their Java class type (beanType), or by being annotated with a specific Annotation (beanAnnotation).
    Configuration (2) creates the com.acme.Header bean instance.
  • Configuration (2.a) defines a value binding onto the Header.date property. Note that the data attribute defines where the binding value is selected from the source message; in this case it is coming from the header/date element. Also note how it defines a decodeParam sub-element. This configures the DateDecoder.
  • Configuration (2.b) defines a value binding configuration onto the Header.customerNumber property. Note how to configure the data attribute to select a binding value from an element attribute on the source message.
    Configuration (2.b) also defines an expression binding where the order total is calculated and set on the Header.total property. The execOnElement attribute tells Smooks that this expression needs to be evaluated (and bound/rebound) on the order-item element. So, if there are multiple order-item elements in the source message, this expression will be executed for each order-item and the new total value rebound into the Header.total property. Note how the expression adds the current orderItem total to the current order total (Header.total).
  • Configuration (2.d) defines an expression binding, where a running total is calculated by adding the total for each order item (quantity * price) to the current total. Configuration (3) creates the ListOrderItem bean instance for holding the OrderItem instances.
  • Configuration (3.a) wires all beans of type com.acme.OrderItem ( i.e. (4)) into the list. Note how this wiring does not define a property attribute. This is because it wires into a Collection (same applies if wiring into an array). You can also perform this wiring using the beanIdRef attribute instead of the beanType attribute.
  • Configuration (4) creates the OrderItem bean instances. Note how the createOnElement is set to the order-item element. This allows for a new instance of this bean to be created for every order-item element (and wired into the ListOrderItem (3.a)).
If the createOnElement attribute for this configuration was not set to the order-item element (if, for example, it was set to one of the order, header or order-items elements), then only a single OrderItem bean instance would be created and the binding configurations ((4.a) etc) would overwrite the bean instance property bindings for every order-item element in the source message, that is, you would be left with a ListOrderItem with just a single OrderItem instance containing the order-item data from the last order-item encountered in the source message.

6.9. Binding Tips

Here are some binding tips:
  • set jb:bean createOnElement to the root element (or #document) for bean instances where only a single instance will exist in the model.
    Set it to the recurring element for collection bean instances.

    Warning

    If you do not specify the correct element in this case, you could lose data.
  • jb:value decoder: in most cases, Smooks will automatically detect the data-type decoder to be used for a jb:value binding. However, some decoders require configuration (one example being that the DateDecoder [decoder="Date"]). In these cases, you must define the decoder attribute (and the jb:decodeParam child elements for specifying the decode parameters for that decoder) on the binding.
  • jb:wiring property is not required when binding to collections.
  • To set the required collection type, define the jb:bean class and wire in the collection entries. For arrays, just postfix the jb:bean class attribute value with square brackets (for example, class=&"com.acme.OrderItem[]").

6.10. DataDecoder/DataEncoder Implementations

The a DataEncoder implements methods for encoding and formatting an object value to a string. These DataDecoder/DataEncoder implementations are available:
  • Date: decodes/encodes a string to a java.util.Date instance.
  • Calendar: decodes/encodes a string to a java.util.Calendar instance.
  • SqlDate: decodes/encodes a string to a java.sql.Date instance.
  • SqlTime: decodes/encodes a string to a java.sql.Time instance.
  • SqlTimestamp: decodes/encodes a string to a java.sql.Timestamp instance.
You configure all of these implementations in the same way.

6.11. DataDecoder/DataEncoder Date Example

Here is a date example:
<jb:value property="date" decoder="Date" data="order/@date">
    <jb:decodeParam name="format">EEE MMM dd HH:mm:ss z yyyy</jb:decodeParam>
    <jb:decodeParam name="locale">sv_SE</jb:decodeParam>
</jb:value>

6.12. DataDecoder/DataEncoder SqlTimestamp Example

Here is an SqlTimestamp example:
<jb:value property="date" decoder="SqlTimestamp" data="order/@date">
    <jb:decodeParam name="format">EEE MMM dd HH:mm:ss z yyyy</jb:decodeParam>
    <jb:decodeParam name="locale">sv</jb:decodeParam>
</jb:value>

6.13. DataDecoder/DataEncoder decodeParam Example

The decodeParam format is based on the ISO 8601 standard for date formatting. The locale decodeParam value is an underscore-separated string, with the first token being the ISO language code for the locale and the second token being the ISO country code. This decodeParam can also be specified as two separate parameters for language and country:
<jb:value property="date" decoder="Date" data="order/@date">
    <jb:decodeParam name="format">EEE MMM dd HH:mm:ss z yyyy</jb:decodeParam>
    <jb:decodeParam name="locale-language">sv</jb:decodeParam>
    <jb:decodeParam name="locale-country">SE</jb:decodeParam>
</jb:value>

6.14. Number-Based DataDecoder/DataEncoder Implementations

Several number-based DataDecoder/DataEncoder implementations are available:
  • BigDecimalDecoder: use this to decode/encode a string to a java.math. BigDecimal instance.
  • BigIntegerDecoder: use this to decode/encode a string to a java.math. BigInteger instance.
  • DoubleDecoder: use this to decode/encode a string to a java.lang.Double instance (including primitive).
  • FloatDecoder: use this to decode/encode a string to a java.lang.Float instance (including primitive).
  • IntegerDecoder: use this to decode/encode a string to a java.lang.Integer instance (including primitive).
  • LongDecoder: use this to decode/encode a string to a java.lang.Long instance (including primitive).
  • ShortDecoder: use this to decode/encode a string to a java.lang.Short instance (including primitive).
You configure all of these implementations in the same way.

6.15. Number-Based DataDecoder/DataEncoder Example

Here is a BigDecimal example:
<jb:value property="price" decoder="BigDecimal" data="orderItem/price">
    <jb:decodeParam name="format">#,###.##</jb:decodeParam>
    <jb:decodeParam name="locale">en_IE</jb:decodeParam>
</jb:value>

6.16. Number-Based DataDecoder/DataEncoder Integer Example

<jb:value property="percentage" decoder="Integer" data="vote/percentage">
    <jb:decodeParam name="format">#%</jb:decodeParam>
</jb:value>

6.17. Number-Based DataDecoder/DataEncoder decodeParam Example

The format decodeParam is based on the NumberFormat pattern's syntax. The locale decodeParam value is an underscore-separated string, with the first token being the ISO Language Code for the locale and the second token being the ISO country code. You can also specify this decodeParam as two separate parameters for language and country. See the example:
  
<jb:value property="price" decoder="Double" data="orderItem/price">
    <jb:decodeParam name="format">#,###.##</jb:decodeParam>
    <jb:decodeParam name="locale-language">sv</jb:decodeParam>
    <jb:decodeParam name="locale-country">SE</jb:decodeParam>
</jb:value>

6.18. Using the Mapping Decoder to Bind

  1. Configure the Mapping Decoder as shown below to bind a different value to your object model, based on the data in your input message:
    <jb:value property="name" decoder="Mapping" data="history/@warehouse">
        <jb:decodeParam name="1">Dublin</jb:decodeParam>
        <jb:decodeParam name="2">Belfast</jb:decodeParam>
        <jb:decodeParam name="3">Cork</jb:decodeParam>
    </jb:value>
    
  2. An input data value of "1" is mapped to the name property as a value of "Dublin". Likewise for values "2" and "3".

6.19. The Enum Decoder

The Enum Decoder is a specialized version of the Mapping Decoder. Normally, enumerations are decoded automatically (without any specific configuration needed) if the data input values map exactly to the enum values/names. However when this is not the case, you need to define mappings from the input data value to the enum value/name.

6.20. Enum Decoder Example

  1. In the following example, the header/priority field in the input message contains values of LOW, MEDIUM and HIGH. You should map these to the LineOrderPriority enum values of NOT_IMPORTANT, IMPORTANTand VERY_IMPORTANT respectively:
    <jb:value property="priority" data="header/priority" decoder="Enum">
        <jb:decodeParam name="enumType">example.trgmodel.LineOrderPriority</jb:decodeParam>
        <jb:decodeParam name="LOW">NOT_IMPORTANT</jb:decodeParam>
        <jb:decodeParam name="MEDIUM">IMPORTANT</jb:decodeParam>
        <jb:decodeParam name="HIGH">VERY_IMPORTANT</jb:decodeParam>
    </jb:value>
    
  2. If mappings are required, specify the enumeration type using the enumType decodeParam.

6.21. BeanContext Configuration

By default, every bean in the Smooks configuration except the first one is removed from the BeanContext after the fragment that created the bean (createOnElement) is processed. (In other words, the bean is added to the BeanContext on the start/visitBefore of the createOnElement fragment, and is removed from the BeanContext at the end/visitAfter.)
By default, this rule applies to all but the first bean configured in the Smooks configuration. (The first bean is the only bean that is retained in the BeanContext, and so can be accessed after the message has been processed.)
To change this behaviour, use the jb:bean element's retain configuration attribute. This attribute allows you to manually control bean retention within the Smooks BeanContext.

6.22. Javabean Cartridge Actions

The Java Bean cartridge:
  • extracts string values from the source/input message stream.
  • decodes the string value based on the decoder and decodeParam configurations (if these are not defined, an attempt is made to reflectively resolve the decoder).
  • sets the decoded value on the target bean.

6.23. Pre-processing String Data

Before decoding, you may need to pre-process the string data value. An example of this is when the source data has some characters not supported by the locale configuration on Numeric Decoding, such as the numeric value 876592.00 being represented as "876_592!00"jb:bean element. To decode this value as (for instance) a double value, delete the underscore and replace the exclamation mark with a full-stop. You can specify a valuePreprocess decodeParam, which is a simple expression that you can applied to the String value before decoding it.

6.24. Pre-processing Example

This example provides a solution to a numeric decoding issue:
<!-- A bean property binding example: -->
<jb:bean beanId="orderItem" class="org.milyn.javabean.OrderItem" createOnElement="price">
    <jb:value property="price" data="price" decoder="Double">
        <jb:decodeParam name="valuePreprocess">value.replace("_", "").replace("!", ".")</jb:decodeParam>
    </jb:value>
</jb:bean>
<!-- A direct value binding example: -->
<jb:value beanId="price" data="price" decoder="BigDecimal">
    <jb:decodeParam name="valuePreprocess">value.replace("_", "").replace("!", ".")</jb:decodeParam>
</jb:value>
The String data value is referenced in the expression via the value variable name. (The expression can be any valid MVEL expression that operates on the value String and returns a String.)

6.25. The Javabean Cartridge and Factories

The Javabean Cartridge allows you to use factories to create beans. In these cases you do not need to use a public parameterless constructor. You do not need to have defined the actual class name in the class attribute. Any of the object's interfaces suffice. However you can only bind to that interface's methods. (Even if you define a factory, you must always set the class attribute in the bean definition.)
The factory definition is set in the bean element's factory attribute. The default factory definition language looks like this:
some.package.FactoryClass#staticMethod{.instanceMethod}
Use this basic definition language to define a static public parameterless method that Smooks will call to create the bean. (The instanceMethod part is optional. If you set it, it defines the method that will be called on the object that is returned from static method, which should create the bean. The { } characters are only there to illustrate that the part is optional and should be left out of the actual definition.)

6.26. Instantiate an ArrayList Object Using a Static Factory Method

  1. Follow this example to instantiate an ArrayList object using a static factory method:
    <jb:bean
       beanId="orders"
       class="java.util.List"
       factory="some.package.ListFactory#newList"
       createOnElement="orders"
    >
         <!-- ... bindings -->
    </jb:bean>
    
    The some.package.ListFactory#newList factory definition establishes that the newList method must be called on the some.package.ListFactory class in order to create the bean. The class attributes define the bean as a List object. The specific kind of List object that it is (be it an ArrayList or a LinkedList), is decided by the ListFactory itself.
  2. Observe this additional example:
    <jb:bean
       beanId="orders"
       class="java.util.List"
       factory="some.package.ListFactory#getInstance.newList"
       createOnElement="orders"
    >
         <!-- ... bindings -->
    </jb:bean>
    
    This defines that an instance of the ListFactory needs to be retrieved using the static method getInstance and then the newList method needs to be called on the ListFactory object to create the List object. This construct lets you use singleton factories.

6.27. Declaring Definition Language

These are the ways you can declare which definition language you want to use:
  • Each definition language can have an alias. For instance MVEL has the alias mvel. To define that you want to use MVEL for a specific factory definition you put mvel: in front of the definition. For example. mvel:some.package.ListFactory.getInstance().newList(). The alias of the default basic language is basic.
  • To set a language as a global default you need to set the ‘factory.definition.parser.class’ global parameter to the full class path of the class that implements the FactoryDefinitionParser interface for the language that you want to use. If you have a definition with your default language that includes a : you should prefix that definition with default: to avoid an exception.
  • You can set the full classpath of the class that implements the FactoryDefinitionParser interface for the language that you want to use. (For example, org.milyn.javabean.factory.MVELFactoryDefinitionParser:some.package.ListFactory.getInstance().newList().) You should generally use this for test purposes only. It is much better to define an alias for your language.

6.28. Using Your Own Definition Language

  1. To define your own language, implement the org.milyn.javabean.factory.FactoryDefinitionParser interface. Observe the org.milyn.javabean.factory.MVELFactoryDefinitionParser or org.milyn.javabean.factory.BasicFactoryDefinitionParser for examples.
  2. To define the alias for a definition language, add the org.milyn.javabean.factory.Alias annotation with the alias name to your FactoryDefinitionParser class.
  3. For Smooks to find your alias you need create the file META-INF/smooks-javabean-factory-definition-parsers.inf on the root of your classpath. T

6.29. The MVEL Language

MVEL has some advantages over the basic default definition language. It lets you use objects from the bean context as the factory object and allows you to call factory methods with parameters. These parameters can be defined within the definition or they can be objects from the bean context. To use MVEL, use the alias mvel or set the factory.definition.parser.class global parameter to org.milyn.javabean.factory.MVELFactoryDefinitionParser.

6.30. MVEL Example

<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">
 
    <jb:bean
        beanId="orders"
        class="java.util.List"
        factory="mvel:some.package.ListFactory.getInstance().newList()"
        createOnElement="orders"
    >
        <!-- ... bindings -->
    </jb:bean>
 
</smooks-resource-list>

6.31. Extracting a List Object with MVEL

To use MVEL to extract a List object from an existing bean in the bean context, see the example below. (The Order object in this example has a method that returns a list which we must use to add order lines.)
<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">
 
    <jb:bean
        beanId="order"
        class="some.package.Order"
        createOnElement="order"
    >
        <!-- ... bindings -->
    </jb:bean>
 
    <!-- 
           The factory attribute uses MVEL to access the order 
           object in the bean context and calls its getOrderLines() 
           method to get the List. This list is then added to the bean 
           context under the beanId 'orderLines' 
    -->
    <jb:bean
        BeanId="orderLines"
        class="java.util.List"
        factory="mvel:order.getOrderLines()" 
        createOnElement="order"
    >
        <jb:wiring BeanIdRef="orderLine" />
    </jb:bean>
 
    <jb:bean
        BeanId="orderLine"
        class="java.util.List"
        createOnElement="order-line"
    >
        <!-- ... bindings -->
    </jb:bean>
 
</smooks-resource-list>

Note

Array objects are not supported. If a factory returns an array then Smooks will throw an exception.

6.32. The jb:value Property

If the jb:value property attribute of a binding is not defined when binding key value pairs into maps, the name of the selected node will be used as the map entry key (where the beanClass is a Map). You can also define the jb:value property attribute by putting the "@" character in front of it. The rest of the value then defines the attribute name of the selected node, from which the map key is selected.

6.33. jb:value Property Example

<root>
    <property name="key1">value1</property>
    <property name="key2">value2</property>
    <property name="key3">value3</property>
</root>
On the config:
<jb:bean BeanId="keyValuePairs" class="java.util.HashMap" createOnElement="root">
    <jb:value property="@name" data="root/property" />
</jb:bean>
This would create a HashMap with three entries with the keys set [key1, key2, key3].

Note

The "@" character notation doesn't work for bean wiring. The cartridge will simply use the value of the property attribute, including the "@" character, as the map entry key.

6.34. Virtual Object Models

You can create a virtual object model without writing your own Bean classes. This virtual model is created using only maps and lists. This is convenient if you use the Javabean Cartridge between two processing steps. For example, as part of a model driven transform - xml to java to xml or xml to java to edi.

6.35. Virtual Object Model Example

<?xml version="1.0"?>
<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"
                      xmlns:ftl="http://www.milyn.org/xsd/smooks/freemarker-1.1.xsd">
 
    <!--
    Bind data from the message into a Virtual Object model in the bean context....
    -->    
    <jb:bean beanId="order" class="java.util.HashMap" createOnElement="order">
        <jb:wiring property="header" beanIdRef="header" />
        <jb:wiring property="orderItems" beanIdRef="orderItems" />
    </jb:bean>    
    <jb:bean beanId="header" class="java.util.HashMap" createOnElement="order">
        <jb:value property="date" decoder="Date" data="header/date">
            <jb:decodeParam name="format">EEE MMM dd HH:mm:ss z yyyy</jb:decodeParam>
        </jb:value>
        <jb:value property="customerNumber" decoder="Long" data="header/customer/@number" />
        <jb:value property="customerName" data="header/customer" />
        <jb:expression property="total" execOnElement="order-item" >
            header.total + (orderItem.price * orderItem.quantity);
        </jb:expression>
    </jb:bean>    
    <jb:bean beanId="orderItems" class="java.util.ArrayList" createOnElement="order">
        <jb:wiring beanIdRef="orderItem" />
    </jb:bean>    
    <jb:bean beanId="orderItem" class="java.util.HashMap" createOnElement="order-item">
        <jb:value property="productId" decoder="Long" data="order-item/product" />
        <jb:value property="quantity" decoder="Integer" data="order-item/quantity" />
        <jb:value property="price" decoder="Double" data="order-item/price" />
    </jb:bean>
 
    <!--
    Use a FreeMarker template to perform the model driven transformation on the Virtual Object Model...
    -->
    <ftl:freemarker applyOnElement="order">
        <ftl:template>/templates/orderA-to-orderB.ftl</ftl:template>
    </ftl:freemarker>
 
</smooks-resource-list>
Note above how the decoder attribute for a Virtual Model (Map) is always defined. This is because Smooks has no way of auto-detecting the decode type for data binding to a Map. If you need typed values bound into your Virtual Model, you need to specify an appropriate decoder. If the decoder is not specified in this case, Smooks will simply bind the data into the Virtual Model as a String.

6.36. Merging Multiple Data Entities Into a Single Binding

You can merge multiple data entities into a single binding using Expression Based Bindings (jb:expression). The Javabean cartridge uses the Smooks DataDecoder to create an Object from a selected data element/attribute. It then adds it directly to the bean context.

6.37. Value Binding

The ValueBinder class is the visitor that does the value binding. The value binding XML configuration is part of the JavaBean schema from Smooks 1.3 on http://www.milyn.org/xsd/smooks/javabean-1.4.xsd. The element for the value binding is "value".

6.38. Value Binding Attributes

The ValueBinder class has the following attributes:
  • beanId: The ID under which the created object is to be bound in the bean context.
  • data: The data selector for the data value to be bound. (For example, order/orderid or order/header/@date
  • dataNS: The namespace for the data selector
  • decoder: The DataDecoder name for converting the value from a String into a different type. The DataDecoder can be configured with the decodeParam elements.
  • default: The default value for if the selected data is null or an empty string.

6.39. Value Binding Example

The Order message will be used as an example. It will be configured for getting the order number, name and date as Value Objects in the form of an Integer and String.
The message input:
<order xmlns="http://x">
     <header>
         <y:date xmlns:y="http://y">Wed Nov 15 13:45:28 EST 2006</y:date>
         <customer number="123123">Joe</customer>
         <privatePerson></privatePerson>
     </header>
     <order-items>
         <!-- .... -->
     </order-items>
 </order>
The configuration:
<?xml version="1.0"?>
 <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">
 
    <jb:value
       beanId="customerName"
       data="customer"
       default="unknown"
    />
 
    <jb:value
       beanId="customerNumber"
       data="customer/@number"
       decoder="Integer"
    />
 
   <jb:value
       beanId="orderDate"
       data="date"
       dateNS="http://y"
       decoder="Date"
    >
         <jb:decodeParam name="format">EEE MMM dd HH:mm:ss z yyyy</jb:decodeParam>
         <jb:decodeParam name="locale-language">en</jb:decodeParam>
         <jb:decodeParam name="locale-country">IE</jb:decodeParam>
   </jb:value>
 
 </smooks-resource-list>

6.40. Programmatic Value Binding Example

The value binder can be programmatically configured using the org.milyn.javabean.Value object:
//Create Smooks. normally done globally!
Smooks smooks = new Smooks();
 
//Create the Value visitors
Value customerNumberValue = new Value( "customerNumber", "customer/@number").setDecoder("Integer");
Value customerNameValue = new Value( "customerName", "customer").setDefault("Unknown");
 
//Add the Value visitors
smooks.addVisitors(customerNumberValue);
smooks.addVisitors(customerNameValue);
 
//And the execution code: 
JavaResult result = new JavaResult();
 
smooks.filterSource(new StreamSource(orderMessageStream), result);
Integer customerNumber = (Integer) result.getBean("customerNumber");
String customerName = (String) result.getBean("customerName");

6.41. Java Binding in Smooks

Java Binding Configurations can be programmatically added to a Smooks using the Bean configuration class. This class can be used to programmatically configure a Smooks instance for performing a Java Bindings on a specific class. To populate a graph, you simply create a graph of bean instances by binding beans onto beans. The bean class uses a Fluent API (all methods return the bean instance), making it easy to string configurations together to build up a graph of bean configuration.

6.42. Java Binding Example

This Order message example shows how to bind it into a corresponding Java Object model.
The message input:
<order xmlns="http://x">
    <header>
        <y:date xmlns:y="http://y">Wed Nov 15 13:45:28 EST 2006</y:date>
        <customer number="123123">Joe</customer>
        <privatePerson></privatePerson>
    </header>
    <order-items>
        <order-item>
            <product>111</product>
            <quantity>2</quantity>
            <price>8.90</price>
        </order-item>
        <order-item>
            <product>222</product>
            <quantity>7</quantity>
            <price>5.20</price>
        </order-item>
    </order-items>
</order>
The Java Model (not including getters/setters):
public class Order {
    private Header header;
    private List<OrderItem> orderItems;
}
 
public class Header {
    private Long customerNumber;
    private String customerName;
}
 
public class OrderItem {
    private long productId;
    private Integer quantity;
    private double price;
}
The configuration code:
  
Smooks smooks = new Smooks();
 
Bean orderBean = new Bean(Order.class, "order", "/order");
 
orderBean.bindTo("header",
    orderBean.newBean(Header.class, "/order")
        .bindTo("customerNumber", "header/customer/@number")
        .bindTo("customerName", "header/customer")
    ).bindTo("orderItems",
    orderBean.newBean(ArrayList.class, "/order")
        .bindTo(orderBean.newBean(OrderItem.class, "order-item")
            .bindTo("productId", "order-item/product")
            .bindTo("quantity", "order-item/quantity")
            .bindTo("price", "order-item/price"))
    );
 
smooks.addVisitors(orderBean);
The execution code:
 JavaResult result = new JavaResult();
 
smooks.filterSource(new StreamSource(orderMessageStream), result);
Order order = (Order) result.getBean("order");
Here is an example where an anonymous Factory class is defined and used:
Bean orderBean = new Bean(Order.class, "order", "/order", new Factory<Order>() {
 
 public Order create(ExecutionContext executionContext) {
  return new Order();
 }
 
});

6.43. The org.milyn.javabean.gen.ConfigGenerator Utility Class

The Javabean Cartridge contains the org.milyn.javabean.gen.ConfigGenerator utility class that can be used to generate a binding configuration template. This template can then be used as the basis for defining a binding.

6.44. org.milyn.javabean.gen.ConfigGenerator Example

From the commandline:
$JAVA_HOME/bin/java -classpath <classpath> org.milyn.javabean.gen.ConfigGenerator -c <rootBeanClass> -o <outputFilePath> [-p <propertiesFilePath>]
  • The-c commandline arg specifies the root class of the model whose binding config is to be generated.
  • The -o commandline arg specifies the path and filename for the generated config output.
  • The -p commandline arg specifies the path and filename optional binding configuration file that specifies additional binding parameters.
  • The optional -p properties file parameter allows specification of additional config parameters.
  • packages.included: Semi-colon separated list of packages. Any fields in the class matching these packages will be included in the binding configuration generated.
  • packages.excluded: Semi-colon separated list of packages. Any fields in the class matching these packages will be excluded from the binding configuration generated.

6.45. Programming the Binding Configuration

After running the the org.milyn.javabean.gen.ConfigGenerator utility class against the target class, you should perform these tasks to make the binding configuration work for your Source data model.
  1. For each jb:bean element, set the createOnElement attribute to the event element that should be used to create the bean instance.
  2. Update the jb:value data attributes to select the event element/attribute supplying the binding data for that BFean property.
  3. Check the jb:value decoder attributes. Not all will be set, depending on the actual property type. These must be configured by hand. You may need to configure jb:decodeParam sub-elements for the decoder on some of the bindings, for example, on a date field.
  4. Double-check the binding configuration elements (jb:value and jb:wiring), making sure all Java properties have been covered in the generated configuration.

6.46. Configuring Transformations

  1. Access the HTML Reporting Tool when determining selector values. It helps you visualise the input message model (against which the selectors will be applied) as seen by Smooks.
  2. Generate a report using your Source data, but with an empty transformation configuration. In the report, you can see the model against which you need to add your configurations. Add the configurations one at a time, rerunning the report to check they are being applied.
  3. Add the configurations one at a time, rerunning the report to check they are being applied.
  4. As a result, a configuration that looks like this will be generated (note the $TODO$ tokens):
    <?xml version="1.0"?>
    <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">
     
        <jb:bean beanId="order" class="org.milyn.javabean.Order" createOnElement="$TODO$">
            <jb:wiring property="header" beanIdRef="header" />
            <jb:wiring property="orderItems" beanIdRef="orderItems" />
            <jb:wiring property="orderItemsArray" beanIdRef="orderItemsArray" />
        </jb:bean>
     
        <jb:bean beanId="header" class="org.milyn.javabean.Header" createOnElement="$TODO$">
            <jb:value property="date" decoder="$TODO$" data="$TODO$" />
            <jb:value property="customerNumber" decoder="Long" data="$TODO$" />
            <jb:value property="customerName" decoder="String" data="$TODO$" />
            <jb:value property="privatePerson" decoder="Boolean" data="$TODO$" />
            <jb:wiring property="order" beanIdRef="order" />
        </jb:bean>
     
        <jb:bean beanId="orderItems" class="java.util.ArrayList" createOnElement="$TODO$">
            <jb:wiring beanIdRef="orderItems_entry" />
        </jb:bean>
     
        <jb:bean beanId="orderItems_entry" class="org.milyn.javabean.OrderItem" createOnElement="$TODO$">
            <jb:value property="productId" decoder="Long" data="$TODO$" />
            <jb:value property="quantity" decoder="Integer" data="$TODO$" />
            <jb:value property="price" decoder="Double" data="$TODO$" />
            <jb:wiring property="order" beanIdRef="order" />
        </jb:bean>
     
        <jb:bean beanId="orderItemsArray" class="org.milyn.javabean.OrderItem[]" createOnElement="$TODO$">
            <jb:wiring beanIdRef="orderItemsArray_entry" />
        </jb:bean>
     
        <jb:bean beanId="orderItemsArray_entry" class="org.milyn.javabean.OrderItem" createOnElement="$TODO$">
            <jb:value property="productId" decoder="Long" data="$TODO$" />
            <jb:value property="quantity" decoder="Integer" data="$TODO$" />
            <jb:value property="price" decoder="Double" data="$TODO$" />
            <jb:wiring property="order" beanIdRef="order" />
        </jb:bean>
     
    </smooks-resource-list>
    

    Note

    There is no guarantee as to the exact contents of a JavaResult instance after calling the Smooks.filterSource method. After calling this method, the JavaResult instance will contain the final contents of the bean context, which can be added to by any Visitor implementation.

6.47. jb:result Configuration Example

You can restrict the Bean set returned in a JavaResult by using a jb:result configuration in the Smooks configuration. In the following example configuration, we tell Smooks to only retain the order bean in the ResultSet:
<?xml version="1.0"?>
<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">
 
    <!-- Capture some data from the message into the bean context... -->
    <jb:bean beanId="order" class="com.acme.Order" createOnElement="order">
        <jb:value property="orderId" data="order/@id"/>
        <jb:value property="customerNumber" data="header/customer/@number"/>
        <jb:value property="customerName" data="header/customer"/>
        <jb:wiring property="orderItems" beanIdRef="orderItems"/>
    </jb:bean>
    <jb:bean beanId="orderItems" class="java.util.ArrayList" createOnElement="order">
        <jb:wiring beanIdRef="orderItem"/>
    </jb:bean>
    <jb:bean beanId="orderItem" class="com.acme.OrderItem" createOnElement="order-item">
        <jb:value property="itemId" data="order-item/@id"/>
        <jb:value property="productId" data="order-item/product"/>
        <jb:value property="quantity" data="order-item/quantity"/>
        <jb:value property="price" data="order-item/price"/>
    </jb:bean>
 
    <!-- Only retain the "order" bean in the root of any final JavaResult. -->
    <jb:result retainBeans="order"/>
 
</smooks-resource-list>
After applying this configuration, any calls to the JavaResult.getBean(String) method for non-order Bean results will return null. This will work in cases such as the above example, because the other bean instances are wired into the order graph.

Note

Note that as of Smooks v1.2, if a JavaSource instance is supplied to the Smooks.filterSource method (as the filter Source instance), Smooks will use the JavaSource to construct the bean context associated with the ExecutionContect for that Smooks.filterSource invocation. This will mean that some of the JavaSource bean instances may be visible in the JavaResult.

Chapter 7. Templates

7.1. Smooks Templates

The two kinds of templates available in Smooks are FreeMarker (http://freemarker.org) and XSL (http://www.w3.org/Style/XSL/).
These technologies can be used within the context of a Smooks filtering process. This means that they:
  • can be applied to the source message on a per-fragment basis. This is in contrast to the fragment-based transformation process which is applied the whole message. Applying them on a per-fragment basis is useful when there is a need to insert a piece of data into a message at a very specific point, such as when adding a header to a SOAP message. In this case, the process can be "aimed" at the fragment of interest without disrupting the rest of it.
  • can take advantage of other Smooks technologies such as the Javabean Cartridge, which can be used to decode and bind message data to the bean context. It can then make reference to that decoded data from within the FreeMarker template. (Smooks makes data available to FreeMarker.)
  • can be used to process huge; message streams (those which are many gigabytes in size) while at the same time maintaining a simple processing model and a small memory footprint.
  • can be used to generate split message fragments. These can then be routed to physical or logical endpoints on an enterprise service bus.

7.2. FreeMarker Templates

FreeMarker is a very powerful template engine. Smooks can use FreeMarker as a means of generating text-based content. This content can then be inserted into a message stream (this process is known as a fragment-based transformation). The process can also be used to split message fragments for subsequent routing to another process.
Set Smooks' FreeMarker templates by using the configuration namespace http://www.milyn.org/xsd/smooks/freemarker-1.1.xsd. Then, configure the XSD in an integrated development environment in order to begin using it.

7.3. FreeMarker Template Examples

  • Inline template example:
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:ftl="http://www.milyn.org/xsd/smooks/freemarker-1.1.xsd">
        <ftl:freemarker applyOnElement="order">
            <ftl:template><!--<orderId>${order.id}</orderId>--></ftl:template>
        </ftl:freemarker>
    </smooks-resource-list>
    
  • FreeMarker external template example:
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:ftl="http://www.milyn.org/xsd/smooks/freemarker-1.1.xsd">
        <ftl:freemarker applyOnElement="order">
            <ftl:template>/templates/shop/ordergen.ftl</ftl:template>
        </ftl:freemarker>
    </smooks-resource-list>
    
  • Add the <use> element to the <ftl:freemarker> configuration in order to allow Smooks to perform a number of operations upon the resulting output. See the example below:
    <ftl:freemarker applyOnElement="order">
        <ftl:template>/templates/shop/ordergen.ftl</ftl:template>
        <ftl:use>
            <ftl:inline directive="insertbefore" />
        </ftl:use>
    </ftl:freemarker>
    

7.4. Inlining in Smooks

Inlining, as its name implies, allows you to "inline" the templating result to a Smooks.filterSource result. A number of directives are supported:
  • addto: this adds the templating result to the targeted element.
  • replace (default): this uses the templating result to replace the targeted element. This is the default behavior for the <ftl:freemarker> configuration when the <use> element is not configured.
  • insertbefore: this adds the templating result before the targeted element.
  • insertafter: this adds the templating result after the targeted element.

7.5. The ftl:bindTo Element

The ftl:bindTo element allows you to bind a templating result to the Smooks bean context. The result can then be accessed by other Smooks components, such as those used for routing. This is especially useful when you are splitting huge; messages into smaller ones. The split fragments can then be routed to another process.

7.6. ftl:bindTo Example

<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"
                      xmlns:jms="http://www.milyn.org/xsd/smooks/jms-routing-1.2.xsd"
                      xmlns:ftl="http://www.milyn.org/xsd/smooks/freemarker-1.1.xsd">

    <jms:router routeOnElement="order-item" beanId="orderItem_xml" destination="queue.orderItems" />
    
    <ftl:freemarker applyOnElement="order-item">
        <ftl:template>/orderitem-split.ftl</ftl:template>
        <ftl:use>
            <!-- Bind the templating result into the bean context, from where
                 it can be accessed by the JMSRouter (configured above). -->
            <ftl:bindTo id="orderItem_xml"/>
        </ftl:use>
    </ftl:freemarker>

</smooks-resource-list>

7.7. The ftl:outputTo Element

The ftl:outputTo can be used to write an output result directly to an OutputStreamResource class. This is another useful mechanism for splitting huge messages into smaller ones.

7.8. ftl:outputTo Example

Example of writing the template result to an OutputStreamSource:
<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"
                      xmlns:jb="http://www.milyn.org/xsd/smooks/javabean-1.3.xsd"
                      xmlns:file="http://www.milyn.org/xsd/smooks/file-routing-1.1.xsd"
                      xmlns:ftl="http://www.milyn.org/xsd/smooks/freemarker-1.1.xsd">

    <!-- Create/open a file output stream. This is written to by the freemarker template (below).. -->
    <file:outputStream openOnElement="order-item" resourceName="orderItemSplitStream">
        <file:fileNamePattern>order-${order.orderId}-${order.orderItem.itemId}.xml</file:fileNamePattern>
        <file:destinationDirectoryPattern>target/orders</file:destinationDirectoryPattern>
        <file:listFileNamePattern>order-${order.orderId}.lst</file:listFileNamePattern>
        
        <file:highWaterMark mark="3"/>
    </file:outputStream>
    
    <!--
    Every time we hit the end of an <order-item> element, apply this freemarker template,
    outputting the result to the "orderItemSplitStream" OutputStream, which is the file
    output stream configured above.
    -->
     <ftl:freemarker applyOnElement="order-item">
        <ftl:template>target/classes/orderitem-split.ftl</ftl:template>
        <ftl:use>
            <!-- Output the templating result to the "orderItemSplitStream" file output stream... -->
            <ftl:outputTo outputStreamResource="orderItemSplitStream"/>
        </ftl:use>
    </ftl:freemarker>

</smooks-resource-list>

Note

7.9. Configuring Transformations

  1. Access the HTML Reporting Tool when determining selector values. It helps you visualise the input message model (against which the selectors will be applied) as seen by Smooks.
  2. Generate a report using your Source data, but with an empty transformation configuration. In the report, you can see the model against which you need to add your configurations. Add the configurations one at a time, rerunning the report to check they are being applied.
  3. Add the configurations one at a time, rerunning the report to check they are being applied.
  4. As a result, a configuration that looks like this will be generated (note the $TODO$ tokens):
    <?xml version="1.0"?>
    <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">
     
        <jb:bean beanId="order" class="org.milyn.javabean.Order" createOnElement="$TODO$">
            <jb:wiring property="header" beanIdRef="header" />
            <jb:wiring property="orderItems" beanIdRef="orderItems" />
            <jb:wiring property="orderItemsArray" beanIdRef="orderItemsArray" />
        </jb:bean>
     
        <jb:bean beanId="header" class="org.milyn.javabean.Header" createOnElement="$TODO$">
            <jb:value property="date" decoder="$TODO$" data="$TODO$" />
            <jb:value property="customerNumber" decoder="Long" data="$TODO$" />
            <jb:value property="customerName" decoder="String" data="$TODO$" />
            <jb:value property="privatePerson" decoder="Boolean" data="$TODO$" />
            <jb:wiring property="order" beanIdRef="order" />
        </jb:bean>
     
        <jb:bean beanId="orderItems" class="java.util.ArrayList" createOnElement="$TODO$">
            <jb:wiring beanIdRef="orderItems_entry" />
        </jb:bean>
     
        <jb:bean beanId="orderItems_entry" class="org.milyn.javabean.OrderItem" createOnElement="$TODO$">
            <jb:value property="productId" decoder="Long" data="$TODO$" />
            <jb:value property="quantity" decoder="Integer" data="$TODO$" />
            <jb:value property="price" decoder="Double" data="$TODO$" />
            <jb:wiring property="order" beanIdRef="order" />
        </jb:bean>
     
        <jb:bean beanId="orderItemsArray" class="org.milyn.javabean.OrderItem[]" createOnElement="$TODO$">
            <jb:wiring beanIdRef="orderItemsArray_entry" />
        </jb:bean>
     
        <jb:bean beanId="orderItemsArray_entry" class="org.milyn.javabean.OrderItem" createOnElement="$TODO$">
            <jb:value property="productId" decoder="Long" data="$TODO$" />
            <jb:value property="quantity" decoder="Integer" data="$TODO$" />
            <jb:value property="price" decoder="Double" data="$TODO$" />
            <jb:wiring property="order" beanIdRef="order" />
        </jb:bean>
     
    </smooks-resource-list>
    

    Note

    There is no guarantee as to the exact contents of a JavaResult instance after calling the Smooks.filterSource method. After calling this method, the JavaResult instance will contain the final contents of the bean context, which can be added to by any Visitor implementation.

7.10. FreeMarker and the Java Bean Cartridge

The FreeMarkerNodeModel is powerful and easy to use, but this comes at a trade-off in terms of performance. It is not "cheap" to construct W3C DOMs. It also may be the case that the required data has already been extracted and populated into a Java object model, an example being when the data also needs to be routed to a Java Message Service endpoint as a set of objects.
When using the NodeModel would be impractical, use the Java Bean Cartridge to populate a proper Java object or a virtual model. This model can then be used in the FreeMarker templating process.

7.11. NodeModel Example

The following example shows you how to configure the NodeModel element:
<?xml version="1.0"?>
<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"
                      xmlns:jb="http://www.milyn.org/xsd/smooks/javabean-1.3.xsd"
                      xmlns:ftl="http://www.milyn.org/xsd/smooks/freemarker-1.1.xsd">

    <!-- Extract and decode data from the message. Used in the freemarker template (below). -->
    <jb:bean beanId="order" class="java.util.Hashtable" createOnElement="order">
        <jb:value property="orderId" decoder="Integer" data="order/@id"/>
        <jb:value property="customerNumber" decoder="Long" data="header/customer/@number"/>
        <jb:value property="customerName" data="header/customer"/>
        <jb:wiring property="orderItem" beanIdRef="orderItem"/>
    </jb:bean>
    <jb:bean beanId="orderItem" class="java.util.Hashtable" createOnElement="order-item">
        <jb:value property="itemId" decoder="Integer" data="order-item/@id"/>
        <jb:value property="productId" decoder="Long" data="order-item/product"/>
        <jb:value property="quantity" decoder="Integer" data="order-item/quantity"/>
        <jb:value property="price" decoder="Double" data="order-item/price"/>
    </jb:bean>
    
    <ftl:freemarker applyOnElement="order-item">
        <ftl:template><!--<orderitem id="${order.orderItem.itemId}" order="${order.orderId}">
    <customer>
        <name>${order.customerName}</name>
        <number>${order.customerNumber?c}</number>
    </customer>
    <details>
        <productId>${order.orderItem.productId}</productId>
        <quantity>${order.orderItem.quantity}</quantity>
        <price>${order.orderItem.price}</price>
    </details>
</orderitem>-->
        </ftl:template>
    </ftl:freemarker>

</smooks-resource-list>

7.12. Programmatic Configuration

FreeMarker template configurations can be added to a Smooks instance programmatically. Do so by adding and configuring a FreeMarkerTemplateProcessor instance as shown below. This example adds configurations for a Java binding and a FreeMarker template to Smooks:
Smooks smooks = new Smooks();

smooks.addVisitor(new Bean(OrderItem.class, "orderItem", "order-item").bindTo("productId", "order-item/product/@id"));
smooks.addVisitor(new FreeMarkerTemplateProcessor(new TemplatingConfiguration("/templates/order-tem.ftl")), "order-item");

// Then use Smooks as normal... filter a Source to a Result etc...

7.13. XSL Templates

  • To use XSL templates in Smooks, configure the http://www.milyn.org/xsd/smooks/xsl-1.1.xsd XSD in an integrated development environment.

    Important

    In JBoss SOA, the fragment filter is bypassed when the Smooks configuration only contains a single XSLT that is applied to the root fragment. The XSLT is applied directly. This is done for performance reasons and can be disabled by adding a parameter called enableFilterBypass and setting it to false:
    <param name="enableFilterBypass">false</param>
    

7.14. XSL Example

The following example shows you how to configure an XSL template:
<?xml version="1.0"?>
<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:xsl="http://www.milyn.org/xsd/smooks/xsl-1.1.xsd">

    <xsl:xsl applyOnElement="#document">
        <xsl:template><!--<xxxxxx/>--></xsl:template>
    </xsl:xsl>

</smooks-resource-list>
As is the case with FreeMarker, other types of external template can be configured using an URI reference in the <xsl:template> element.

7.15. Points to Note Regarding XSL Support

There is no reason to use Smooks to execute XSL templates unless:
  • there is a need to perform a fragment transformation, as opposed to the transformation of a whole message.
  • there is a need to use other Smooks functionality to perform additional operations on the message, such as splitting or persistence.
  • XSL templating is only supported through the DOM filter. It is not supported through the SAX filter. This can (depending on the XSL being applied) result in lower performance when compared to a SAX-based application of XSL.
  • Smooks applies XSL templates on a per-message fragment basis. This can be very useful for fragmenting XSLs, but do not assume that a template written for a stand-alone context will automatically work in Smooks without modification. For this reason, Smooks handles XSLs targeted at the document root node differently in that it applies the template to the DOM Document Node (rather than the root DOM Element.)
  • most XSLs contain a template that is matched to the root element. Because Smooks applies XSLs on a per-fragment basis, this is no longer valid. Ensure that the style-sheet contains a template that matches against the context node instead (that is, the targeted fragment).

7.16. Potential Issue: XSLT Works Externally but not Within Smooks

This can happen on occasions and normally results from one of the following scenarios:
  • Issues will occur in the Smooks Fragment-Based Processing Model if the stylesheet contains a template that is using an absolute path reference to the document root node. This is because the wrong element is being targeted by Smooks. To rectify the problem, ensure that the XSLT contains a template that matches the context node being targeted by Smooks.
  • SAX versus DOM Processing: "like" is not being compared with "like". In its current state, Smooks only supports a DOM-based processing model for dealing with XSL. In order to undertake an accurate comparison, use a DOMSource (one that is namespace-aware) when executing the XSL template outside Smooks. (A given XSL Processor does not always produce the same output when it tries to apply an XSL template using SAX or DOM.)

Chapter 8. Enriching Output Data

8.1. Out-of-the-Box Enrichment Methods

Three methods for enriching your output data are included with the product:
JDBC Datasources
Use a JDBC Datasource to access a database and use SQL statements to read from and write to the Database. This capability is provided through the Smooks Routing Cartridge. See the section on routing to a database using SQL.
Entity persistence
Use an entity persistence framework (like Ibatis, Hibernate or any JPA compatible framework) to access a database and use its query language or CRUD methods to read from it or write to it.
DAOs
Use custom Data Access Objects (DAOs) to access a database and use its CRUD methods to read from it or write to it.

8.2. Hibernation Example

The data to be processed is an XML order message. Depending on your needs, the input data could also be CSV, JSON, EDI, Java or any other structured/hierarchical data format. The same principals apply, no matter what the data format is. The same principals follow for any JPA compliant framework:
<order>
    <ordernumber>1</ordernumber>
    <customer>123456</customer>
    <order-items>
        <order-item>
            <product>11</product>
            <quantity>2</quantity>
        </order-item>
        <order-item>
            <product>22</product>
            <quantity>7</quantity>
        </order-item>
    </order-items>
</order>

8.3. Hibernate Entities

The following is a snapshot of the hibernate function's entities:
@Entity
@Table(name="orders")
public class Order {
 
    @Id
    private Integer ordernumber;
 
    @Basic
    private String customerId;
 
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List orderItems = new ArrayList();
 
    public void addOrderLine(OrderLine orderLine) {
        orderItems.add(orderLine);
    }
 
    // Getters and Setters....
}
 
@Entity
@Table(name="orderlines")
public class OrderLine {
 
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;
 
    @ManyToOne
    @JoinColumn(name="orderid")
    private Order order;
 
    @Basic
    private Integer quantity;
 
    @ManyToOne
    @JoinColumn(name="productid")
    private Product product;
 
    // Getters and Setters....
}
 
@Entity
@Table(name = "products")
@NamedQuery(name="product.byId", query="from Product p where p.id = :id")
public class Product {
 
    @Id
    private Integer id;
 
    @Basic
    private String name;
 
    // Getters and Setters....
}

8.4. Processing and Persisting an Order

  1. To process and persist an XML "order" message, you should bind the order data into the Order entities (Order, OrderLine and Product). To do this, create and populate the Order and OrderLine entities using the Java Binding framework.
  2. Wire each OrderLine instance into the Order instance.
  3. In each OrderLine instance, you should lookup and wire in the associated order line Product entity.
  4. Finally, insert (persist) the Order instance as seen below:
    <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" 
                          xmlns:dao="http://www.milyn.org/xsd/smooks/persistence-1.2.xsd">
     
        <jb:bean beanId="order" class="example.entity.Order" createOnElement="order">
            <jb:value property="ordernumber" data="ordernumber" />
            <jb:value property="customerId" data="customer" />
            <jb:wiring setterMethod="addOrderLine" beanIdRef="orderLine" />
        </jb:bean>
     
        <jb:bean beanId="orderLine" class="example.entity.OrderLine" createOnElement="order-item">
            <jb:value property="quantity" data="quantity" />
            <jb:wiring property="order" beanIdRef="order" />
            <jb:wiring property="product" beanIdRef="product" />
        </jb:bean>
     
        <dao:locator beanId="product" lookupOnElement="order-item" onNoResult="EXCEPTION" uniqueResult="true">
            <dao:query>from Product p where p.id = :id</dao:query>
            <dao:params>
                <dao:value name="id" data="product" decoder="Integer" />
            </dao:params>
        </dao:locator>
     
        <dao:inserter beanId="order" insertOnElement="order" />
     
    </smooks-resource-list>
    
  5. If you want to use the named query productById instead of the query string, the DAO locator configuration will look like this:
    <dao:locator beanId="product" lookupOnElement="order-item" lookup="product.byId" onNoResult="EXCEPTION" uniqueResult="true">
        <dao:params>
            <dao:value name="id" data="product" decoder="Integer"/>
        </dao:params>
    </dao:locator>
    

8.5. Executing Smooks with a SessionRegister Object

The following code executes Smooks. A SessionRegister object is used so the Hibernate Session can be accessed from within Smooks.
Smooks smooks = new Smooks("smooks-config.xml");
 
ExecutionContext executionContext = smooks.createExecutionContext();
 
// The SessionRegister provides the bridge between Hibernate and the
// Persistence Cartridge. We provide it with the Hibernate session.
// The Hibernate Session is set as default Session.
DaoRegister register = new SessionRegister(session);
 
// This sets the DAO Register in the executionContext for Smooks
// to access it.
PersistenceUtil.setDAORegister(executionContext, register);
 
Transaction transaction = session.beginTransaction();
 
smooks.filterSource(executionContext, source);
 
transaction.commit();

8.6. Persisting an Order with DAO

  1. To persist an order with DAO, observe the example code below. This example will read an XML file containing order information (this works the same for EDI, CSV, and so on). Using the Javabean cartridge, it will bind the XML data into a set of entity beans. It will locate the product entities and bind them to the order entity bean using the ID of the products within the order items (the product element). Finally, the order bean will be persisted.
    The order XML message looks like this:
    <order>
        <ordernumber>1</ordernumber>
        <customer>123456</customer>
        <order-items>
            <order-item>
                <product>11</product>
                <quantity>2</quantity>
            </order-item>
            <order-item>
                <product>22</product>
                <quantity>7</quantity>
            </order-item>
        </order-items>
    </order>
    
  2. Use a custom DAO such as the example below to persist the Order entity:
    @Dao
    public class OrderDao {
     
        private final EntityManager em;
     
        public OrderDao(EntityManager em) {
            this.em = em;
        }
     
        @Insert
        public void insertOrder(Order order) {
            em.persist(order);
        }
    }
    
    When looking at this class you should notice the @Dao and @Insert annotations. The @Dao annotation declares that the OrderDao is a DAO object. The @Insert annotation declares that the insertOrder method should be used to insert Order entities.
  3. Use a custom DAO as shown in the following example to lookup the Product entities:
    @Dao
    public class ProductDao {
     
        private final EntityManager em;
     
        public ProductDao(EntityManager em) {
            this.em = em;
        }
     
        @Lookup(name = "id")
        public Product findProductById(@Param("id")int id) {
            return em.find(Product.class, id);
        }
    }
    
    When looking at this class, you should notice the @Lookup and @Param annotation. The @Lookup annotation declares that the ProductDao#findByProductId method is used to lookup Product entities. The name parameter in the @Lookup annotation sets the lookup name reference for that method. When the name isn’t declared, the method name will be used. The optional @Param annotation lets you name the parameters. This creates a better abstraction between Smooks and the DAO. If you don’t declare the @Param annotation the parameters are resolved by there position.
  4. When you have configured your order as shown above, the resulting Smooks configuration will look like this:
    <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"
                          xmlns:dao="http://www.milyn.org/xsd/smooks/persistence-1.2.xsd">
     
        <jb:bean BeanId="order" class="example.entity.Order" createOnElement="order">
            <jb:value property="ordernumber" data="ordernumber"/>
            <jb:value property="customerId" data="customer"/>
            <jb:wiring setterMethod="addOrderLine" BeanIdRef="orderLine"/>
        </jb:bean>
     
        <jb:bean BeanId="orderLine" class="example.entity.OrderLine" createOnElement="order-item">
            <jb:value property="quantity" data="quantity"/>
            <jb:wiring property="order" BeanIdRef="order"/>
            <jb:wiring property="product" BeanIdRef="product"/>
        </jb:bean>
     
        <dao:locator BeanId="product" dao="product" lookup="id" lookupOnElement="order-item" onNoResult="EXCEPTION">
            <dao:params>
                <dao:value name="id" data="product" decoder="Integer"/>
            </dao:params>
        </dao:locator>
     
        <dao:inserter BeanId="order" dao="order" insertOnElement="order"/>
     
    </smooks-resource-list>
    
  5. Use the following code to execute Smooks:
    Smooks smooks=new Smooks("./smooks-configs/smooks-dao-config.xml");
    ExecutionContext executionContext=smooks.createExecutionContext();
     
    // The register is used to map the DAO's to a DAO name. The DAO name isbe used in
    // the configuration.
    // The MapRegister is a simple Map like implementation of the DaoRegister.
    DaoRegister<object>register = MapRegister.builder()
            .put("product",new ProductDao(em))
            .put("order",new OrderDao(em))
            .build();
     
    PersistenceUtil.setDAORegister(executionContext,mapRegister);
     
    // Transaction management from within Smooks isn't supported yet,
    // so we need to do it outside the filter execution
    EntityTransaction tx=em.getTransaction();
    tx.begin();
     
    smooks.filter(new StreamSource(messageIn),null,executionContext);
     
    tx.commit();
    

Chapter 9. "Groovy" Scripting

9.1. Groovy

Groovy is an agile and dynamic language for the Java Virtual Machine that builds upon the strengths of Java but has additional power features inspired by languages like Python, Ruby and Smalltalk.
Refer to http://groovy.codehaus.org/ for more information.

9.2. Groovy Example

Support for Groovy scripting is available through the configuration namespace (http://www.milyn.org/xsd/smooks/groovy-1.1.xsd.) This namespace provides support for DOM- and SAX-based scripting. See the example below:
<?xml version="1.0"?>
<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:g="http://www.milyn.org/xsd/smooks/groovy-1.1.xsd">

    <g:groovy executeOnElement="xxx">
        <g:script>
        <!--
        //Rename the target fragment element from "xxx" to "yyy"...
        DomUtils.renameElement(element, "yyy", true, true);
        -->
        </g:script>
    </g:groovy>

</smooks-resource-list>

9.3. Groovy Tips

  • The visited element is available to the script through the variable appropriately named element. (It is also available under the variable name which is equal to the element name but only if the name of the latter is limited alpha-numeric characters.)
  • Execute Before/Execute After: by default, the script executes on the visitAfter event. Direct it to execute on the visitBefore by setting the executeBefore attribute to true.
  • Comment/CDATA Script Wrapping: the script can be wrapped in an XML Comment or CDATA section if it contains special XML characters.

9.4. Imports

Add imports using the imports element. A number of classes are automatically imported.
  • org.milyn.xml.DomUtils
  • org.milyn.javabean.context.BeanContext. Only in Smooks 1.3 and later.
  • org.milyn.javabean.repository.BeanRepository
  • org.w3c.dom.*
  • groovy.xml.dom.DOMCategory
  • groovy.xml.dom.DOMUtil
  • groovy.xml.DOMBuilder

9.5. Using Mixed-DOM-and-SAX with Groovy

Groovy has support for the mixed-DOM-and-SAX model. You can use Groovy's DOM utilities to process a targeted message fragment. A DOM "element" will be received by the Groovy script, even when the SAX filter is being used. This makes Groovy scripting using the SAX filter much easier while maintaining the ability to process huge messages in a streamed fashion.

9.6. Mixed-DOM-and-SAX Tips

Things to be careful of:
  • it is only available in the default mode (that is, when executeBefore equals false). If executeBefore is configured to be true, this facility will not be available, which means that the Groovy script will only have access to SAXElements.
  • writeFragment must be called in order to write the DOM fragment to a Smooks.filterSource StreamResult.
  • a performance overhead will be incurred by using this DOM construction facility. (It can still process huge messages but it might take a slightly longer period of time. The compromise is between "usability" and performance.)

9.7. Mixed-DOM-and-SAX Example

Procedure 9.1. Task

  1. Take an XML message such as the following sample:
    <shopping>
        <category type="groceries">
            <item>Chocolate</item>
            <item>Coffee</item>
        </category>
        <category type="supplies">
            <item>Paper</item>
            <item quantity="4">Pens</item>
        </category>
            <category type="present">
            <item when="Aug 10">Kathryn's Birthday</item>
        </category>
    </shopping>
    
  2. You can modify the "supplies" category in the above shopping list by adding two more "pens". To do this, write a simple Groovy script and aim it at the message's <category> elements.
  3. As a result, the script simply iterates over the <item> elements in the category, and in instances where the category type is "supplies" and the item is "pens", the quantity is incremented by two:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" 
       xmlns:core="http://www.milyn.org/xsd/smooks/smooks-core-1.3.xsd"
       xmlns:g="http://www.milyn.org/xsd/smooks/groovy-1.1.xsd">
    
       <core:filterSettings type="SAX" />
        
       <g:groovy executeOnElement="category">
          <g:script>
          <!--
             use(DOMCategory) {
              // Modify "supplies": we need an extra 2 pens...
              if (category.'@type' == 'supplies') {
                  category.item.each { item ->
                      if (item.text() == 'Pens') {
                          item['@quantity'] = item.'@quantity'.toInteger() + 2;
                      }
                  }
              }
          }
            
          // When using the SAX filter, we need to explicitly write the fragment
          // to the result stream...
          writeFragment(category);
          -->
          </g:script>
       </g:groovy>
    
    </smooks-resource-list>
    

Chapter 10. Routing Output Data

10.1. Output Data Options

Smooks supports a number of different options when it comes to splitting and routing message fragments. The ability to split messages into fragments and route these fragments to different kinds of endpoints (files, JMS, etc.) is a very important capability. Smooks provides this along with the following features:
  • Basic Fragment Splitting: You may need to perform a dumb split on a message, that is, splitting all the order-item fragments in a message and route them to a file. ("Dumb split" means you don't need to perform any type of transformation on the split message fragments before routing, such as merging data from other parts of the message hierarchy before routing. (For example, add customer details info to the order-item fragment before routing)). Basic splitting and routing simply involves defining the XPath of the message fragment to be split out and the defining a routing component (for example, JBoss ESB or Camel) to route that unmodified split message fragment.
  • Complex Fragment Splitting: Basic fragment splitting works for "many use" cases and is what most splitting and routing solutions offer. Smooks extends the basic splitting capabilities by allowing you to perform transformations on the split fragment data before routing is applied. For example, merging in the customer-details order information with each order-item information before performing the routing order-item split fragment routing.
  • In Stream Splitting and Routing (huge message support): Because Smooks can perform routing "in stream" (not batched up for routing after processing the complete message), it is able to accommodate processing of huge message streams that are gigabytes in size.
  • Multiple Splitting and Routing: Conditionally split and route multiple message fragments (different formats XML, EDI, Java etc) to different endpoints in a single filtering pass of the input message stream. For example, routing an OrderItem Java object instance to the HighValueOrdersValidation JMS Queue for order items with a value greater than $1,000 and route all order items (unconditional) as XML/JSON to a HTTP endpoint for logging.

10.2. Routing with Apache Camel

  1. To route message fragments to Apache Camel endpoints, use the camel:route configuration from the http://www.milyn.org/xsd/smooks/camel-1.4.xsd configuration namespace.
  2. Route to a Camel endpoint by specifying the following in your Smooks configuration:
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"
             xmlns:camel="http://www.milyn.org/xsd/smooks/camel-1.4.xsd">
     
      <!-- Create some bean instances from the input source... -->
      <jb:bean beanId="orderItem"  ... ">
        <!-- etc... See Smooks Java Binding docs -->
      </jb:bean>
     
      <!-- Route bean to camel endpoints... -->
      <camel:route beanId="orderItem">
        <camel:to endpoint="direct:slow" if="orderItem.priority == 'Normal'" />
        <camel:to endpoint="direct:express" if="orderItem.priority == 'High'" />
      </camel:route>
     
    </smooks-resource-list>
    
    
    In the above example, Javabeans is routed from the Smooks BeanContext to the Camel Endpoints. You can also apply templates (such as FreeMarker) to these same beans and route the templating result instead of the beans (such as as XML and CSV).
    routeOnElement.

Chapter 11. Performance Tuning

11.1. Performance Tuning Tips

Cache and reuse the Smooks object.
Initialization of Smooks takes some time and therefore it is important it is reused.
Pool reader instances where possible
This can result in a huge performance boost, as some readers are very expensive to create.
Use SAX filtering where possible
SAX processing is a lot faster than DOM processing and uses less memory. It is mandatory for processing large messages. Check that all of the Smooks cartridges are SAX-compatible.
Turn off debug logging
Smooks performs some intensive debug logging in parts of the code. This can result in significant additional processing overhead and lower throughput.

Important

Remember that not having your logging configured at all may result in debug log statements being executed.
Only use the HTMLReportGenerator in a development environment.
When it has been enabled, the HTMLReportGenerator incurs a significant performance overhead and with large message, can even result in OutOfMemory exceptions.
Contextual selectors
Contextual selectors can obviously have a negative effect on performance. For example, evaluating a match for a selector like "a/b/c/d/e" will obviously require more processing than that of a selector like "d/e". Obviously there will be situations where your data model will require deep selectors, but where it does not, you should try to optimize your selectors for performance.
Where possible, avoid using the Virtual Bean Model and create beans instead of maps. Creating and adding data to Maps is a lot slower than creating "plain old Java objects" (POJOs) and calling the "setter" methods.

Chapter 12. Testing

12.1. Unit Testing

  • To undertake unit testing with Smooks, follow the example below:
    public class MyMessageTransformTest
    {
        @Test
        public void test_transform() throws IOException, SAXException
        {
            Smooks smooks = new Smooks(
                getClass().getResourceAsStream("smooks-config.xml") );
    
            try {
                Source source = new StreamSource(
                    getClass().getResourceAsStream("input-message.xml" ) );
                StringResult result = new StringResult();
    
                smooks.filterSource(source, result);
    
                // compare the expected xml with the transformation result.
                XMLUnit.setIgnoreWhitespace( true );
                XMLAssert.assertXMLEqual(
                    new InputStreamReader(
                    getClass().getResourceAsStream("expected.xml")), 
                    new StringReader(result.getResult()));
            } finally {
                smooks.close();
            }
        }
    }
    
    The test case above uses a piece of software called XMLUnit (see http://xmlunit.sourceforge.net for more information.)

    Note

    The following Maven dependency was needed for the above test:
    <dependency>
        <groupId>xmlunit</groupId>
        <artifactId>xmlunit</artifactId>
        <version>1.1</version>
    </dependency>
    

Chapter 13. Common Use Cases

13.1. Support for Processing Huge Messages

Smooks supports the following types of processing for huge messages:
  • One-to-one transformation: This is the process of transforming a huge message from its source format (for example, XML) to a huge message in a target format (EDI, CSV, XML, and so on).
  • Splitting and routing: Splitting of a huge message into smaller (more consumable) messages in any format (EDI, XML, Java etc.) and routing of those smaller messages to a number of different destination types (File, JMS, Database).
  • Persistence: Persisting the components of the huge message to a Database, from where they can be more easily queried and processed. Within Smooks, we consider this to be a form of splitting and routing (routing to a Database).
All of the above is possible without writing any code (that is, in a declarative manner). They can also be handled in a single pass over the source message, splitting and routing in parallel (plus routing to multiple destinations of different types and in different formats).

Note

When processing huge messages with Smooks, make sure you are using the SAX filter for better performance.

13.2. Transforming Huge Messages with FreeMarker

To process a huge message by transforming it into a single message of another format, you can apply multiple FreeMarker templates to the Source message Event Stream and output it to a Smooks.filterSource Result stream. You can do this in one of two ways:
  • Using FreeMarker and NodeModels for the model.
  • Using FreeMarker and a Java Object model for the model. The model can be constructed from data in the message, using the Javabean Cartridge.

13.3. Huge Messages and NodeModels

When a message is huge, you must identify its multiple NodeModels so that the runtime memory footprint is as low as possible. You cannot process the message using a single model because the full message is too big to hold in memory. In the case of the order message, there are two models: one for the main order data and one for the order-item data.
Most data that will be in memory at any one time is the main order data, plus one of the order-items. Because the NodeModels are nested, Smooks makes sure that the order data NodeModel never contains any of the data from the order-item NodeModels. Also, as Smooks filters the message, the order-item NodeModel will be overwritten for every order-item (that is, they are not collected).

13.4. Configuring Smooks to Capture Multiple NodeModels

  1. To configure Smooks to capture multiple NodeModels for use by the FreeMarker templates, you should configure the DomModelCreator visitor. It should be targeted at the root node of each model. Note again that Smooks also makes this available to SAX filtering (the key to processing huge messages).
    This is The Smooks configuration for creating the NodeModels for the message:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" 
                          xmlns:core="http://www.milyn.org/xsd/smooks/smooks-core-1.3.xsd"
                          xmlns:ftl="http://www.milyn.org/xsd/smooks/freemarker-1.1.xsd">
     
        <!--
        Filter the message using the SAX Filter (i.e. not DOM, so no
        intermediate DOM for the "complete" message - there are "mini" DOMs
        for the NodeModels below)....
        -->
        <core:filterSettings type="SAX" defaultSerialization="false" />
     
        <!--
        Create 2 NodeModels. One high level model for the "order"
        (header etc) and then one for the "order-item" elements...
        -->
        <resource-config selector="order,order-item">
            <resource>org.milyn.delivery.DomModelCreator</resource>
        </resource-config>
     
        <!-- FreeMarker templating configs to be added below... -->
    
  2. Next, apply the following FreeMarker templates:
    • A template to output the order header details, up to but not including the order items.
    • A template for each of the order items, to generate the item elements in the salesorder.
    • A template to close out the message.
    With Smooks, you can implement this by defining two FreeMarker templates. One to cover points one and three (combined) above, and a second to cover the item elements.
  3. Applt the first FreeMarker template. It is targeted at the order-items element and looks like this:
    <ftl:freemarker applyOnElement="order-items">
            <ftl:template><!--<salesorder>
        <details>
            <orderid>${order.@id}</orderid>
            <customer>
                <id>${order.header.customer.@number}</id>
                <name>${order.header.customer}</name>
            </customer>
        </details>
        <itemList>
        <?TEMPLATE-SPLIT-PI?> 
        </itemList>
    </salesorder>-->
            </ftl:template>
        </ftl:freemarker>
    
    The ?TEMPLATE-SPLIT-PI? processing instruction tells Smooks where to split the template, outputting the first part of the template at the start of the order-items element, and the other part at the end of the order-items element. The item element template (the second template) will be output in between.
  4. Apply the second FreeMarker template. This outputs the item elements at the end of every order-item element in the source message:
    <ftl:freemarker applyOnElement="order-item">
            <ftl:template><!-- <item>
        <id>${.vars["order-item"].@id}</id>
        <productId>${.vars["order-item"].product}</productId>
        <quantity>${.vars["order-item"].quantity}</quantity>
        <price>${.vars["order-item"].price}</price>
    </item>-->
            </ftl:template>
        </ftl:freemarker>
    </smooks-resource-list>
    
    Because the second template fires on the end of the order-item elements, it effectively generates output into the location of the ?TEMPLATE-SPLIT-PI? processing instruction in the first template. Note that the second template could have also referenced data in the order NodeModel.
  5. Apply a closing template of your choice.

    Note

    This approach to performing a one-to-one transformation of a huge message works because the only objects in memory at any one time are the order header details and the current order-item details (in the Virtual Object Model). Obviously it can' work if the transformation is so obscure as to always require full access to all the data in the source message, for example if the messages needs to have all the order items reversed in order (or sorted). In such a case however, you do have the option of routing the order details and items to a database and then using the database's storage, query and paging features to perform the transformation.

13.5. Message Splitting Requirements

You can process huge messages by splitting them into smaller messages that can be processed independently. Splitting and routing is sometimes also needed with smaller messages (message size may be irrelevant) where, for example, order items in an an order message need to be split out and routed (based on content) to different departments or partners for processing. Under these conditions, the message formats required at the different destinations may also vary as shown in the examples below:
  • destination1: required XML via the file system,
  • destination2: requires Java objects via a JMS Queue,
  • destination3: picks the messages up from a table in a Database etc.
  • destination4: requires EDI messages via a JMS Queue,
You can perform multiple splitting and routing operations to multiple destinations (of different types) in a single pass over a message.

13.6. Streaming Split Messages Through Smooks

As you stream the message through Smooks:
  • Repeatedly create a standalone message (split) for the fragment to be routed.
  • Repeatedly bind the split message into the bean context under a unique beanId.
  • Repeatedly route the split message to the required endpoint (whether it be a file, DB, JMS or ESB).
These operations happen for each instance of the split message found in the source message, for example, for each orderItem in an order message.

13.7. Methods for Creating Split Messages

  • A basic (untransformed/unenriched) fragment split and bind. This serializes a message fragment (repeatedly) to its XML form and stores it in the bean context as a String.
  • A more complex approach using the Java Binding and Templating Cartridges, where you configure Smooks to extract data from the source message and and into the bean context (using jb:bean configs) and then (optionally) apply templates to create the split messages. This has the following advantages:
    • Allows for transformation of the split fragments, that is, not just XML as with the basic option.
    • Allows for enrichment of the message.
    • Allows for more complex splits, with the ability to merge data from multiple source fragments into each split message, for example not just the orderItem fragments but the order header info too.
    • Allows for splitting and routing of Java Objects as the Split messages (for example, over JMS).

13.8. Serializing Messages

  1. To split and route fragments of a message, use the basic frag:serialize and *:router components (jms:router, file:router and so on) from the Routing Cartridge. The frag:serialize component has its own configuration in the http://www.milyn.org/xsd/smooks/fragment-routing-1.2.xsd namespace.
  2. Use the example below for serializing the contents of a SOAP message body and storing it in the bean context under the beanId of soapBody:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:frag="http://www.milyn.org/xsd/smooks/fragment-routing-1.2.xsd">
     
        <frag:serialize fragment="Envelope/Body" bindTo="soapBody" childContentOnly="true"/>
     
    </smooks-resource-list>
    
  3. Use this code to execute it:
    Smooks smooks = new Smooks(configStream);
    JavaResult javaResult = new JavaResult();
     
    smooks.filterSource(new StreamSource(soapMessageStream), javaResult);
     
    String bodyContent = javaResult.getBean("soapBody").toString().trim();
    
  4. To do this programatically, use this code:
    Smooks smooks = new Smooks();
     
    smooks.addVisitor(new FragmentSerializer().setBindTo("soapBody"), "Envelope/Body");
     
    JavaResult javaResult = new JavaResult();
    smooks.filterSource(new StreamSource(soapMessageStream), javaResult);
     
    String bodyContent = javaResult.getBean("soapBody").toString().trim();
    

13.9. Routing Split Messages Example

The following is a quick example, showing the configuration for routing split messages (this time order-item fragments) to a JMS destination for processing:
<?xml version="1.0"?>
<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:frag="http://www.milyn.org/xsd/smooks/fragment-routing-1.2.xsd" xmlns:jms="http://www.milyn.org/xsd/smooks/jms-routing-1.2.xsd">
 
    <!-- Create the split messages for the order items... -->
    <frag:serialize fragment="order-items/order-item" bindTo="orderItem" />
 
    <!-- Route each order items split mesage to the orderItem JMS processing queue... -->
    <jms:router routeOnElement="order-items/order-item" beanId="orderItem" destination="orderItemProcessingQueue" />
 
</smooks-resource-list>

Note

The jms:router could be substituted for any of the other routers. For example, if using with JBoss ESB, you could use the esbr:routeBean configuration to route the split message to any ESB endpoint.

13.10. File-based Routing

File-based routing is performed via the file:outputStream configuration from the http://www.milyn.org/xsd/smooks/file-routing-1.1.xsd configuration namespace. You can combine the following Smooks functionality to split a message out into smaller messages on the file system.

13.11. File-based Routing Components

Table 13.1. File-based Routing Components

Component Description
The Javabean Cartridge Extracts data from the message and holds it in variables in the bean context. You could also use DOM NodeModels for capturing the order and order-item data to be used as the templating data models.
file:outputStream This configuration from the Routing Cartridge is used for managing file system streams (naming, opening, closing, throttling creation etc).
Templating Cartridge (FreeMarker Templates) Used for generating the individual split messages from data bound in the bean context by the Javabean Cartridge (see first point above). The templating result is written to the file output stream (see second point above).

13.12. Huge Message Processing

Huge Message Processing

In the example, a huge order message needs to be sent while routing the individual order item details to file. The split messages contain data from the order header and root elements:

<?xml version="1.0"?>
<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"
                      xmlns:core="http://www.milyn.org/xsd/smooks/smooks-core-1.3.xsd"
                      xmlns:jb="http://www.milyn.org/xsd/smooks/javabean-1.4.xsd"
                      xmlns:file="http://www.milyn.org/xsd/smooks/file-routing-1.1.xsd"
                      xmlns:ftl="http://www.milyn.org/xsd/smooks/freemarker-1.1.xsd">
 
        <!--
        Filter the message using the SAX Filter (i.e. not DOM, so no
        intermediate DOM, so we can process huge messages...
        -->
        <core:filterSettings type="SAX" />
 
        <!-- Extract and decode data from the message. Used in the freemarker template (below).
               Note that we could also use a NodeModel here... -->
(1)     <jb:bean beanId="order" class="java.util.Hashtable" createOnElement="order">
            <jb:value property="orderId" decoder="Integer" data="order/@id"/>
            <jb:value property="customerNumber" decoder="Long" data="header/customer/@number"/>
            <jb:value property="customerName" data="header/customer"/>
            <jb:wiring property="orderItem" beanIdRef="orderItem"/>
        </jb:bean>
(2)     <jb:bean beanId="orderItem" class="java.util.Hashtable" createOnElement="order-item">
            <jb:value property="itemId" decoder="Integer" data="order-item/@id"/>
            <jb:value property="productId" decoder="Long" data="order-item/product"/>
            <jb:value property="quantity" decoder="Integer" data="order-item/quantity"/>
            <jb:value property="price" decoder="Double" data="order-item/price"/>
        </jb:bean>
 
        <!-- Create/open a file output stream. This is writen to by the freemarker template (below).. -->
(3)     <file:outputStream openOnElement="order-item" resourceName="orderItemSplitStream">
            <file:fileNamePattern>order-${order.orderId}-${order.orderItem.itemId}.xml</file:fileNamePattern>
            <file:destinationDirectoryPattern>target/orders</file:destinationDirectoryPattern>
            <file:listFileNamePattern>order-${order.orderId}.lst</file:listFileNamePattern>
 
            <file:highWaterMark mark="10"/>
        </file:outputStream>
 
        <!--
        Every time we hit the end of an <order-item> element, apply this freemarker template,
        outputting the result to the "orderItemSplitStream" OutputStream, which is the file
        output stream configured above.
        -->
(4)     <ftl:freemarker applyOnElement="order-item">
            <ftl:template>target/classes/orderitem-split.ftl</ftl:template>
            <ftl:use>
                <!-- Output the templating result to the "orderItemSplitStream" file output stream... -->
                <ftl:outputTo outputStreamResource="orderItemSplitStream"/>
            </ftl:use>
        </ftl:freemarker>
 
</smooks-resource-list>
Smooks Resource configurations shown in number one and two above define the Java Bindings for extracting the order header information (config #1) and the order-item information (config #2). When processing a huge message, make sure you only have the current order item in memory at any one time. The Smooks Javabean Cartridge manages all this for you, creating and recreating the orderItem beans as the order-item fragments are being processed.
The file:outputStream configuration in configuration number three manages the generation of the files on the file system. As you can see from the configuration, the file names can be dynamically constructed from data in the bean context. You can also see that it can throttle the creation of the files via the highWaterMark configuration parameter. This helps you manage file creation so as not to overwhelm the target file system.
Smooks Resource configuration number four defines the FreeMarker templating resource used to write the split messages to the OutputStream created by the file:outputStream (config #3). See how configuration 4 references the file:outputStream resource. The Freemarker template is as follows:
<orderitem id="${.vars["order-item"].@id}" order="${order.@id}">
    <customer>
        <name>${order.header.customer}</name>
        <number>${order.header.customer.@number}</number>
    </customer>
    <details>
        <productId>${.vars["order-item"].product}</productId>
        <quantity>${.vars["order-item"].quantity}</quantity>
        <price>${.vars["order-item"].price}</price>
    </details>
</orderitem>

13.13. JMS Routing

JMS Routing

JMS routing is performed via the jms:router configuration from the http://www.milyn.org/xsd/smooks/jms-routing-1.2.xsd configuration namespace. The following is an example jms:router configuration that routes an orderItem_xml bean to a JMS Queue named smooks.exampleQueue:

<?xml version="1.0"?>
<smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"
                      xmlns:core="http://www.milyn.org/xsd/smooks/smooks-core-1.3.xsd"
                      xmlns:jms="http://www.milyn.org/xsd/smooks/jms-routing-1.2.xsd"
                      xmlns:ftl="http://www.milyn.org/xsd/smooks/freemarker-1.1.xsd">
 
        <!--
        Filter the message using the SAX Filter (i.e. not DOM, so no
        intermediate DOM, so we can process huge messages...
        -->
        <core:filterSettings type="SAX" />
 
(1)     <resource-config selector="order,order-item">
            <resource>org.milyn.delivery.DomModelCreator</resource>
        </resource-config>
 
(2)     <jms:router routeOnElement="order-item" beanId="orderItem_xml" destination="smooks.exampleQueue">
            <jms:message>
                <!-- Need to use special FreeMarker variable ".vars" -->
                <jms:correlationIdPattern>${order.@id}-${.vars["order-item"].@id}</jms:correlationIdPattern>
            </jms:message>
            <jms:highWaterMark mark="3"/>
        </jms:router>
 
(3)     <ftl:freemarker applyOnElement="order-item">
            <!--
            Note in the template that we need to use the special FreeMarker variable ".vars"
            because of the hyphenated variable names ("order-item"). See http://freemarker.org/docs/ref_specvar.html.
            -->
            <ftl:template>/orderitem-split.ftl</ftl:template>
            <ftl:use>
                <!-- Bind the templating result into the bean context, from where
                it can be accessed by the JMSRouter (configured above). -->
                <ftl:bindTo id="orderItem_xml"/>
            </ftl:use>
        </ftl:freemarker>
 
</smooks-resource-list>
In this case, we route the result of a FreeMarker templating operation to the JMS Queue (that is, as a String). We could also have routed a full Object Model, in which case it would be routed as a Serialized ObjectMessage.

13.14. Routing to a Database

  1. To route an order and order item data to a database, you should define a set of Java bindings that extract the order and order-item data from the data stream:
    <?xml version="1.0"?>
    <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">
     
        <!-- Extract the order data... -->
        <jb:bean beanId="order" class="java.util.Hashtable" createOnElement="order">
            <jb:value property="orderId" decoder="Integer" data="order/@id"/>
            <jb:value property="customerNumber" decoder="Long" data="header/customer/@number"/>
            <jb:value property="customerName" data="header/customer"/>
        </jb:bean>
     
        <!-- Extract the order-item data... -->
        <jb:bean beanId="orderItem" class="java.util.Hashtable" createOnElement="order-item">
            <jb:value property="itemId" decoder="Integer" data="order-item/@id"/>
            <jb:value property="productId" decoder="Long" data="order-item/product"/>
            <jb:value property="quantity" decoder="Integer" data="order-item/quantity"/>
            <jb:value property="price" decoder="Double" data="order-item/price"/>
        </jb:bean>
    
  2. Next you need to define datasource configuration and a number of db:executor configurations that will use that datasource to insert the data that was bound into the Java Object model into the database. This is the datasource configuration (namespace http://www.milyn.org/xsd/smooks/datasource-1.3.xsd) for retrieving a direct database connection:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:ds="http://www.milyn.org/xsd/smooks/datasource-1.3.xsd">
     
        <ds:direct bindOnElement="#document"
                datasource="DBExtractTransformLoadDS"
                driver="org.hsqldb.jdbcDriver"
                url="jdbc:hsqldb:hsql://localhost:9201/milyn-hsql-9201"
                username="sa"
                password=""
                autoCommit="false" />
     
    </smooks-resource-list>
    
  3. It is possible to use a JNDI datasource for retrieving a database connection:
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd" xmlns:ds="http://www.milyn.org/xsd/smooks/datasource-1.3.xsd">
     
        <!-- This JNDI datasource can handle JDBC and JTA transactions or 
               it can leave the transaction managment to an other external component.
               An external component could be an other Smooks visitor, the EJB transaction manager
               or you can do it your self. -->
        <ds:JNDI
            bindOnElement="#document"
            datasource="DBExtractTransformLoadDS"
            datasourceJndi="java:/someDS"
            transactionManager="JTA"
            transactionJndi="java:/mockTransaction"
            targetProfile="jta"/>
     
    </smooks-resource-list>
    
  4. The datasource schema describes and documents how you can configure the datasource. This is the db:executor configuration (namespace http://www.milyn.org/xsd/smooks/db-routing-1.1.xsd):
    <?xml version="1.0"?>
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.1.xsd"
                          xmlns:db="http://www.milyn.org/xsd/smooks/db-routing-1.1.xsd">
     
        <!-- Assert whether it's an insert or update. Need to do this just before we do the insert/update... -->
        <db:executor executeOnElement="order-items" datasource="DBExtractTransformLoadDS" executeBefore="true">
            <db:statement>select OrderId from ORDERS where OrderId = ${order.orderId}</db:statement>
            <db:resultSet name="orderExistsRS"/>
        </db:executor>
     
        <!-- If it's an insert (orderExistsRS.isEmpty()), insert the order before we process the order items... -->
        <db:executor executeOnElement="order-items" datasource="DBExtractTransformLoadDS" executeBefore="true">
            <condition>orderExistsRS.isEmpty()</condition>
            <db:statement>INSERT INTO ORDERS VALUES(${order.orderId}, ${order.customerNumber}, ${order.customerName})</db:statement>
        </db:executor>
     
        <!-- And insert each orderItem... -->
        <db:executor executeOnElement="order-item" datasource="DBExtractTransformLoadDS" executeBefore="false">
            <condition>orderExistsRS.isEmpty()</condition>
            <db:statement>INSERT INTO ORDERITEMS VALUES (${orderItem.itemId}, ${order.orderId}, ${orderItem.productId}, ${orderItem.quantity}, ${orderItem.price})</db:statement>
        </db:executor>
     
        <!-- Ignoring updates for now!! -->
     
    </smooks-resource-list>
    

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...
}

Chapter 15. Apache Camel Integration

15.1. Apache Camel-Smooks Integration

The Camel-Smooks integration lets you to access all of Smooks' capabilities from within Apache Camel. You can take an existing Smooks configuration and use this in your Camel routes.

15.2. Methods of Using Smooks in Apache Camel

There are three ways in which you can use Smooks in Apache Camel:
  • SmooksComponent
  • SmooksDataformat
  • SmooksProcessor

15.3. SmooksComponent

The SmooksComponent is a Camel module which you can use when you want to process the Camel message body using Smooks.

15.4. Using the SmooksComponent

  1. Add a route to your Camel route configuration as shown below:
    from("file://inputDir?noop=true")
    .to("smooks://smooks-config.xml")
    .to("jms:queue:order")
    
  2. Edit the values in the 'smooks-config-xml' file. You cannot tell what type of output the SmooksComponent is producing just by looking at the route configuration. Instead, this is expressed in the Smooks configuration via the exports element.

    Note

    If you would prefer to configure Smooks programmatically, you can set this using the SmooksProcessor

15.5. SmooksComponent Configuration

An Apache component can use any options that are specified after the Smooks configuration file. Currently only one option is available for the SmooksComponent:
  • reportPath: this is the path (including the file name) to the Smooks Execution Report that is to be generated.

15.6. SmooksDataFormat

SmooksDataFormat is an Apache Camel DataFormat. It is capable of transforming one data format to another and back again. Use this when you are only interested in transforming from one format to another and do not need to use any other Smooks features.

15.7. SmooksDataFormat Example

This example code demonstrates how to use SmooksDataFormat to transform a comma-separated value string into a java.util.List of Customer object instances:
SmooksDataFormat sdf = new SmooksDataFormat("csv-smooks-unmarshal-config.xml");
from("direct:unmarshal")
.unmarshal(sdf)
.convertBodyTo(List.class)
.to("mock:result");

15.8. SmooksProcessor

Using the SmooksProcessor gives you full control over Smooks. (If, for example, you want to programmatically create the underlying Smooks instance you would use this component.) When you are using the SmooksProcessor, you can programmatically configure Smooks and then pass a Smooks instance to its constructor.

15.9. SmooksProcessor Example

This example demonstrates how to use the SmooksProcessor in an Apache Camel route:
Smooks smooks = new Smooks("edi-to-xml-smooks-config.xml");
ExecutionContext context = smooks.createExecutionContext();
...
SmooksProcessor processor = new SmooksProcessor(smooks, context);
 
from("file://input?noop=true")
.process(processor)
.to("mock:result");

Similar to the SmooksComponent we have not specified the result type that Smooks produces (if any that is). Instead this is expressed in the Smooks configuration using the exports element or you can do the same programmatically like this:

Smooks smooks = new Smooks();
ExecutionContext context = smooks.createExecutionContext();
smooks.setExports(new Exports(StringResult.class));
SmooksProcessor processor = new SmooksProcessor(smooks, context);
...
from("file://input?noop=true")
.process(processor)
.to("mock:result");

15.10. Camel Properties

Table 15.1. Camel Properties

Name Description
camel-dataformat
This example illustrates how to use the SmooksDataFormat. (A DataFormat is a class that implements Camel's org.apache.camel.spi.DataFormat.)
camel-integration
This example illustrates how to use the Camel SmooksComponent. ("smooks://file:./configs/smooks-config.xml")
splitting-camel
This example demonstrates how to use Smooks and Apache Camel to process a UN/EDIFACT message interchange, splitting the individual interchange messages out into Java and XML fragments and routing the fragments using Apache Camel.

Chapter 16. Integrating Smooks With the JBoss Enterprise SOA Platform

16.1. Message Transformation

The JBoss Enterprise SOA Platform's message transformation functionality is provided by the SmooksAction component. This is an ESB Action module that allows you to plug the Smooks framework into an ESB action processing pipeline. This lets you set up an ESB action that exposes Smooks and needs a configuration file.

Note

Your JBoss Enterprise SOA Platform product comes with a number of sample transformation quick starts. Look at the transform_ quickstarts to gain an understanding of how they work. The XML2XML example is good to study when you are learning.

16.2. The resource-config Property

In its most basic configuration, message transformation uses a resource-config property that references a Smooks configuration file.
The value of the resource-config property can be any URI-based resource, as defined by the URIResourceLocator class:
<action name="transform" class="org.jboss.soa.esb.actions.converters.SmooksTransformer">
        <property name="resource-config" value="/smooks-config.xml" ></property>
</action>

16.3. Input/Output Configuration

This action uses the MessagePayloadProxy to access the message payload. By default, it is configured to obtain the payload from the Message.Body.DEFAULT_LOCATION location. It will also return it to here after it has processed it.

Note

Override these default settings by altering the get-payload-location and set-payload-location action properties.

16.4. Java Output Configuration

  1. To transform your message into a Java object, observe the Transform_XML2POJO quickstarts examples.
  2. Use the constructed Java object models as part of a model-driven transformation. You can also allow them to be used by other ESB action instances that follow on after the SmooksAction in the pipeline. Such Java object graphs are available to subsequent pipeline action instances because they are attached to the ESB message output by this action and which is then input to the following actions.
  3. Attach the objects to the ESB message output using the Message.getBody().add(...) under a key based directly on the beanId objects as defined in the Smooks Java bean configuration. This means that the objects are available through the ESB Message Body by performing Body.get(beanId) calls.
  4. You can also attach the full Java object map to the output message by adding a java-output-location property:
    <action name="transform" class="org.jboss.soa.esb.actions.converters.SmooksTransformer">
            <property name="resource-config" value="/smooks-config.xml" ></property>
            <property name="java-output-location" value="order-message-objects-map" ></property>
        </action>
    
  5. This is a shorthand way to bind the map to the Default Message Body Location:
     <action name="transform" class="org.jboss.soa.esb.actions.converters.SmooksTransformer">
            <property name="resource-config" value="/smooks-config.xml" ></property>
            <property name="java-output-location" value="$default" ></property>
        </action>
    

16.5. Profile-Based Transformation

  1. To perform a profile-based transformation, observe the example below. You need to define a single transformation configuration file (smooks-config.xml). Messages are being exchanged between three different sources and one target destination. The ESB needs to transform the messages supplied (in different formats) by each of the different three sources into Java objects before sending them to the destination service. From an ESB perspective, there is a single service configuration for the destination:
    <service category="ServiceCat" name="TargetService" description="Target Service">
            <listeners>
                <!-- Message listners for getting the message into the action pipeline... -->
                <jms-listener name="Gateway-Listener" busidref="quickstartGwChannel" is-gateway="true"></jms-listener>
                <jms-listener name="Service-Listener" busidref="quickstartEsbChannel"></jms-listener>
            </listeners>
            <actions>
    
                
                <action name="transform" class="org.jboss.soa.esb.actions.converters.SmooksTransformer">
                    <property name="resource-config" value="/smooks-config.xml"></property>
                </action>
    
                <!-- An action to process the Java Object(s) -->
                <action name="process" class="com.acme.JavaProcessor" ></action>
    
            </actions>
        </service>
    
  2. Define three different transformation, one for each source. This is done using a Smooks Message Profiling.
  3. Store the definitions in three Smooks configuration files (from_source1.xml, from_source2.xml and from_source3.xml).
  4. In each of the above files, specify the default-target-profile for that configuration set:
     <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.0.xsd" default-target-profile="from:source1">
            <!-- Source1 to Target Java message transformation resource configurations... -->
        </smooks-resource-list>
    
  5. Add those files to the top-level smooks-config.xml:
    <smooks-resource-list xmlns="http://www.milyn.org/xsd/smooks-1.0.xsd">
            <import file="classpath:/from_source1.xml" ></import>
            <import file="classpath:/from_source2.xml" ></import>
            <import file="classpath:/from_source3.xml" ></import>
        </smooks-resource-list>
    
    You have now configured the system to load a single SmooksAction instance with three different transformations, each of which is defined under it's own unique profile name.
  6. In order for the SmooksAction to know which of the three transformations is to be applied to a given message, you must set the message's from property before the message flows into the SmooksAction. You can either do this at the source itself or in a content-based action that precedes the SmooksAction.

    Note

    The JBoss Enterprise SOA Platform also supports other profiles in addition to from, namely from-type, to and to-type. You can use these combined, leading to more intricate exchange-based transformations.

16.6. Transform_XML2POJO2

Message transformations can be implemented as quickstarts called /samples/quickstarts/transform_XML2POJO2/. In this quickstart there are two message sources. The quickstart runs a Groovy script on the action pipeline that detects and sets the incoming message's from profile.

16.7. Transform_XML2POJO2 Files

These are the files used in transformations:
  • jboss-esb.xml: this is the JBoss ESB configuration file.
  • smooks-config.xml: this file contains the top-level transformation configuration.
  • from-dvdstore.xml: this contains the DVD Store message transformations configuration, which is imported into the top-level smooks-config.xml file. (Make note of the profile configuration.) This is configuration is designed to transform a DVD Store message into Java Objects.
  • from-petstore.xml: this file contains the Pet Store message transformations configuration, which is imported into the top-level smooks-config.xml file. (Make note of the profile configuration.) This is configuration is designed to transform a DVD Store message into Java Objects.
  • check-origin.groovy: this is a simple Groovy script that determines the origin of each message based on it's contents.

Appendix A. Revision History

Revision History
Revision 5.3.1-79.4002013-10-31Rüdiger Landmann
Rebuild with publican 4.0.0
Revision 5.3.1-79Wed Feb 06 2013 David Le Sage
Built from Content Specification: 9506, Revision: 371839 by dlesage

Legal Notice

Copyright © 2013 Red Hat, Inc.
This document is licensed by Red Hat under the Creative Commons Attribution-ShareAlike 3.0 Unported License. If you distribute this document, or a modified version of it, you must provide attribution to Red Hat, Inc. and provide a link to the original. If the document is modified, all Red Hat trademarks must be removed.
Red Hat, as the licensor of this document, waives the right to enforce, and agrees not to assert, Section 4d of CC-BY-SA to the fullest extent permitted by applicable law.
Red Hat, Red Hat Enterprise Linux, the Shadowman logo, JBoss, OpenShift, Fedora, the Infinity logo, and RHCE are trademarks of Red Hat, Inc., registered in the United States and other countries.
Linux® is the registered trademark of Linus Torvalds in the United States and other countries.
Java® is a registered trademark of Oracle and/or its affiliates.
XFS® is a trademark of Silicon Graphics International Corp. or its subsidiaries in the United States and/or other countries.
MySQL® is a registered trademark of MySQL AB in the United States, the European Union and other countries.
Node.js® is an official trademark of Joyent. Red Hat Software Collections is not formally related to or endorsed by the official Joyent Node.js open source or commercial project.
The OpenStack® Word Mark and OpenStack logo are either registered trademarks/service marks or trademarks/service marks of the OpenStack Foundation, in the United States and other countries and are used with the OpenStack Foundation's permission. We are not affiliated with, endorsed or sponsored by the OpenStack Foundation, or the OpenStack community.
All other trademarks are the property of their respective owners.