Skip to Content
Author's profile photo Martin Pankraz

Get groov’in with your iFlows – groovy scripting with Eclipse for CPI

Today, I’m going to make your life as integration developer a whole lot easier. Whenever there’s a complex transformation requirement, for instance combining rows from an excel sheet into one object by a common identifier, Scripting is a great way to tackle the issue. SAP offers two flavors of scripting – JavaScript and Groovy.

The only downside is that the SAP Cloud Platform Integration WebUI does development environment. So, basically, it is like developing whilst blindfolded and then hoping for the best during deployment and runtime. As an integration developer you want a little more than syntax highlighting. You at least want to have syntax checks, code completion and the ability to test messages easily without deploying entire iFlows and executing them.

Fortunately, there are some clever people at the SAP community, who can show us how to setup a development environment for those CPI scripts.

I am going to build upon their amazing work and equip you with a custom iFlow and a Postman request that allows you to “hunt” for the Java-Libraries residing on your own Cloud Platform Integration tenant. You will need them to be able to execute your groovy script from eclipse.

Find out how my custom iFlow finds java libraries on your CPI tenant on Vadim’s amazing blog post.

Also, Eng Swee produced a fantastic write up on how to start your first functional tests with a groovy script.

Ready to groove?

So let’s say you’ve installed Eclipse Neon, the groovy plugin and added Eng Swee’s initial scripts for message processing and running the test. Then, you notice that the project produces errors due to missing dependencies. This is what we need to fix.

Have a look at my slightly adapted demo structure below, which I am going to refer to from hereon.

Message processing script “src/com/convista/groovydemo/Script1.groovy”:

package com.convista.groovydemo
import java.util.HashMap;

