Using Eclipse for Developing SAP XI User Defined Functions
When creating User Defined Functions (UDFs) in SAP XI, the editor for entering the Java source code is merely a plain text box. Being used to develop Java code using Eclipse, this feels like being back in the Dark Ages, missing simple things like code completion etc.
Also, simple syntax and programming errors takes a fair number of editing/saving/executing/wondering/editing/... cycles in SAP XI to get a working function in place, and the runtime errors reported by SAP XI may not always lead you on the right path, as the code you enter is compiled together in a class resulting in severely misaligned line numbers.
A much more joyful approach is to develop the UDFs using the SAP NetWeaver Development Infrastructure, or to use the freely available Eclipse IDE to do development and testing before pasting the final result into the plain text box of SAP XI. Also, if you are developing UDFs that are more than a couple of lines, a better approach will be to build a complete JAR file with your UDF and reference it from within SAP XI.
This article describes a small foundation for a SAP XI UDF Workbench that can be used to develop, test, and build SAP XI User Defined Functions using Eclipse.
Setting up Eclipse
To create a small testbench for SAP XI UDFs in Eclipse, we need a few JAR files from a SAP XI installation in order to be able to reference types commonly used in UDFs like Container, GlobalContainer, AbstractTrace, and ResultList.
These types are defined in the JAR files aii_map_api.jar and aii_mt_rt.jar, and can be retrieved from the SAP XI installation.
If you do not have direct access to you SAP XI installation, contact your favorite SAP Basis administrator and ask him politely to retrieve the files for you.
Now, inside Eclipse, you create a new Java project - say UDF - and add the two JAR files to a directory called /lib. Next, you right-click the JAR files and select "Build Path | Add to Build Path", and you have a ready-to-go Java project for developing SAP XI UDFs, which should look something like the picture in figure 1.
SAP XI UDF Example
To try it out, lets create a small UDF that will count instances of values it is presented with during mapping, and having it return that number when called. Calling the function with "ABC", "DEF", "ABC", "XYZ", "XYZ", "DEF", and "ABC" would return values "1", "1", "2", "1", "2", "2", and "3". The function is created as if it was a UDF with Cache = Value, resulting it in taking String and Container parameters.
There might be real world usage scenario for such a function, but for the purpose of demonstration it serves two specific purposes:
- When entering the java code, we get code completion on types like
Container,Map, andHashMap. The import statements created for the last two types can be using when entering the import information in SAP XI. The import statement forContaineris not required as it is referenced by default by XI. - When accessing the
containervariable, we get method completion allowing easy access to the syntax of.getParameter()methods on thecontainerobject. The same would be true if we accessed other members of theContainerclass, like for example.getTrace().
I've just created a java class dk.conspicio.sap.xi.udf.Example1 and entered the method outlined below.
public String instanceCount(String a, Container container)
{
Map instanceMap;
Integer value;
// Get existing map of instances.
instanceMap = (Map) container.getParameter("instanceCountMap");
// If no map exist, create a new one.
if (instanceMap == null)
{
instanceMap = new HashMap();
container.setParameter("instanceCountMap", instanceMap);
}
if (instanceMap.containsKey(a))
// Read and increase existing value.
value = new Integer(((Integer) instanceMap.get(a)).intValue() + 1);
else
{
// Create new value with count 1.
value = new Integer(1);
container.getTrace().addDebugMessage("The first instance of " + a + " was found.");
}
// Write value back to instance map.
instanceMap.put(a, value);
return value.toString();
}When entering code like the one above we get on-the-fly syntax check and code completion helping us make sure that the UDF is at least correct Java code that can be pasted into SAP XI. Just this simple fact is a huge improvement over entering the code directly in the SAP XI editor.
However, the code we have entered cannot be tested directly in Eclipse, because the UDF uses the Container and the AbstractTrace classes. Instances of these classes are normally provide by the SAP XI mapping runtime when executing the function inside a SAP XI mapping.
Adding Testing Capabilities
To allow us to actually run and test a UDF like the one above, we need to execute the function in a context where it has access to instances of classes like Container and GlobalContainer. If the function had been created with Cache = Context or Cache = Queue, we would also require access to an instance of the ResultList object used to return the result of the function.
To enable our testbench to have access to instances of Container, GlobalContainer, and ResultList we need to develop implementation versions of these interfaces. Furthermore, the Container provides access to the AbstractTrace class used for submitting debugging and tracing information, and that needs to be part of our testbench as well.
Developing instances of these classes is no big deal, as the interfaces are provided for us in the JAR files we have referenced.
In my small testbench project, I've created a package dk.conpicio.sap.xi.udfbench and added implementation versions called AbstractTraceImpl, ContainerImpl, GlobalContainerImpl, and ResultListImpl. These classes provides straight forward implementations of the SAP XI interfaces used during mapping.
Having the above implementations we will be able to test the simple instanceCount(...) method entered previously, simply by creating instances of GlobalContainerImpl, ContainerImpl and of course our Example1 class and then calling the instanceCount(...) method using the ContainerImpl instance.
GlobalContainer gc = new GlobalContainerImpl();
Container c = new ContainerImpl(gc, null, null);
// Create instance of our example class holding the example functions.
Example1 udf = new Example1();
// Make call to instanceCount() to check that it works.
String[] input = new String[] {"ABC", "DEF", "ABC", "XYZ", "XYZ", "DEF", "ABC"};
System.out.println("\nProcessing instanceCount:");
for (int i = 0; i < input.length; i++)
{
String value = udf.instanceCount(input[i], c);
System.out.println("Input '" + input[i] + "' = " + value);
}Running the above inside the main method of Example1 will produce the following output:
Processing instanceCount: [DEBUG] The first instance of ABC was found. Input 'ABC' = 1 [DEBUG] The first instance of DEF was found. Input 'DEF' = 1 Input 'ABC' = 2 [DEBUG] The first instance of XYZ was found. Input 'XYZ' = 1 Input 'XYZ' = 2 Input 'DEF' = 2 Input 'ABC' = 3
From inspecting the output we see that the instance of Container works inside our UDF, and that the implementation of AbstractTrace allows us to use the debugging methods. The trivial implementation of AbstractTrace simply prints information on stderr, but the testbench code allows you to configure alternatives - such as a file.
To see the ResultListImpl in use, we create a small UDF that expects a whole context or queue (Cache = Context or Cache = Queue), and where data is integers. The function then takes numbers for each context and sums them up, providing one number per context.
public void contextSum(String[] numbers, ResultList result, Container container)
{
int sum = 0;
for (int i = 0; i < numbers.length; i++)
{
if (numbers[i] == ResultList.CC)
{
result.addValue(String.valueOf(sum));
sum = 0;
}
else
sum += Integer.parseInt(numbers[i]);
}
result.addValue(String.valueOf(sum));
}To execute this function, we add the following code to the main method of Example1:
// Create new Container instance and a ResultList.
c = new ContainerImpl(gc, null, null);
ResultList result = new ResultListImpl();
// Call contextSum() method to check that it works on our simple array.
String[] numbers = new String[] {"1", "2", "3", ResultList.CC, "4", "5", "6"};
System.out.println("\nProcessing contextSum:");
udf.contextSum(numbers, result, c);
UDFBench.printArray(numbers); System.out.println();
UDFBench.printResultList((ResultListImpl) result); System.out.println();Running the above will produce the following output:
Processing contextSum: 1,2,3,[CC],4,5,6 6,15
As demonstrated, we have a small and very simple testbench for developing and testing our SAP XI UDFs before compiling the code and deploying it as a JAR file in SAP XI, or simply pasting the code from Eclipse into the SAP XI UDF textbox.
In the examples above, I've simply executed the test code for my UDFs inside the main method of Example1, which is OK if only a simple test is required. In general, to create a more substantial unit test suite of all my SAP XI UDFs, I would use the JUnit framework (see http://www.junit.org/) and run the UDFs in test cases with assertions.
In the examples above, the test input for my UDFs are either simple strings, or arrays of strings with context changes (ResultList.CC). Again, this serves the purpose if I only want to make a simple 10 lines test of the function and I can produce a simple input that will test all aspects of the developed function.
Extracting Test Data from XML Documents
In some cases though, we might be interessed in executing and testing the functions on input collected from a real world XML document. This could be an example message we have received to help testing our SAP XI mapping.
To do this inside this simple testbench, we need some simple way of getting input for the UDF functions from such an XML document automatically. Extracting test data from an XML document can be accomplished in a number of ways, including using SAX, DOM, XLST, or other technologies.
Ideally, for a small testbench like the one presented here, I personally think I that a simple approach would be to be able to write an XPath expression like "//invoice/items/item/quantity" and then get all the quantity elements from a document containing invoices back as a context queue (i.e. an array of data with appropriate context changes applied).
Java supports executing XPaths expressions against a XML document, so the only thing we need to add for our testbench purpose is the ability to return the result as arrays with the appropriate context changes (ResultList.CC).
The "trick" is to observe that a context (change) is basically a question of if two nodes share the same parent node. In the general case it might not be the parent node, but the grandparent, great grandparent, etc. node that they will have in common. All this, you know very well if you have been setting the context for a mapping inside the SAP XI mapping editor.
OK - so all of this I've wrapped up into a Java class called dk.conspicio.sap.xi.udfbench.XMLExtractor which implements javax.xml.namespace.NamespaceContext to allow the class itself to control XML namespaces for the documents we want to process.
The main methods for extracting values, contexts, or queues from real world XML documents are:
String[] extractValues(String expression):
Allows you to extract values as a single array with no context switches, in order to test UDFs defined with cache = value.String[][] extractContexts(String expression):
Allows you to extract values as a multi-dimentional array having one entry for for each context. Each row will hold the data for a single context with no context switches, and can be used to test UDFs defined with cache = contextString[] extractQueues(String expression):
This allows you extract the full queue including context switches in order to test UDFs defined as cache = queueString[] extractValues(String expression, int context):
As for theextractValues(...)method above, but lets you set the context.String[][] extractContexts(String expression, int context):
As for theextractContexts(...)method above, but lets you set the context.String[] extractQueues(String expression, int context):
As for theextractQueues(...)method above, but lets you set the context.
To exemplify the usage of the XMLExtractor, I've included a simple document in data/invoices.xml of the xi-udf-bench project. The example code showing how to use the UDF bench is primarily included in the dk.conspicio.sap.xi.udf.Example1 java class.
The followin Java code shows how the XMLExtractor class can be used to extract a queue to test the contextSum() UDF.
// Simple test using the XML Extractor.
XMLExtractor xmlex;
// Create XML extractor and feed our simple invoices.xml file into it.
xmlex = new XMLExtractor();
xmlex.setInput(new File("data/invoices.xml"));
// Make sure you set the appropriate namespaces. This should be available in the
// documentation of the XML format you are using - or by simply inspecting a sample
// document.
xmlex.addPrefix("ns", "http://xsd.conspicio.dk/invoices/v1");
// Create new container and result list.
c = new ContainerImpl(gc, null, null);
result = new ResultListImpl();
// Try out the contextSum(..) method using the input extracted from the XML document.
System.out.println("\nXML Extractor example.");
System.out.println("Processing contextSum with context = 3 (invoice):");
String[] quantities = xmlex.extractQueues("//ns:quantity", 3);
udf.contextSum(quantities, result, c);
UDFBench.printArray(quantities); System.out.println();
UDFBench.printResultList((ResultListImpl) result); System.out.println();
System.out.println("\nXML Extractor example.");
System.out.println("Processing contextSum with context = 4 (invoices):");
c = new ContainerImpl(gc, null, null);
result = new ResultListImpl();
quantities = xmlex.extractQueues("//ns:quantity", 4);
udf.contextSum(quantities, result, c);
UDFBench.printArray(quantities); System.out.println();
UDFBench.printResultList((ResultListImpl) result); System.out.println();Please note : The code example shows the difference between extracting data from different context levels. The first execution extracts a queue at context level 3 which means that %lt;invoice> tags will denote context changes while the second execution uses context level 4 (<invoices>).
When running the contextSum() UDF this means that the values of <quantity> will be summed up per invoice or for the complete file. The output is shown below.
XML Extractor example. Processing contextSum with context = 3 (invoice): 2,28,[CC],4 30,4 XML Extractor example. Processing contextSum with context = 4 (invoices): 2,28,4 34
Try it out...
The source code for my (very) simple SAP XI UDF workbench can be downloaded from the "Projects" page. Download, use, and extend it to suit your own needs.
Make sure to read the README and LICENSE files for further information.
Feedback is always welcome using the contact form on this site.