Your first SAP CCO plugin: local setup and Hello World

Developers who are new to SAP Customer Checkout usually don't struggle with the plugin API. They struggle with the first hour. Where does the SDK come from? Which Java version? Why does the POS ignore my jar? This post walks through that first hour with the smallest plugin that still shows something: a message box on the POS screen that says Hello World.

SAP Customer Checkout, cloud edition: a message box "Hello World from my first CCO plugin!" over the login screen

I cover both editions that are current today:

The Java code is identical for both. Three values in the pom differ. Whether you need separate jars for the two editions at all is covered near the end.

The approach: start with your local Maven repository

Every CCO plugin compiles against ENV.jar, the core library of the POS. SAP does not publish it to a Maven repository. It ships inside the installer, together with a matching POM file. For a single developer, the cleanest way to use it is to install ENV.jar into the local Maven repository on your machine (~/.m2/repository), once per CCO version. The pom then declares it like any other dependency.

This takes a few minutes, and it pays off later. When a second developer joins or you set up a build server, you upload the same file with the same coordinates to a central Maven repository. The pom stays as it is. The last section of this post shows that step, and Treating SAP CCO plugins as real software describes the CI/CD setup around it.

I would avoid the two shortcuts I often find in plugin repositories. One is committing ENV.jar (135 MB for FP21) into Git. The other is a systemPath dependency that points to a folder on one developer's laptop. Both work until the second developer or the first build server shows up.

What you need

The Hello World plugin itself never talks to the backend.

Step 1: Download the installer

Log in to the SAP Software Center and download the latest patch of your edition. You get a ZIP that contains the Windows installer:

Edition Download (example) Installer inside
FP21 SAPCUSCHK21_27-70001338.ZIP (FP21 PL27) SapCustomerCheckout_2_0.exe
Cloud edition SAPCheckoutCE602_2-80008961.ZIP (FP2602 PL02) SapCustomerCheckout_3_0.exe

For the cloud edition, search for SAP Customer Checkout point-of-sale, cloud edition, select SAP Customer Checkout POS CLOUD 3.0 and pick the SP2602 package.

Step 2: Install and start the POS

On Windows, run the installer and keep the default folder C:\SapCustomerCheckout. Start CCO from the desktop shortcut and complete the initial configuration with the connection data from above. The SAP Installation and Update Guide describes each field (chapter 4.1 for FP21, chapter 4.2 for the cloud edition).

On macOS or Linux you can extract the installer with 7-Zip and run CCO with a shell script. That is not officially supported, but it works well for development. I describe it in SAP CCO Cloud Edition: Run on Mac.

Log in once to confirm the POS works, then close it again.

Step 3: Find ENV.jar and the version

The installation folder contains everything you need:

C:\SapCustomerCheckout\
  ENV.jar                         the CCO API you compile against
  version.txt                     the exact version of this installation
  tools\env-pom.xml               SAP's Maven POM for ENV.jar
  tools\ENV-2.21.34-javadoc.jar   the API documentation
  cco\envLib\                     libraries the POS ships with (Spring, Jackson, slf4j, ...)
  cco\POSPlugins\AP\              your plugin goes here
  log\                            log files
  run.bat

You can also get these files without installing: 7z x SapCustomerCheckout_2_0.exe (or SapCustomerCheckout_3_0.exe) extracts them into a CustomerCheckout folder.

Open version.txt. For FP21 PL27 it reads:

version=2.21.34
build.date=221.2608051634
customer.version=2.0 FP21 PL27

And for the cloud edition FP2602 PL02:

version=3.2602.4
build.date=2602.2608110253
customer.version=3.0 FP2602 PL02

You need two values from this file:

Step 4: Install ENV.jar into your local Maven repository

Open a terminal in the installation folder. For FP21 you run three commands:

