Develop your pfunctions with Maven

Traditionally, SAP Solution Sales Configuration developers implement their pfunctions directly in the Solution Modeling Environment (SME). While this setup is quick and efficient from a developer perspective, it is the opposite from a DevOps perspective, although the product team makes constant efforts to improve the situation. Unlike SAP Commerce, Solution Model Environment does not allow to easily automate tasks required for continuous integration and continuous delivery, preventing an harmonization of the development and operation procedures. I observed long term how this affected negatively the quality and velocity of my project and decided to look for a solution.

Why Maven?

I listed first the most important requirements the solution should fulfill:

  • It should integrate seamlessly with Solution Modeling Environment and should not affect noticeably the productivity of pfunctions developers. In particular, it should be easy to ramp-up and find help, if needed.
  • It should possibly integrate with IDEs used by SAP Commerce developers like Eclipse or IntelliJ IDEA, who might need to implement pricing user exits (pricing user exits are like pfunctions but instead of being called by the configuration runtime engine, it is called by the pricing runtime engine)
  • It should support automation for development and operation tasks like code compilation, code scans, execution of unit tests and more
  • It should help to manage dependencies as pfunctions might have dependencies on external libraries like the SAP Commerce platform
  • It should be able to produce different assemblies since the pfunctions assemblies for SAP Commerce and Solution Modeling Environment are different. The former is a simple JAR while the latter is an Eclipse plug-in.
  • It should work easily with common CI/CD solutions like Jenkins or Bamboo

If you are familiar with Maven, you will understand why it was an obvious choice. You might also find that other tools or frameworks could have been good choices as well. I’m very familiar with Maven and that’s the main reason why I picked it over other solutions.

If you are not familiar with Maven, you can find here a 5 minutes introduction. You can also find tons of tutorials and documentation on the web. In a nutshell, Maven is one, if not the most, popular build framework in the Java world. It integrates very well with Eclipse and therefore Solution Modeling Environment as well as other IDEs. It offers a lot of plug-ins allowing to build a lot of assemblies, including Eclipse plug-ins and is very easy to configure in all major CI/CD solutions.

How to configure it?

I assume you have Maven installed locally and you understand its basics. I setup my Maven pfunctions project with the following file system structure:

pfunctions |--- pom.xml |--- src | |--- pom.xml | |--- src | |--- main | |--- java | |--- ... pfunctions source code |--- assembly |--- pom.xml |--- commerce | |--- pom.xml | |--- main | |--- assembly | |--- all-pfunctions-jar.xml |--- sme |--- pom.xml

The project is divided into 4 subprojects. The src subproject compiles the pfunctions code, while the assembly subproject generates the pfunctions assemblies. The sme and commerce subprojects under the assembly subproject are creating the assemblies respectively for the Solution Modeling Environment and SAP Commerce.

The pfunctions/pom.xml file contains the following content:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>mygroup</groupId> <artifactId>pfunctions-parent</artifactId> <version>1.0.0</version> <packaging>pom</packaging> <name>My pFunctions</name> <organization> <name>My Organization</name> </organization> <properties> <java.version>1.8</java.version> <ssc.release>3.2</ssc.release> <ssc.version>${ssc.release}.21</ssc.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.build.timestamp.format>yyyyMMddHHmmssSSS</maven.build.timestamp.format> <build.version>${project.version}.${maven.build.timestamp}</build.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>com.sap.custdev.projects.fbs.slc</groupId> <artifactId>ssc</artifactId> <version>${ssc.version}</version> <scope>provided</scope> </dependency> </dependencies> </dependencyManagement> <modules> <module>src</module> <module>assembly</module> </modules>
</project>

It declares all dependencies required in the project and can be used as base by all subprojects.

The pfunctions/src/pom.xml file contains the following content:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>mygroup</groupId> <artifactId>pfunctions-parent</artifactId> <version>1.0.0</version> <relativePath>..</relativePath> </parent> <artifactId>pfunctions</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <name>My pFunctions Sources</name> <dependencies> <dependency> <groupId>com.sap.custdev.projects.fbs.slc</groupId> <artifactId>ssc</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> <configuration> <encoding>${project.build.sourceEncoding}</encoding> <source>${java.version}</source> <target>${java.version}</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.1</version> <configuration> <archive> <addMavenDescriptor>false</addMavenDescriptor> <manifest> <addDefaultImplementationEntries>true</addDefaultImplementationEntries> <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries> </manifest> <manifestEntries> <Implementation-Version>${build.version}</Implementation-Version> </manifestEntries> </archive> </configuration> </plugin> </plugins> </build>
</project>

It is the standard configuration for a Java library. Note the dependency to Solution Sales Configuration, that need to be setup later since this library does not exist in any public Maven repositories.

