I received a requirement to develop an interface for payment files with a bank as part of an S/4HANA implementation project. This project involved migrating from the existing SAP ECC6 on-premise + SAP PI/PO setup to S/4HANA Cloud, Private Edition + SAP BTP-CPI.
The payment files needed to be encrypted or hashed using the bank’s proprietary executable JAR files before being sent. Here's a brief overview of the requirements:
java -jar Bank-A-Encrypt.jar <input-file-full-path> <output-file-full-path>
java -Dcustom_config.properties="./config/custom_config.properties" -jar Bank-B-Hash.jar <input-file-full-path> <output-file-full-path>
In the previous setup, SAP PI/PO handled this task by running OS commands on the communication channel. However, with the new cloud-based solution, I couldn’t use standard Java libraries and had to rely on the bank's provided executable JAR files.
Running executable JAR files in a cloud-based solution like S/4HANA Cloud + SAP CPI posed several challenges. Here are the solutions I tried and why they didn't work:
After trying these approaches, I found a workaround that worked in my case.
I developed a Java program to wrap and run the executable JAR files, then deployed it on SAP BTP. To simplify the implementation, I used Spring Boot.
Here’s an overview of the architecture and development steps:
In this blog, I'll focus on Steps 1-4 and demonstrate the solution using a mock JAR file to simulate the scenario.
To simulate the bank’s JAR file, I created a mock JAR file. Running the following command processes an input file and generates an encrypted output file in the same folder:
java -jar bank-executable-mock.jar <input-file-full-path>
package com.binlah.sap.btp.appdev; import java.io.*; import java.nio.charset.StandardCharsets; public class BankExecutableMock { public static void main(String[] args) { if (args.length < 1) { System.err.println("Usage: java BankExecutableMock <inputFile>"); return; } String inputFilePath = args[0]; String outputFilePath = getOutputFileName(inputFilePath); // Read content from the input file String fileContent = readFromFile(inputFilePath); if (fileContent == null) { System.err.println("Failed to read from the input file."); return; } // Process the content String processedContent = "Encrypted((" + fileContent.trim() + "))"; // Write processed content to the output file writeToFile(outputFilePath, processedContent); } public static String getOutputFileName(String inputFileName) { // Replace the extension with ".txt" return inputFileName.replaceAll("\\.\\w+$", ".encrypt"); } public static void writeToFile(String filePath, String content) { try (BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(filePath), StandardCharsets.UTF_8))) { writer.write(content); System.out.println("Content written to file successfully: " + filePath); } catch (IOException e) { System.err.println("Error writing to file: " + e.getMessage()); } } public static String readFromFile(String filePath) { StringBuilder content = new StringBuilder(); try (BufferedReader reader = new BufferedReader( new InputStreamReader(new FileInputStream(filePath), StandardCharsets.UTF_8))) { String line; while ((line = reader.readLine()) != null) { content.append(line).append(System.lineSeparator()); } } catch (IOException e) { System.err.println("Error reading from file: " + e.getMessage()); return null; } return content.toString(); } }
This is my payment file.
Encrypted((This is my payment file.))
Use Spring Initializr to create a Spring Boot application. Inside the application, use ProcessBuilder to run the executable JAR.
Generate a Spring Boot project with the following dependencies:
After creating the Spring project, extract the files and copy bank-executable-mock.jar into the src/resources folder. Initially, comment out security-related dependencies in pom.xml to skip authentication during testing.
Example Code (pom.xml):
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Security dependencies (commented for now) --> <!-- Uncomment these after testing --> </dependencies>
Create a new controller class to receive HTTP requests as shown below.
package com.binlah.sap.btp.appdev.bank_wrapper; import org.springframework.web.bind.annotation.*; import org.springframework.http.ResponseEntity; import java.io.FileNotFoundException; import java.io.InputStream; import java.nio.file.*; import java.util.*; import java.io.OutputStreamWriter; import java.io.BufferedWriter; import java.nio.charset.StandardCharsets; @RestController @RequestMapping("/api") public class EncryptionController { @PostMapping("/encrypt") public ResponseEntity<Map<String, String>> encryptFile( @RequestBody Map<String, String> request) { try { String rootPath = "C:/Dev/Bank"; String javaPath = "java"; // Extract input fields String inputFileName = request.get("input_file"); String base64Input = request.get("input_data"); if (inputFileName == null || inputFileName.isEmpty()) { return ResponseEntity.badRequest() .body(Map.of("error", "Input field 'input_file' is missing or empty.")); } if (base64Input == null || base64Input.isEmpty()) { return ResponseEntity.badRequest() .body(Map.of("error", "Input field 'input_data' is missing or empty.")); } // Generate unique folder for the request String uniqueFolderName = UUID.randomUUID().toString(); Path uniqueFolderPath = Paths.get(rootPath, uniqueFolderName); Files.createDirectories(uniqueFolderPath); // Decode Base64 input byte[] decodedBytes = Base64.getDecoder().decode(base64Input); // Write input file with UTF-8 Path inputFilePath = uniqueFolderPath.resolve(inputFileName); writeUtf8(inputFilePath, decodedBytes); // Define output file path with dynamic extension String outputFileName = getOutputFileName(inputFileName); Path outputFilePath = uniqueFolderPath.resolve(outputFileName); // Run the external JAR to process the file ProcessBuilder processBuilder = new ProcessBuilder(javaPath, "-jar", getJarPath(), inputFilePath.toString()); Process process = processBuilder.start(); int exitCode = process.waitFor(); if (exitCode != 0) { return ResponseEntity.status(500) .body(Map.of("error", "Encryption process failed.")); } // Read the output file and encode it to Base64 byte[] encryptedFileBytes = Files.readAllBytes(outputFilePath); String base64Encrypted = Base64.getEncoder().encodeToString(encryptedFileBytes); // Clean up the folder // cleanUpDirectory(uniqueFolderPath); // Return the response in the specified format return ResponseEntity .ok(Map.of("output_file", outputFileName, "output_data", base64Encrypted)); } catch (Exception e) { e.printStackTrace(); return ResponseEntity.status(500).body(Map.of("error", e.getMessage())); } } private String getOutputFileName(String inputFileName) { // Replace the extension with ".txt" return inputFileName.replaceAll("\\.\\w+$", ".encrypt"); } private String getJarPath() throws Exception { // Extract the bundled JAR to a temporary location InputStream jarStream = getClass().getResourceAsStream("/bank-executable-mock.jar"); if (jarStream == null) { throw new FileNotFoundException("Bundled JAR not found."); } Path tempJarPath = Files.createTempFile("bank-executable-mock", ".jar"); Files.copy(jarStream, tempJarPath, StandardCopyOption.REPLACE_EXISTING); tempJarPath.toFile().deleteOnExit(); return tempJarPath.toString(); } private void writeUtf8(Path filePath, byte[] content) throws Exception { // Write UTF-8 try (BufferedWriter writer = new BufferedWriter( new OutputStreamWriter(Files.newOutputStream(filePath), StandardCharsets.UTF_8))) { // Write content writer.write(new String(content, StandardCharsets.UTF_8)); } } private void cleanUpDirectory(Path directoryPath) throws Exception { // Recursively delete files and the directory Files.walk(directoryPath).sorted(Comparator.reverseOrder()).forEach(path -> { try { Files.delete(path); } catch (Exception e) { e.printStackTrace(); } }); } }
Encode files as base64 strings to process them, then run the Spring Boot application on your local machine and test it using Curl/Postman.
http://localhost:8080/api/encrypt
{ "input_file": "input.txt", "input_data": "VGhpcyBpcyBteSBwYXltZW50IGZpbGUu" }
{ "output_file": "input.encrypt", "output_data": "RW5jcnlwdGVkKChUaGlzIGlzIG15IHBheW1lbnQgZmlsZS4pKQ" }
You can also verify the results by checking the temporary files in the generated folder.
Before deploying to BTP Cloud Foundry, update the code as follows:
// Modify file paths and Java runtime as shown below // Old paths // String rootPath = "C:/Dev/Bank"; // String javaPath = "java"; // Updated paths for SAP BTP String rootPath = "/tmp"; String javaPath = "/home/vcap/app/META-INF/.sap_java_buildpack/sap_machine_jdk/bin/java"; // Uncomment to clean up the folder after processing cleanUpDirectory(uniqueFolderPath);
Modify the application to support deployment on SAP BTP, and create a manifest.yml file.
applications: - name: bank-wrapper path: ./target/bank-wrapper-0.0.1-SNAPSHOT.jar random-route: true memory: 1024M buildpacks: - sap_java_buildpack_jakarta env: TARGET_RUNTIME: tomcat JBP_CONFIG_COMPONENTS: "jres: ['com.sap.xs.java.buildpack.jdk.SAPMachineJDK']" JBP_CONFIG_SAP_MACHINE_JDK: '{ version: 21.+ }'
mvn clean install cf api https://api.cf.us10-001.hana.ondemand.com cf login --sso cf push
Test the API after deployment to verify it behaves as expected.
If the Java path is not updated, the following error may occur:
{ "error": "Cannot run program "java": error=2, No such file or directory" }
Enable security capabilities by integrating XSUAA.
{ "xsappname": "bank-wrapper", "tenant-mode": "dedicated", "scopes": [ { "name": "$XSAPPNAME.api_access", "description": "Access to the API" } ], "role-templates": [ { "name": "API_ACCESS", "description": "Role for API access", "scope-references": [ "$XSAPPNAME.api_access" ] } ], "oauth2-configuration": { "redirect-uris": [ "https://*.cfapps.us10.hana.ondemand.com/**" ] } }
package com.binlah.sap.btp.appdev.bank_wrapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.web.SecurityFilterChain; @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests( auth -> auth.requestMatchers("/api/*").authenticated().anyRequest().permitAll()) .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.decoder(jwtDecoder()))); return http.build(); } @Bean public JwtDecoder jwtDecoder() { String jwkSetUri = "https://[your sub account].authentication.us10.hana.ondemand.com/token_keys"; return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build(); } }
Update manifest.yml to include the XSUAA service:
applications: - name: bank-wrapper path: ./target/bank-wrapper-0.0.1-SNAPSHOT.jar random-route: true memory: 1024M buildpacks: - sap_java_buildpack_jakarta env: TARGET_RUNTIME: tomcat JBP_CONFIG_COMPONENTS: "jres: ['com.sap.xs.java.buildpack.jdk.SAPMachineJDK']" JBP_CONFIG_SAP_MACHINE_JDK: '{ version: 21.+ }' services: - bank-xsuaa-instance
Deployment Command:
mvn clean install cf create-service xsuaa application bank-xsuaa-instance -c xs-security.json cf create-service-key bank-xsuaa-instance defaultKey # (optional) check service key cf service-key bank-xsuaa-instance defaultKey # Deploy application cf push cf bind-service bank-wrapper bank-xsuaa-instance cf restage bank-wrapper
Now, the API is secured with OAuth 2.0 by using Bearer {{token}}.
https://[your sub account].authentication.us10.hana.ondemand.com/oauth/token
This approach allowed me to handle executable JAR files in a cloud-based environment on SAP BTP while addressing challenges like file storage and security. I hope this guide helps other developers facing similar requirements.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
12 | |
11 | |
9 | |
6 | |
6 | |
6 | |
5 | |
4 | |
4 | |
4 |