cd C:\SapCustomerCheckout
mvn install:install-file "-Dfile=ENV.jar" "-DpomFile=tools\env-pom.xml" "-Djavadoc=tools\ENV-2.21.34-javadoc.jar"
mvn install:install-file "-Dfile=cco\envLib\Likey-1.0.1.jar" "-DgroupId=com.sap.security.core.server" "-DartifactId=Likey" "-Dversion=1.0.1" "-Dpackaging=jar" "-DgeneratePom=true"
mvn install:install-file "-Dfile=cco\envLib\com.sap.js.passport.api-1.2.0.jar" "-DgroupId=com.sap.core.jdsr" "-DartifactId=com.sap.js.passport.api" "-Dversion=1.2.0" "-Dpackaging=jar" "-DgeneratePom=true"

For the cloud edition, the first command changes:

cd C:\SapCustomerCheckout
mvn install:install-file "-Dfile=ENV.jar" "-DpomFile=tools\env-pom.xml" "-Djavadoc=tools\ENV-3.2602.4-javadoc.jar"

The other two commands are identical, because FP2602 ships the same versions of both libraries. You need them once per machine.

Keep the quotes. PowerShell splits unquoted -D arguments at the first dot, so -Dfile=ENV.jar would arrive at Maven broken. The quoted form works in cmd, PowerShell and bash.

What these commands do:

For FP21, Maven copies ENV.jar to ~/.m2/repository/com/sap/customercheckout/ENV/2.21.34/ (on Windows %USERPROFILE%\.m2\repository\...). Repeat the first command for every CCO version you want to build against.

Step 5: Create the plugin project

The project has three files:

hello-world-plugin/
  pom.xml
  src/main/java/com/example/cco/hello/HelloWorldPlugin.java
  src/main/resources/hello-world.js