The pfunctions/assembly/pom.xml file contains the following content:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>mygroup</groupId> <artifactId>pfunctions-parent</artifactId> <version>1.0.0</version> <relativePath>..</relativePath> </parent> <artifactId>pfunctions-assembly</artifactId> <packaging>pom</packaging> <name>My pFunctions Assembly POM</name> <description>Assemble the pFunctions into different packages to be used in different environments</description> <dependencyManagement> <dependencies> <dependency> <groupId>${project.groupId}</groupId> <artifactId>pfunctions</artifactId> <version>${project.version}</version> </dependency> </dependencies> </dependencyManagement> <build> <pluginManagement> <plugins> <plugin> <artifactId>maven-eclipse-plugin</artifactId> <version>3.1.0</version> </plugin> </plugins> </pluginManagement> </build> <modules> <module>commerce</module> <module>sme</module> </modules>
</project>

It extends the base configuration with dependencies required specifically for the assemblies and can be used as base for all assembly subprojects. It declares a dependency on the subproject compiling and packaging the pfunctions sources, so that the produced JAR can be used in the assembly process.

The pfunctions/assembly/commerce/pom.xml file contains the following content:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>mygroup</groupId> <artifactId>pfunctions-assembly</artifactId> <version>1.0.0</version> <relativePath>..</relativePath> </parent> <artifactId>pfunctions-assembly-commerce</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <name>My pFunctions Assembly for SAP Commerce</name> <dependencies> <dependency> <groupId>${project.groupId}</groupId> <artifactId>pfunctions</artifactId> </dependency> </dependencies> <build> <finalName>mypfunctions</finalName> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <appendAssemblyId>false</appendAssemblyId> <descriptors> <descriptor>src/main/assembly/all-pfunctions-jars.xml</descriptor> </descriptors> <archive> <addMavenDescriptor>false</addMavenDescriptor> <manifest> <addDefaultImplementationEntries>true</addDefaultImplementationEntries> <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries> </manifest> <manifestEntries> <Implementation-Version>${build.version}</Implementation-Version> </manifestEntries> </archive> </configuration> <executions> <execution> <id>assemble-pfunctions-jar</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
</project>

It configures the build to use a specific assembly plugin, which is configured in the external file all-pfunctions-jars.xml. Here is the content of this file.

<?xml version="1.0" encoding="UTF-8"?>
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.0.0 http://maven.apache.org/xsd/assembly-2.0.0.xsd"> <id>all-pfunctions-jars</id> <formats> <format>jar</format> </formats> <includeBaseDirectory>false</includeBaseDirectory> <dependencySets> <dependencySet> <unpack>true</unpack> <unpackOptions> <excludes> <exclude>META-INF/**</exclude> </excludes> </unpackOptions> <useTransitiveDependencies>true</useTransitiveDependencies> </dependencySet> </dependencySets>
</assembly>

In a nutshell, the plugin will take the JAR produced from the src subproject and will repackage it, in order to include all non-provided dependencies. If you are wondering what is a non-provided dependency, it is a dependency, which is not available without explicit action from your side in the runtime environment, where your pfunctions will be executed. For example, if you decide to log messages with Log4J2, you will add a dependency to the library in the pom.xml of the src subproject. The library is actually part of SAP Commerce and therefore provided when pfunctions are executed within SAP Commerce. Now, if you decide to use resilience4j library, it is not provided by the SAP Commerce platform but will needed to execute properly your pfunctions. This dependency is not provided and the assembly plugin will make sure to retrieve the resilence4j library and include it as well as its dependencies into the assembly with the compiled pfunctions code. For more details, I invite you to read about dependency scope in the Maven project documentation.

The pfunctions/assembly/sme/pom.xml file contains the following content:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>mygroup</groupId> <artifactId>pfunctions-assembly</artifactId> <version>1.0.0</version> <relativePath>..</relativePath> </parent> <artifactId>pfunctions-assembly-sme</artifactId> <version>1.0.0</version> <packaging>bundle</packaging> <name>My pFunctions Assembly for Eclipse/SME</name> <dependencies> <dependency> <groupId>${project.groupId}</groupId> <artifactId>pfunctions-assembly-commerce</artifactId> <version>${project.version}</version> <scope>compile</scope> </dependency> </dependencies> <build> <finalName>mypfunctions_${project.version}</finalName> <plugins> <plugin> <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> <version>2.5.0</version> <extensions>true</extensions> <configuration> <archive> <addMavenDescriptor>false</addMavenDescriptor> <manifest> <addDefaultImplementationEntries>true</addDefaultImplementationEntries> <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries> </manifest> <manifestEntries> <Implementation-Version>${build.version}</Implementation-Version> </manifestEntries> </archive> <manifestLocation>${project.build.outputDirectory}/META-INF</manifestLocation> <instructions> <Bundle-Name>My pFunctions</Bundle-Name> <Bundle-SymbolicName>mypfunctions;singleton=true</Bundle-SymbolicName> <Bundle-Vendor>My Organization</Bundle-Vendor> <Bundle-Version>${project.version}</Bundle-Version> <Bundle-RequiredExecutionEnvironment>JavaSE-1.6</Bundle-RequiredExecutionEnvironment> <Bundle-ActivationPolicy>lazy</Bundle-ActivationPolicy> <Export-Package>com.sap.sce.user,com.sap.sce.user.helpers,com.sap.sce.engine.ddb,com.sap.front.base</Export-Package> <Require-Bundle>com.sap.custdev.projects.fbs.slc.ejb-ipc;bundle-version="${ssc.release}";resolution:=optional</Require-Bundle> <Eclipse-RegisterBuddy>com.sap.custdev.projects.fbs.slc.ejb-ipc</Eclipse-RegisterBuddy> <Embed-Dependency>*</Embed-Dependency> <Embed-Transitive>false</Embed-Transitive> <Embed-StripGroup>true</Embed-StripGroup> <Embed-StripVersion>false</Embed-StripVersion> </instructions> </configuration> </plugin> </plugins> </build>
</project>

