Sunday, November 3, 2013

Handling Groovy scripts in Mule ESB

In my work project we had to make plenty of XML manipulations on some complex structures. As Mule supports Groovy (scripts) natively, we decided to make use of Groovy's XMLSlurper.
Two things I wanted to have covered, before I begin: How to easily handle those scripts in Mule Studio and how to unit test them.

1. Groovy scripts in Mule Studio

Mule, when it comes to Groovy components, can embed script code inside the Mule configuration file or use an external script file. Of course, at all times, I don't want to clutter my Mule code with embedded scripts. The only exception I make, is when I need to do something simple and I'm unable to use Mule Expression Language to achieve that (i.e. throwing an exception directly from flow - JIRA).

First thing I did was installation of Groovy plugin for the IDE. Latest Mule Studio version is 3.5.0, which is based on Eclipse 3.8. Update site URL for the plugin is: http://dist.springsource.org/release/GRECLIPSE/e4.2/. My example project was built on Mule ESB 3.4.0. It uses Groovy 1.8.6, hence we have to add an extra Groovy compiler when installing plugin and we have to switch it on in Groovy preferences afterwards.

Next, we need to think about where to locate our scripts. There are several possibilities, but I find most convinient to put them in src/main/scripts in a subfolder named as a Mule configuration file, which uses it. Then, we need to force IDE to be as useful as possible, especially the m2e plugin built in Mule Studio. I always work on Maven-supported Mule projects and strongly recommend to do the same.

  • Enable Groovy scripts folders in Groovy preferences and add src/main/scripts and src/test/scripts patterns (note that Compiler version is already set to 1.8.6):
  • Now we need to make sure that Maven will put those scripts in proper location. Ideal for me, is to put them in classes/scripts directory. It will cause nice separation with other project resources. Achieving that appeared to be simple. In pom.xml specify resources in the <build> part:
<build>
  <resources>
    <resource>
      <directory>src/main/resources</directory>
    </resource>
    <resource>
      <directory>src/main/scripts</directory>
      <targetPath>scripts</targetPath>
    </resource>
  </resources>
  ...
</build>
Maven's build <resources/> element is described here. When doing research about the above I found out something interesting. With the default structure of pom.xml file generated by Mule Studio, all Mule configuration files are duplicated in the built zip file. Sample Mule application structure generated by Maven would look like:
/
 \- classes
  \- mule-config.xml
  |- mule-deploy.properties
  |- mule-app.properties
 |- lib
 |- mule-config.xml
 |- mule-deploy.properties
 |- mule-app.properties
All these bolded files are redundant (per Mule's application format reference). Responsible for that mess is Build Helper Maven Plugin:
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>build-helper-maven-plugin</artifactId>
  <version>1.7</version>
  <executions>
    <execution>
      <id>add-resource</id>
      <phase>generate-resources</phase>
      <goals>
        <goal>add-resource</goal>
      </goals>
      <configuration>
        <resources>
          <resource>
            <directory>src/main/app/</directory>
          </resource>
        </resources>
      </configuration>
    </execution>
  </executions>
</plugin>
If I remove that plugin part, everything still seems to be working perfectly fine and my project's output structure is neat and clean:
/
 \- classes
  \- scripts
   \- GroovyScriptTesting
    \- CalculateSquareNumber.groovy
  |- log4j.properties
 |- GroovyScriptTesting.xml
 |- mule-deploy.properties
 |- mule-app.properties
It looks like maven-mule-plugin is doing all the necessary work for src/main/app folder. So, if you know why the build helper plugin is set up in the pom.xml in the first place or if you're having any problem without it, please let me know.

In the end, with everything in place, my project in Mule Studio should look like this:


and we are ready to go to the second part.

2. Unit testing Groovy scripts

Mule provide direct access to Mule context objects and variables in Groovy scripts the same way as it does for MEL. In terms of testing, it's important to decide and be consequent about how we are accessing Mule variables:

If I'm sure that the variable will be defined during script execution, I access them directly (as 'number' in the example):
def square = number.toInteger() * number.toInteger()
If it's possible that the variable won't be set up (can be a part of the script logic - to check for variable presence), then I would try to get it from the message context (it's always available):
def number = message.getInvocationProperty('number')
and then do some null checks etc. Otherwise, I would get Groovy's MissingPropertyException.
Setting variables via scripts always needs to happen using message.setProperty method.

Let's look at this simple Groovy script (CalculateSquareNumber.groovy):
def square = number.toInteger() * number.toInteger()
message.setInvocationProperty('squareNumber', square)
and it's unit test (CalculateSquareNumberTest.java):
@RunWith(JUnitParamsRunner.class)
public class CalculateSquareNumberTest {

	private static final String PAYLOAD = "payload";

	@Test
	@Parameters(method = "numbersAndSquares")
	public void test(int number, int square) throws Exception {
		
		Binding binding = new Binding();
		binding.setVariable("number", number);
		binding.setVariable("message", TestMuleMessage.withPayload(PAYLOAD));
		
		GroovyShell shell = new GroovyShell(binding);
		shell.evaluate(getFile("/scripts/GroovyScriptTesting/CalculateSquareNumber.groovy"));
		MuleMessage message = (MuleMessage) binding.getVariable("message");
		
		assertThat((Integer)message.getInvocationProperty("squareNumber"), is(square));
	}
	
	@SuppressWarnings("unused")
	private Object[] numbersAndSquares() {
		return $(
             $(3, 9),
             $(5, 25),
             $(10, 100),
             $(12, 144)
        );
	}
	
	private File getFile(String pathToFile) throws URISyntaxException, FileNotFoundException {
		URL url = CalculateSquareNumberTest.class.getResource(pathToFile);
		if (url == null) {
			throw new FileNotFoundException("Couldn't find: " + pathToFile);
		}
		return new File(url.toURI());
	}
}

To evaluate Groovy scripts in our test we need to use GroovyShell along with Binding's setVariable() method to make variables available. As you can see it's pretty easy, the trickier part is how to pass proper MuleMessage instance to our test. To do that, I have prepared helper TestMuleMessage class, which is preparing message with default Mule context:
public class TestMuleMessage {

	public static MuleMessage withPayload(Object payload) {
		
		MuleContextFactory contextFactory = new DefaultMuleContextFactory();
		MuleContext muleContext = null;
		try {
			muleContext = contextFactory.createMuleContext();
		} catch (InitialisationException e) {
			e.printStackTrace();
		} catch (ConfigurationException e) {
			e.printStackTrace();
		}
		
		return new DefaultMuleMessage(payload, muleContext);
	}
}
Another useful thing in the test is the helper getFile() method which retrieves script file not from hardcoded full path, but from classpath instead, which is always a better idea. Usually, you would put it in some test util class.

Your main target for assertions is MuleMessage object, which can be retrieved using Binding getVariable() method. There you can check for variables' values or message payload itself.

As you can see, keeping few simple rules, can make work with Groovy scripts (in Mule) a pleasant, easily testable experience. Sample project, I prepared, is using Quartz endpoint and Groovy script to count and display square number of number 5 :) It is available here.

2 comments:

  1. Thanks for a very helpful post. I just started studying Mule Studio and was looking for best practices for project structure organization. Mule documentation is vague on this point. I faced the same issue with duplication of files in zip file - removed this plugin configuration.

    ReplyDelete
  2. Glad you find it useful! That reminds me that I need to catch up on my blog! :)

    ReplyDelete