Here is the pom.xml:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example.cco</groupId>
    <artifactId>hello-world-plugin</artifactId>
    <version>1.0.0</version>
    <name>Hello World Plugin</name>

    <properties>
        <maven.compiler.release>17</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <profiles>
        <!-- On-premise: SAP Customer Checkout 2.0 FP21 (default) -->
        <profile>
            <id>fp21</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <cco.env.groupId>com.sap.customercheckout</cco.env.groupId>
                <cco.env.version>2.21.34</cco.env.version>
                <cco.cashDeskVersions>2.0 FP21</cco.cashDeskVersions>
            </properties>
        </profile>
        <!-- SAP Customer Checkout, cloud edition 3.0 FP2602: mvn package -Pcloud -->
        <profile>
            <id>cloud</id>
            <properties>
                <cco.env.groupId>com.sap.customercheckout.pos</cco.env.groupId>
                <cco.env.version>3.2602.4</cco.env.version>
                <cco.cashDeskVersions>3.0 FP2602</cco.cashDeskVersions>
            </properties>
        </profile>
    </profiles>

    <dependencies>
        <!-- The CCO API plus all libraries of cco/envLib (via SAP's POM).
             "provided": the POS already has them, they must not end up in our jar. -->
        <dependency>
            <groupId>${cco.env.groupId}</groupId>
            <artifactId>ENV</artifactId>
            <version>${cco.env.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.15.0</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <archive>
                        <manifest>
                            <!-- writes Implementation-Version = ${project.version} -->
                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                        </manifest>
                        <manifestEntries>
                            <pluginName>${project.name}</pluginName>
                            <cashdeskPOSPlugin>com.example.cco.hello.HelloWorldPlugin</cashdeskPOSPlugin>
                            <cashDeskVersions>${cco.cashDeskVersions}</cashDeskVersions>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

What matters in this pom:

Two profiles. Without options, Maven builds for FP21. -Pcloud builds for the cloud edition. If you only need one edition, delete the other profile and move its three properties up into <properties>.

One dependency, scope provided. Through SAP's POM, ENV brings all libraries of cco\envLib along, and provided applies to them as well. The POS already has all of them, so your jar contains only your own classes. The finished Hello World jar is 4 KB. The slf4j logger in the plugin class comes from this classpath too.

Three manifest entries. CCO does not scan jars for classes. It reads the manifest of every jar in the plugin folder:

addDefaultImplementationEntries also writes the pom version into the manifest as Implementation-Version. The plugin class reads it from there, so the version in the POS always matches the pom.

The compiler plugin has a fixed version. If you leave it out, the version of maven-compiler-plugin depends on your Maven installation. Maven 3.8 picks a version that ignores maven.compiler.release without a warning, and the build then fails with "Source option 5 is no longer supported". With a fixed version the build behaves the same on every machine.

Step 6: Write the plugin class and the JavaScript

The plugin consists of a Java class that runs inside the POS and a small JavaScript file for the user interface. This is the same approach as the NGUIHelloWorld example in SAP's example plug-ins.

The Java class:

package com.example.cco.hello;

import com.sap.scco.ap.plugin.BasePlugin;
import com.sap.scco.ap.plugin.annotation.ui.JSInject;
import java.io.InputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorldPlugin extends BasePlugin {

    private static final Logger LOG = LoggerFactory.getLogger(HelloWorldPlugin.class);

    @Override
    public String getId() {
        return "HelloWorldPlugin";
    }

    @Override
    public String getName() {
        return "Hello World Plugin";
    }

    @Override
    public String getVersion() {
        // Implementation-Version from the jar manifest, i.e. the version in the pom
        String version = getClass().getPackage().getImplementationVersion();
        return version != null ? version : "dev";
    }

    @Override
    public void startup() {
        LOG.info("Hello World plugin started, version {}", getVersion());
    }

    // Hands our JavaScript to the POS user interface (NGUI).
    @JSInject(targetScreen = "NGUI")
    public InputStream injectJs() {
        return getClass().getResourceAsStream("/hello-world.js");
    }
}

Every CCO plugin extends BasePlugin and implements three methods. getId() returns a unique technical ID, getName() and getVersion() describe the plugin. CCO calls startup() once after it has loaded the plugin. Here it only writes a log line, which is the first thing to look for when a plugin does not behave.

The user interface of the POS, which SAP calls NGUI, runs in the browser. At startup CCO looks for methods annotated with @JSInject and delivers what they return to the browser together with the user interface. targetScreen = "NGUI" means the sales user interface. The method reads hello-world.js from the jar, and Maven puts it there because the file sits in src/main/resources.

The JavaScript file:

Plugin.helloWorld = class HelloWorld {

    constructor(pluginService, eventBus) {
        // show a message box five seconds after the POS user interface has loaded
        setTimeout(() => {
            eventBus.push('SHOW_MESSAGE_BOX', 'Hello World from my first CCO plugin!');
        }, 5000);
    }
};

The file registers a class under Plugin.helloWorld. When the user interface loads, CCO creates an instance of every class registered this way and passes services into the constructor. It picks them by parameter name, so keep the names pluginService and eventBus exactly as they are. The event bus connects the parts of the user interface. SHOW_MESSAGE_BOX is one of its standard events and opens a message box with the text you pass. The five-second delay comes from SAP's example.

Two rules for plugin JavaScript are worth learning right away:

Step 7: Build

mvn clean package            # FP21
mvn clean package -Pcloud    # cloud edition

Both commands produce target/hello-world-plugin.jar.

Step 8: Deploy and test

  1. Close the POS.
  2. Copy target\hello-world-plugin.jar to C:\SapCustomerCheckout\cco\POSPlugins\AP\. The folder exists after the first start of CCO. It has to be the AP subfolder. CCO ignores jars that sit directly in POSPlugins.
  3. Start the POS.

About five seconds after the POS has started, the message box from the screenshot at the top of this post appears over the login screen. You don't need to log in for it. It looks the same on FP21 and on the cloud edition, and it comes back every time the POS user interface loads.

The log file C:\SapCustomerCheckout\log\cco_standard.log shows what CCO did with your jar (timestamps removed):

PluginManager - Adding jar file:.../cco/temp/.POSPlugins/AP0/hello-world-plugin.jar
PluginManager - Found classes "com.example.cco.hello.HelloWorldPlugin" in manifest of jar "..."
PluginManager - Found plugin class class com.example.cco.hello.HelloWorldPlugin
PluginManager - Initializing plugin: com.example.cco.hello.HelloWorldPlugin
HelloWorldPlugin - Hello World plugin started, version 1.0.0

Don't be confused by the path. At startup CCO copies the AP folder to cco\temp\.POSPlugins\AP0 and loads the plugins from that copy.

You will repeat these three steps many times a day, so put them into a small script that copies the jar and restarts the POS.

When nothing happens

Search cco_standard.log for the name of your jar. CCO logs plugin problems at INFO level, so cco_error.log usually shows nothing.

Keep exactly one version of your jar in the AP folder. CCO loads every jar it finds there. If an old hello-world-plugin-0.9.jar is still lying around, you can no longer tell which code is running.

One jar for both editions?

The two profiles produce two jars, but you don't necessarily need both. The compiled class of the Hello World plugin is byte for byte the same, whether Maven builds it against FP21 or against FP2602. Only the manifest differs. So you can build a single jar that declares both versions:

mvn clean package "-Dcco.cashDeskVersions=2.0 FP21, 3.0 FP2602"

If you want this permanently, put the value into the fp21 profile instead. I ran exactly this jar on FP21 PL27 and on FP2602 PL02, and it worked on both.

This only holds as long as your plugin uses API that exists in both editions with the same signatures. The editions do differ. The cloud edition has features that FP21 lacks, such as the Self-Checkout UI, and classes and methods change between feature packs. If your plugin calls something that is missing on one side, the jar still passes the version check there. It then fails with a NoSuchMethodError or NoClassDefFoundError, either while CCO loads the plugin or only when that code path runs. So a shared jar is fine for small plugins, but test it on both editions before you ship it. As soon as the code has to differ, build two jars.

With the cloud edition, keep one more thing in mind. Every feature pack has its own version string, such as 3.0 FP2503 or 3.0 FP2602. After an upgrade to the next feature pack, CCO skips your plugin until you add the new version to cashDeskVersions and deliver a new jar. On FP21 this does not happen, because patches keep the version 2.0 FP21.

Later: moving to a central Maven repository

The local repository works well as long as you are the only developer. Once a colleague or a CI pipeline needs to build the plugin, upload the three artifacts from step 4 once to a shared repository. Nexus, Artifactory or the package registry of GitLab or GitHub all work. ENV.jar goes up with SAP's POM, exactly as before:

cd C:\SapCustomerCheckout
mvn deploy:deploy-file "-Dfile=ENV.jar" "-DpomFile=tools\env-pom.xml" "-Djavadoc=tools\ENV-2.21.34-javadoc.jar" "-Durl=https://repo.example.com/repository/cco" "-DrepositoryId=company"

-DrepositoryId refers to a <server> entry with the same ID in your settings.xml, which holds the credentials for the upload.

For the two SAP libraries, take the jar and the POM that Maven generated in step 4 from your local repository:

cd C:\Users\you\.m2\repository\com\sap\security\core\server\Likey\1.0.1
mvn deploy:deploy-file "-Dfile=Likey-1.0.1.jar" "-DpomFile=Likey-1.0.1.pom" "-Durl=https://repo.example.com/repository/cco" "-DrepositoryId=company"

The same goes for com\sap\core\jdsr\com.sap.js.passport.api\1.2.0. Don't take the shortcut with -DgeneratePom=true here. deploy:deploy-file ignores that flag and uploads SAP's internal POM from inside the jar. The internal POM of the passport library points to a parent that only exists at SAP, and everyone who builds against your repository gets the error from step 4.

Every developer and the CI runner then add the repository and their credentials to their Maven settings.xml. The pom.xml of the plugin does not change at all. I describe the CI/CD side with reproducible builds, releases from tags and security scans in Treating SAP CCO plugins as real software.

What's next?

From here the real work starts: plugin exits and hooks on POS services, your own configuration properties, JavaScript for the POS user interface. Before your plugin touches receipts or master data, read Working with the CDBSession. Database sessions are where most first plugins go wrong.

Did you get your Hello World running with this guide, or did you get stuck at one of the steps? Tell me in the comments or send me a message on LinkedIn.

ccosapSAPCustomerCheckoutpluginmavenjavatutorial