It configures the build to take the assembly for SAP Commerce and package it in an Eclipse plugin. The Eclipse plugin descriptor is configured via the <instructions> element.

Last but not least, the Solution Sales Configuration library needs to be installed in a Maven repository, so that Maven can resolve the dependencies. If your organization has a private central Maven repository and your environment is already configured to use it, I would recommend to download Solution Sales Configuration from SAP Software Center and add it to your organization private central repository. If your organization does not have a central Maven repository, you can always use your local Maven repository and run the following command to install Solution Sales Configuration library after specifying the path to the library and its version.

mvn install:install-file \ -Dfile=<SSC JAR file path> \ -DgroupId=com.sap.custdev.projects.fbs.slc \ -DartifactId=ssc \ -Dpackaging=jar \ -Dversion=<SSC JAR version (e.g. 3.2.21)>

How to use it?

From your command line, execute mvn clean install. It will build the pfunctions code, create the assemblies and add these assemblies to your local Maven repository. If you want to grab the assemblies, go into the target folder of each assembly subproject to find the JAR.

If you want to build your project against a different version of Solution Sales Configuration without changing the POM, you can execute mvn -Dssc.version=<your SSC version> clean install. Ensure this version of Solution Sales Configuration is either available in your organization private Maven repository or in your local Maven repository.

What is next?

I only shared the most basic setup of a pfunctions Maven project and you will most likely want more than just compiling and building assemblies. You might want to execute unit tests using Mockito library and measure code coverage of your unit tests. You might also want to perform static code checks to ensure your code doesn’t contain any obvious bugs. Finally, you might also want to check your code does not contain security leaks and verify your dependencies are safe as well.

The good news is that Maven offers plugins to support all these scenarios and a lot of documentation is available on the web to explain step-by-step how to configure them for your project. As example, let us add support for unit test, using JUnit 5.

First, let us add the dependency to the JUnit 5 library by adding the following content to the pfunctions/pom.xml file:

<?xml version=""?>
<project ...> [...] <dependencyManagement> <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.7.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.5.13</version> <scope>test</scope> </dependency> <!-- needed by Solution Sales Configuration library to execute unit tests --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>LATEST</version> <scope>test</scope> </dependency> [...] </dependencies> </dependencyManagement> [...]
</project>

Secondly, let us extend the file structure of the src subproject to add the code for JUnit tests.

pfunctions |--- src |--- src |--- main |--- test |--- java |--- ... pfunctions unit test code 

Finally, let us configure the build of the src subproject to run the JUnit tests by adding the following content to the pfunctions/src/pom.xml file.

<?xml version="1.0"?>
<project ...> [...] <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </dependency> </dependencies> [...] <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.0.0-M3</version> <configuration> <includes> <include>**/*Test.java</include> <include>**/Test*.java</include> <include>**/*Tests.java</include> <include>**/*TestCase.java</include> <include>**/*_TEST.java</include> <include>**/TEST_*.java</include> </includes> <classpathDependencyExcludes> <classpathDependencyExclude>com.sap.custdev.projects.fbs.slc:ejb-ipc</classpathDependencyExclude> </classpathDependencyExcludes> </configuration> </plugin> </plugins> </build> [...]
</project>

Now, when you will run Maven, it will compile the pfunctions code and then run the JUnit tests. If a unit test fails, the build will stop and fail. You can detect early regressions and prevent the deployment of faulty pfunctions in your SAP Commerce environment.

Last but not least, once your Maven project is setup and performs all needed tasks, streamline  the delivery of your pfunctions by moving your project to the CI/CD solution of your choice. You will be able to automate the complete delivery process as well as the deployment of your pfunctions in SAP Commerce.

Conclusion

Moving your pfunctions project from Solution Modeling Environment to Maven is not easy. I hope however that this blog post is clear and detailed enough to help you migrating quickly your project. The migration is a steep step but all next ones like unit testing, measuring code coverage, performing static code checks, managing code quality metrics through SonarQube are easy and very well documented and supported by the Maven community. You will be able to configure in no time a very solid development process and control the quality of your deliverable. Finally, once you will have automated the delivery process of your pfunctions, you will see the time and efficiency gains, so that you will never regret the move.