def Message processData(Message message) {
       def body = message.getBody();
       message.setBody(body + "Body is modified");
       def map = message.getHeaders();
       def value = map.get("oldHeader");
       message.setHeader("oldHeader", value + "modified");
       message.setHeader("newHeader", "newHeader");
       map = message.getProperties();
       value = map.get("oldProperty");
       message.setProperty("oldProperty", value + "modified");
       message.setProperty("newProperty", "newProperty");
       return message;

Note: If you run into encoding problems, for example, with messages, created by the csv converter, consider adding the java.lang.String class to your get-method (e.g. message.getBody(java.lang.String)).

And the script “Tester.groovy” that executes the actual test. This piece simulates the message passed by the CPI framework so to say.

package com.equalize.groovy.testing


// Load Groovy Script
GroovyShell shell = new GroovyShell()
def script = shell.parse(new File("src/com/convista/groovydemo/Script1.groovy"))

// Initialize message with body, header and property
Message msg = new MessageImpl()
msg.setBody(new String("Hello Groovy World"))
msg.setHeader("oldHeader", "MyGroovyHeader")
msg.setProperty("oldProperty", "MyGroovyProperty")

// Execute script

// Display results of script in console
println("Body:\r\n" + msg.getBody())

def displayMaps = { String mapName, Map map ->
	println mapName
	map.each { key, value -> println( key + " = " + value) }
displayMaps("Headers:", msg.getHeaders())
displayMaps("Properties:", msg.getProperties())

The blogs, mentioned above, walk you through the dependency problem with SAP’s standard class “Message” and explains, in detail, how to solve it.

So: “What are we doing here then?”

Vadim and Eng Swee left you with a very cumbersome approach to retrieve the java libraries. I’d like to show you a much more integrated way of doing this without comprising Eng Swee’s goal of teaching a man to fish instead of simply giving him a fish to eat, so he has the skills to feed himself going forward.

What else do you get?

Vadim’s and Eng Swee’s approach involves executing your incomplete groovy project, identifying an error relating to a missing dependency in eclipse, and using that missing class name on an additional groovy script, which you need to deploy on CPI to download the corresponding java library from your tenant. Once you have the java library file you can add it to your class path and re-execute your project to find the next missing class. For this next one you need to alter the groovy script on the iFlow and re-deploy.

Ugh, that is quite a lot of work, because you currently need to download ten java library files to get a complete set and the deployment times of the iFlow can take several minutes. It can easily take 20 minutes to complete them. So that is why I decided to develop an iFlow, which can do all of this in one go. You can download it here.

To achieve what I wanted I needed the ability to pass the java class name dynamically without hard coding it on a groovy script. That way we can circumvent the re-deployment process. So, to achieve that, I use an https connector that allows the forwarding of the HTTP query string parameters. SAP is running the Apache Camel server for that reason. You can read more about this here.

A couple of weeks ago my colleague Dominic Beckbauer gave you an introduction on how to use Postman for testing your iFlows. You are going to need that knowledge now.

As you can see the URL for my iFlow https endpoint requires the following pattern:

/external/trigger?lib=<java class name>

The yellow part is required for the Camel server, the blue part addresses the https endpoint of the iFlow and the red part is the name of the query string variable, which can be accessed on the iFlow later on. Check it out below:

You can access the query string parameters by reading the header “CamelHttpQuery”. I decided to parse it by applying a substring to actually get the value of the parameter (

I feed the result into Vadim’s script logic that utilizes the Java API to lookup the jar file, which contains the class name. The result is then base64 encoded and added as a string attachment to an iFlow message.

For the convenience of the developer I added the jar file name including its version as the title of the attachment.

Finally you need to copy the base64 encoded string and create a jar file from it. I decided to do this by running another groovy script once more. As preparation you need to copy the encoded string into a text file and put it on the libs folder of my demo project. You can also download it from my GitHub repository. The script is called Base64Decoder.groovy. Add the jar file to your project’s build path, try to run the groovy script “Tester” once again and start the process of “hunting” for more missing jar files again.

Once you are done “hunting” for all the SAP jar files and added all of them to your build path, you can finally execute tests with your groovy scripts:

That wasn’t too hard, was it?

Eng Swee Yeoh suggests you take the testing approach even further. If you want to delve further into Test Driven Development with your groovy scripts take a look here.

Before concluding, I would like to emphasize the power of creating message logs (with attachments) with groovy scripts. In our case we used it to output the content of the jar files from the CPI tenant. But, of course, possible use cases for custom logging are only limited by your imagination and implementation needs.

Final Words

So I’ve introduced you to the setup procedure and told you where to find instructions on how to setup your groovy development environment in eclipse to get started with groovy scripting for your CPI iFlows. I also showed you how to download the required java libraries more easily and quicker than my SAP community colleagues Vadim and Eng Swee suggest.

Next time I am going to combine the topics of our blogs, which we posted so far and talk about end-to-end testing of your iFlows. So stay tuned for more.

As always feel free to leave comments and ask lots of follow up questions.



Assigned Tags

      You must be Logged on to comment or reply to a post.
      Author's profile photo Eng Swee Yeoh
      Eng Swee Yeoh

      Hi Martin


      Nice to see your blog expanding on Vadim Klimov and my ideas (but does the title really have to be in all UPPER CASE??)


      It's good to know that you understood our intention of just highlighting the fundamental concepts, and then leaving the rest to the imagination of developers like yourself. IMHO, to appreciate automation, one needs to have experienced how things are done "the hard way" 😉


      A few comments on your approach:-

      1) You can further bypass the Base64 encoding/decoding steps by setting the byte content of the file into the message body. With this you can use the "Send and Download" feature in Postman to directly retrieve the binary contents of the JAR files. If you want the filename, you can additionally store it back in the HTTP header of the response.


      2) While MPL attachments are indeed useful - take note that SAP have repeatedly cautioned against using it unnecessarily - see here and here.



      Eng Swee

      Author's profile photo Martin Pankraz
      Martin Pankraz
      Blog Post Author

      Hi Eng Swee,

      Thanks for your input. I drafted the blog in a different place where the styling of titles need to be "shouting" ;-). When I copied from there I forgot about the styling. So thanks for pointing that out.


      Good point to enhance the base64 encoding step by moving it also into the message. I was simply itching to do some groovy instead.


      Kind regards


      Author's profile photo Vadim Klimov
      Vadim Klimov

      Hello Martin,


      Very nice blog – thank you for sharing it with the community in a detailed way, clear and easy to follow every step you walk through, as well as it presents a good example of technique applied in action and its practical utilization and usability.


      I agree with Eng Swee Yeoh in part that our posts were not intended to provide a ready to run packaged solution, as only imagination and creativity of the developer being put into context of a specific use case / requirement, will be boundary of practical usage of the described techniques – Eng Swee provided reference examples on technique as a core, so that it can be wrapped and bundled into a specific case and tailored to specific needs. This brings us to a variety of options on how to:

      • invoke functionality. For example expose it as an API and call it via Postman or its equivalents, or build user-friendly UI on top of it, or embed libraries retrieval logic into scripts of dependency management system, and
      • digest and process output. For example, save it to local file, or upload to central binaries repository for fellow developers’ usage across the team. Or, as a more advanced option, to build script that will download updated versions of libraries when CPI tenant gets updated – to keep your libraries in sync with those used by CPI tenant’s runtime. Here it is worth referring to work already done by Morten Wittrock and CPI Tracker that he developed and shared in blog series – here, here and here, as it can be used as a core to track CPI updates and use it as a trigger for required actions – such as earlier downloaded libraries’ refresh on client side.

      In certain cases, it was also driven by non-technical factors – such as legal / end user license agreement – cases, where it might have been considered as violation of EULA articles, techniques were described in very high level and agnostic way (note disclaimer that I put at the beginning of several blogs – it was actually put for this specific reason).


      Regarding the script, to protect it from issues caused by incorrect URL constructed by the consumer of the flow, the check against query parameter name might become handy, so that class name retrieved from the expected query parameter. This is to avoid situation if somebody will enter URL with multiple parameters for some reason, where they shouldn’t have done so, and the script will get executed with some multi-parameter query.

      String queryParameterName = 'lib';
      if (httpQuery) {
         def queryParameters = [:];
         def queryParameter;
         httpQuery.split('&').each {
            queryParameter = it.split('=');
            queryParameters.put(queryParameter[0], queryParameter[1]);
         className = queryParameters[queryParameterName];

      The above logic assumes the required query parameter name is ‘lib’, but obviously it can be replaced with any other reasonable query parameter name of your choice (I have chosen ‘lib’ to follow URL you passed in the request and captured in Postman screenshot).





      Author's profile photo Martin Pankraz
      Martin Pankraz
      Blog Post Author

      Hi Vadim,

      Thanks a lot for your thorough comment. You guys made it easy to build upon your great research to bundle something usable. Furthermore your thoughts on the topic really allowed a deep insight of the workings of the groovy related topics and how the cpi messages can be manipulated.

      Your suggestion for enhancing the example script will increase the robustness. So I will give it another look. But there is always something that can be added, isn't there? 😉


      Kind regards


      Author's profile photo Vadim Klimov
      Vadim Klimov

      That's very true, Martin - I agree, there shall be a certain point, when the one makes full stop and releases findings to community, otherwise enhancements, tweaks, introduction of extra features and functionality might go forever, and will finally quite significantly deviate from the original scope, and even distract from the main subject. And all other ideas can be put in new blogs or blog series - so keep on exploring, developing and sharing your thoughts and findings, those are valuable inputs and appreciated efforts!



      Author's profile photo Illya Kuys
      Illya Kuys

      Hi Martin,


      It's a very nice blog – thank you for sharing it with us in such a detailed way to follow each step along the process.

      1/ However, I still encounter some small issues. When I try to retrieve

      URI classFilePath = clazz.protectionDomain.codeSource.location.toURI();

      This causes an error: URI is not hierarchical.

      When I look further into this issue, the jar location is not correctly specified.

      As it should return a path which would look like this: /usr/sap/ljs/plugins/...jar

      It actually returns: jar:bundle://627.0:0/!/

      When I request the location with a script of Vadim Klimov (many thanks as well for the great blog), the following response is returned.

      Fully qualified class name:
      JAR file containing class file: jar:bundle://627.0:0/!/

      Has there been a change of location or has the security changed?


      2/ Is it a possibility to provide a groovy example project with all the jars included?

      The possibility to develop groovy in an editor opens a whole lot of possibilities.


      Kind regards,

      Illya Kuys

      Author's profile photo Martin Pankraz
      Martin Pankraz
      Blog Post Author

      Hi Illya,


      It is indeed possible that the library structure changed. SAP is updating CPI frequently. I would advice to check with Vadim Klimov on this matter, since he investigated with most detail.

      In case this issue is a deal breaker I will publish the last working version I have.

      Kind regards


      Author's profile photo Vadim Klimov
      Vadim Klimov

      Hi Martin, hi Illya,


      I have now tested the script for identification of path to JAR file - and it worked for me when being run in CPI. Although these are internals and not publicly exposed CPI APIs, they rely on Java APIs such as Reflection API, and it is unlikely to be something that can get changed easily, as it is one of ground level APIs in JVM.

      Is it possible for you to provide full code of the script (or part of that script that queries class path) to check if there can be any difference in what has been executed by you and me?

      I used the following one:

      String className = "<fully qualified class name>";
      Class clazz = Class.forName(className);
      URI classFilePath = clazz.protectionDomain.codeSource.location.toURI();


      Regarding a project to run scripts locally. Eng Swee Yeoh has written a blog about this where he described steps that can be followed to get it set. It will require at least dependency resolved for external library with class Message, but you might need additional dependencies to be resolved in case some other functionality will be used in the script. Although it is possible to use older versions of that library, I would suggest regularly downloading current versions of required libraries from CPI and refreshing project dependencies for them to keep them up to date and aligned with those used at runtime by CPI tenant.




      Author's profile photo Martin Pankraz
      Martin Pankraz
      Blog Post Author

      Thanks a lot Vadim Klimov for the comment!


      Kind regards



      Author's profile photo Tomasz Mosko
      Tomasz Mosko

      /http/amba/cpifilesystemdownload///usr/sap/ljs//webapps/ROOT.war works for me in year 2021

      It contains:


      Author's profile photo Naveen Kumar Mallenahalli Parameshwarappa
      Naveen Kumar Mallenahalli Parameshwarappa

      Hi Vadim Klimov, Hi  Martin Pankraz ,

      I am also facing similar issue mentioned by Illya Kuys

      I am using below code(copied from Vadim post)

      def Message processData(Message message) {
      String className = "";
      StringBuilder builder = new StringBuilder();
      Class clazz = Class.forName(className);
      URI classFilePath = clazz.protectionDomain.codeSource.location.toURI();
      File classFile = new File(classFilePath);
      byte[] classFileContent = classFile.bytes;
      builder << classFileContent.encodeBase64().toString();
      def messageLog = messageLogFactory.getMessageLog(message);
      messageLog.addAttachmentAsString("JAR file content: Base64 encoding", builder.toString(), "text/plain");
      return message;


      Error :

      javax.script.ScriptException: java.lang.Exception: java.lang.IllegalArgumentException: URI is not hierarchical@ line 46 in script2.groovy, cause: java.lang.IllegalArgumentException: URI is not hierarchical


      I get class path as "JAR file containing class file: jar:bundle://518.0:0/!/ "  when i execute the code for accessing  classpath.

      I suspect, due to restricted authorization i am not able to access the class path .

      Could you please have a look.

      Thanks in advance.

      Best begards,


      Author's profile photo Vadim Klimov
      Vadim Klimov

      Hello Naveen,


      This observed behavior is a consequence of the way how an OSGi container that is now used by CPI – Apache Karaf – stores data of deployed bundles. In contrast to an earlier directory layout where all JAR files were located in a single directory, Karaf caches deployed bundles in their own sub-directories. For example, an output that you got – jar:bundle://518.0:0/!/ – suggests that the required file can be found in a version 0.0 of a bundle with ID 518. With this in mind, as well as considering standard structure of Karaf directories, we can modify the script so that it retrieves corresponding bundle file:


      Message processData(Message message) {
      	String className = ''
      	StringBuilder builder = new StringBuilder()
      	Class clazz = Class.forName(className)
      	URI classFilePath = clazz.protectionDomain.codeSource.location.toURI()
      	String regex = '\\/\\/(.*?)\\/!\\/'
      	def matcher = (classFilePath =~ regex)
          String bundleIdVersion = matcher[0].getAt(1)
          String bundleId = bundleIdVersion.take(bundleIdVersion.indexOf('.'))
          String bundleVersion = bundleIdVersion.drop(bundleId.length() + 1)
          String cacheBundleFilePath = "/usr/sap/ljs/data/cache/bundle${bundleId}/version${bundleVersion.replace(':', '.')}/bundle.jar"
      	File classFile = new File(cacheBundleFilePath)
      	byte[] classFileContent = classFile.bytes
      	builder << classFileContent.encodeBase64().toString()
      	def messageLog = messageLogFactory.getMessageLog(message)
      	messageLog.addAttachmentAsString('JAR file content: Base64 encoding', builder.toString(), 'text/plain')
      	return message


      Note an enhanced middle part of the script code snippet: after we get location of the source of the given class, we decompose it into bundle ID and version, and make use of them when constructing a full path to the required bundle file.




      Author's profile photo Md Quddus
      Md Quddus

      Hello Vadim,

      I tried with the modify script shared above and I am facing "FileNotFoundException". Can you please help.



      Author's profile photo Vadim Klimov
      Vadim Klimov

      Hello Quddus,

      Since a location of deployed bundles changed - and to allow further dynamic determination of their current location, after a bundle ID (bundleId) has been identified in the code snippet above, you can use OSGi Framework API to find a location of that bundle. Example is below:

      BundleContext context = FrameworkUtil.getBundle(Message).bundleContext
      String location = context.getBundle(bundleId as long).location

      (also ensure that you add org.osgi.framework.BundleContext and org.osgi.framework.FrameworkUtil to imports section of a script)

      After the location of a bundle has been identified, you shall be able to access file content by its location.




      Author's profile photo Mikel Maeso Zelaya
      Mikel Maeso Zelaya

      Hi Vadim,

      Where can we add this piece of code in the previous script (September 2019)?

      Would you mind updating that one please?


      Being able to test the scripts in eclipse would be great, but I have been fighting with this for a while now (downloading the required jars) and I'm feeling I won't be able to solve it by myself.

      Thanks in advance!


      Author's profile photo Mikel Maeso Zelaya
      Mikel Maeso Zelaya

      This works for downloading a war that contains most of the "problematic" classes, so devs can prepare the Groovy Scripting Testing Environment locally.

      Vadim's script was beauuuuuutiful, and I have messed it up with some workarounds, just to get it working again.

      It wont work for all missing classes, but it does work, as stated before, for the "problematic ones". For the other ones, Google will do.

      I have also added some comments to the code and two new features:

      • Response Type (File), so use the "Send and Download" button in Postman to get the file (takes a bit to get executed).
      • Custom HTTP Header in Response "JarName" with the name of the Jar containing the incoming class (QP)
      import java.util.HashMap;
      import org.osgi.framework.BundleContext 
      import org.osgi.framework.FrameworkUtil
      def Message processData(Message message) {
          //Get the className from the incoming QP
          String httpQuery = message.getHeaders()["CamelHttpQuery"]
          int index = httpQuery.indexOf("=")
          String className = httpQuery.substring(index + 1) // Example: "";
      	Class clazz = Class.forName(className)
      	URI classFilePath = clazz.protectionDomain.codeSource.location.toURI()
      	String regex = '\\/\\/(.*?)\\/!\\/'
      	def matcher = (classFilePath =~ regex)
          String bundleIdVersion = matcher[0].getAt(1)
          String bundleId = bundleIdVersion.take(bundleIdVersion.indexOf('.'))
      	//Get jar location in server
      	BundleContext context = FrameworkUtil.getBundle(clazz).bundleContext
          String location = context.getBundle(bundleId as long).location//This returns "jar:file:/home/vcap/app/webapps/stack.worker_cf.karaf-6.20.11.war!"
      	//Following code is to get the war path cleaned (contains the SAP Jars needed)
      	location = location.replace("jar:file:","")
          String[] war_jar = location.split('!')
      	//Read war 
      	File classFile = new File(war_jar[0])
      	//Add jar to response for postman "Send and download"
          message.setHeader("Content-Type", "application/java-archive");
          //Also add the jar name that contains the incoming class name, in order to take only that one from the war
          message.setHeader("JarName", war_jar[1]);
      	return message


      GET --> https://xxxxxx/http/external/trigger?



      Author's profile photo Ravikanth Tunuguntla
      Ravikanth Tunuguntla

      Hi Martin, Vadim,

      I was trying to set up the Groovy project to test my scripts. I added the required JARs as mentioned. But I got the following error when executing the tester.groovy script. I am not able to find the class org.osgi.framework.FrameworkUtil, seems like I am missing something. Can you please advise how I can fix the issue? 

      Thanks Ravikanth




      I did add library org.osgi.core-4.2.0.jar (found through google) and fixed the error. I could successfully run the script now. But I still feel this step is not needed and something amiss in my set up. Please let me know if I have to set up the groovy environment differently. Thanks

      Author's profile photo Martin Pankraz
      Martin Pankraz

      Hi Ravikanth,

      I am on a different S-User now, so I don't messages here anymore. I am trying to replicate this on my CloudFoundry environment just now. So I think given the time that has passed there are probably some changes on the SAP side. You should be fine with your current setup. Especially if you can execute Message locally.



      Author's profile photo Ravikanth Tunuguntla
      Ravikanth Tunuguntla

      Thanks Martin