From 946bd4c37bfb78d251a202c605d867179ac25dde Mon Sep 17 00:00:00 2001 From: Maic Stohr Date: Wed, 8 Apr 2026 17:54:00 +0200 Subject: [PATCH 1/2] feat(cli): add parameterized launcher for runtime-configured environments --- README.md | 17 +- .../ParameterizedEbicsClientLauncher.java | 407 ++++++++++++++++++ .../ParameterizedEbicsClientLauncherTest.java | 47 ++ 3 files changed, 470 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/kopi/ebics/client/ParameterizedEbicsClientLauncher.java create mode 100644 src/test/java/org/kopi/ebics/client/ParameterizedEbicsClientLauncherTest.java diff --git a/README.md b/README.md index feadd9e1..2665a523 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,22 @@ How to get started: https://github.com/ebics-java/ebics-java-client/wiki/EBICS-Client-HowTo +Parameterized launcher (without `ebics.txt`): + +``` +export EBICS_PASSWORD='changeit' +export EBICS_USER_ID='USER123' +export EBICS_PARTNER_ID='PARTNER123' +export EBICS_HOST_ID='HOST123' +export EBICS_BANK_URL='https://bank.example/ebics' + +mvn exec:java \ + -Dexec.mainClass=org.kopi.ebics.client.ParameterizedEbicsClientLauncher \ + -Dexec.args="--create --ini --hia --hpb" +``` + +This mode is useful for containerized or ephemeral environments where `ebics.txt` should not be persisted. + You can build it directly from the source with maven or use the releases from [JitPack](https://jitpack.io/#ebics-java/ebics-java-client/). Gradle: @@ -47,4 +63,3 @@ Maven ``` - diff --git a/src/main/java/org/kopi/ebics/client/ParameterizedEbicsClientLauncher.java b/src/main/java/org/kopi/ebics/client/ParameterizedEbicsClientLauncher.java new file mode 100644 index 00000000..a24872ee --- /dev/null +++ b/src/main/java/org/kopi/ebics/client/ParameterizedEbicsClientLauncher.java @@ -0,0 +1,407 @@ +/* + * Copyright (c) 1990-2012 kopiLeft Development SARL, Bizerte, Tunisia + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +package org.kopi.ebics.client; + +import java.io.File; +import java.net.URL; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; +import org.kopi.ebics.interfaces.EbicsBank; +import org.kopi.ebics.interfaces.EbicsPartner; +import org.kopi.ebics.interfaces.PasswordCallback; +import org.kopi.ebics.session.DefaultConfiguration; +import org.kopi.ebics.session.OrderType; +import org.kopi.ebics.session.Product; + +/** + * Parameter-based launcher that avoids relying on a persisted ebics.txt file in the workspace. + * It receives runtime parameters from environment variables. + */ +public final class ParameterizedEbicsClientLauncher { + private static final Set RESERVED_FLAGS = Set.of( + "--create", + "--ini", + "--hia", + "--hpb", + "--help" + ); + + private ParameterizedEbicsClientLauncher() { + } + + public static void main(String[] args) throws Exception { + ParsedArguments parsedArguments = ParsedArguments.parse(args); + if (parsedArguments.hasFlag("--help")) { + printUsage(); + return; + } + + String passphrase = requiredEnv("EBICS_PASSWORD"); + String userId = requiredEnv("EBICS_USER_ID"); + String partnerId = requiredEnv("EBICS_PARTNER_ID"); + String hostId = requiredEnv("EBICS_HOST_ID"); + String bankUrl = requiredEnv("EBICS_BANK_URL"); + String languageCode = env("EBICS_LANGUAGE_CODE", "de"); + String countryCode = env("EBICS_COUNTRY_CODE", "DE").toUpperCase(Locale.ROOT); + + propagateOptionalSystemProperty( + "ebics.key.length", + normalize(System.getenv("EBICS_KEY_LENGTH")) + ); + propagateOptionalSystemProperty( + "ebics.cert.validity.years", + normalize(System.getenv("EBICS_CERT_VALIDITY_YEARS")) + ); + + Properties properties = buildConfigurationProperties(languageCode, countryCode); + File rootDirectory = rootDirectory(); + DefaultConfiguration configuration = createConfiguration( + rootDirectory, + properties, + languageCode, + countryCode + ); + EbicsClient client = new EbicsClient(configuration, null); + Product product = new Product( + env("EBICS_PRODUCT_NAME", "EBICS Java Client"), + languageCode, + null + ); + PasswordCallback passwordCallback = () -> passphrase.toCharArray(); + + User user; + if (parsedArguments.hasFlag("--create")) { + user = client.createUser( + new URL(bankUrl), + env("EBICS_BANK_NAME", hostId), + hostId, + partnerId, + userId, + env("EBICS_USER_NAME", userId), + env("EBICS_USER_EMAIL", userId + "@example.invalid"), + env("EBICS_USER_COUNTRY", countryCode), + env("EBICS_USER_ORGANIZATION", "EBICS"), + resolveUseCertificate(), + true, + passwordCallback + ); + } else { + user = client.loadUser(hostId, partnerId, userId, passwordCallback); + ensureLoadedUserMatchesConfiguredEndpoint(user, bankUrl, hostId); + } + + if (parsedArguments.hasFlag("--ini")) { + client.sendINIRequest(user, product); + } + if (parsedArguments.hasFlag("--hia")) { + client.sendHIARequest(user, product); + } + if (parsedArguments.hasFlag("--hpb")) { + client.sendHPBRequest(user, product); + } + + String orderFlag = parsedArguments.firstOrderFlag(); + if (orderFlag != null) { + OrderType orderType = OrderType.valueOf(orderFlag.substring(2).toUpperCase(Locale.ROOT)); + if (parsedArguments.inputPath() != null) { + client.sendFile( + new File(parsedArguments.inputPath()), + user, + product, + orderType, + defaultUploadParams(user, orderType) + ); + } else if (parsedArguments.outputPath() != null) { + if (parsedArguments.startDate() != null || parsedArguments.endDate() != null) { + System.err.println( + "Date range arguments are ignored in parameterized mode for this order type." + ); + } + client.fetchFile( + new File(parsedArguments.outputPath()), + user, + product, + orderType, + Boolean.parseBoolean(env("EBICS_TEST_MODE", "false")) + ); + } + } + + client.quit(); + } + + private static void printUsage() { + String usage = "Usage: ParameterizedEbicsClientLauncher [--create] [--ini] [--hia] [--hpb]" + + " [--] [-i inputFile] [-o outputFile]\n" + + "Required environment variables: EBICS_PASSWORD, EBICS_USER_ID, EBICS_PARTNER_ID," + + " EBICS_HOST_ID, EBICS_BANK_URL"; + System.out.println(usage); + } + + private static EbicsUploadParams defaultUploadParams(User user, OrderType orderType) { + if (orderType == OrderType.XE2) { + var orderParams = new EbicsUploadParams.OrderParams( + "MCT", + "CH", + null, + "pain.001", + "03", + true + ); + return new EbicsUploadParams(null, orderParams); + } + return new EbicsUploadParams(user.getPartner().nextOrderId(), null); + } + + private static File rootDirectory() { + String explicit = normalize(System.getenv("EBICS_ROOT_DIR")); + if (explicit != null) { + return new File(explicit); + } + String userHome = System.getProperty("user.home"); + if (userHome == null || userHome.isBlank()) { + throw new IllegalStateException("Missing user.home for EBICS workspace resolution."); + } + return new File(new File(userHome), "ebics/client"); + } + + private static DefaultConfiguration createConfiguration( + File rootDirectory, + Properties properties, + String languageCode, + String countryCode + ) { + Locale locale = new Locale( + languageCode.toLowerCase(Locale.ROOT), + countryCode.toUpperCase(Locale.ROOT) + ); + return new DefaultConfiguration(rootDirectory, properties) { + @Override + public Locale getLocale() { + return locale; + } + }; + } + + private static Properties buildConfigurationProperties( + String languageCode, + String countryCode + ) { + Properties properties = new Properties(); + properties.setProperty("conf.file.name", "ebics.properties"); + properties.setProperty("keystore.dir.name", "keystore"); + properties.setProperty("traces.dir.name", "traces"); + properties.setProperty("serialization.dir.name", "serialized"); + properties.setProperty("ssltruststore.dir.name", "ssl"); + properties.setProperty("sslkeystore.dir.name", "ssl"); + properties.setProperty("sslbankcert.dir.name", "ssl"); + properties.setProperty("users.dir.name", "users"); + properties.setProperty("letters.dir.name", "letters"); + properties.setProperty("signature.version", env("EBICS_SIGNATURE_VERSION", "A005")); + properties.setProperty("authentication.version", env("EBICS_AUTHENTICATION_VERSION", "X002")); + properties.setProperty("encryption.version", env("EBICS_ENCRYPTION_VERSION", "E002")); + properties.setProperty("ebics.version", env("EBICS_VERSION", "H003")); + properties.setProperty("languageCode", languageCode); + properties.setProperty("countryCode", countryCode); + return properties; + } + + private static boolean resolveUseCertificate() { + String explicit = normalize(System.getenv("EBICS_USE_CERTIFICATE")); + if (explicit != null) { + return "true".equalsIgnoreCase(explicit); + } + String signatureVersion = env("EBICS_SIGNATURE_VERSION", "A005"); + return "A006".equalsIgnoreCase(signatureVersion); + } + + private static void ensureLoadedUserMatchesConfiguredEndpoint( + User user, + String configuredBankUrl, + String configuredHostId + ) { + if (user == null) { + return; + } + + EbicsPartner partner = user.getPartner(); + EbicsBank bank = partner == null ? null : partner.getBank(); + if (bank == null) { + return; + } + + String expectedUrl = normalize(configuredBankUrl); + String loadedUrl = bank.getURL() == null ? null : normalize(bank.getURL().toString()); + if (expectedUrl != null && !expectedUrl.equals(loadedUrl)) { + throw new IllegalStateException( + "Loaded user endpoint does not match configured EBICS_BANK_URL. " + + "Run with --create or clean serialized state." + ); + } + + String expectedHostId = normalize(configuredHostId); + String loadedHostId = normalize(bank.getHostId()); + if ( + expectedHostId != null && + loadedHostId != null && + !expectedHostId.equals(loadedHostId) + ) { + throw new IllegalStateException( + "Loaded user host id does not match configured EBICS_HOST_ID. " + + "Run with --create or clean serialized state." + ); + } + } + + private static void propagateOptionalSystemProperty(String key, String value) { + if (value != null) { + System.setProperty(key, value); + } + } + + private static String requiredEnv(String key) { + String value = normalize(System.getenv(key)); + if (value == null) { + throw new IllegalArgumentException("Missing required environment variable: " + key); + } + return value; + } + + private static String env(String key, String fallback) { + String value = normalize(System.getenv(key)); + return value == null ? fallback : value; + } + + static String normalize(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + static final class ParsedArguments { + private final Set flags = new LinkedHashSet<>(); + private final String inputPath; + private final String outputPath; + private final String startDate; + private final String endDate; + + private ParsedArguments( + Set flags, + String inputPath, + String outputPath, + String startDate, + String endDate + ) { + this.flags.addAll(flags); + this.inputPath = inputPath; + this.outputPath = outputPath; + this.startDate = startDate; + this.endDate = endDate; + } + + static ParsedArguments parse(String[] args) { + Set flags = new LinkedHashSet<>(); + String inputPath = null; + String outputPath = null; + String startDate = null; + String endDate = null; + if (args != null) { + for (int index = 0; index < args.length; index++) { + String arg = args[index]; + if (arg == null || arg.isBlank()) { + continue; + } + if ("-i".equals(arg) || "--input".equals(arg)) { + inputPath = requireValue(args, ++index, arg); + continue; + } + if ("-o".equals(arg) || "--output".equals(arg)) { + outputPath = requireValue(args, ++index, arg); + continue; + } + if ("-s".equals(arg) || "--start".equals(arg)) { + startDate = requireValue(args, ++index, arg); + continue; + } + if ("-e".equals(arg) || "--end".equals(arg)) { + endDate = requireValue(args, ++index, arg); + continue; + } + if (arg.startsWith("--")) { + flags.add(arg.toLowerCase(Locale.ROOT)); + } + } + } + return new ParsedArguments(flags, inputPath, outputPath, startDate, endDate); + } + + private static String requireValue(String[] args, int index, String option) { + if (args == null || index >= args.length) { + throw new IllegalArgumentException("Missing value for option " + option); + } + String value = normalize(args[index]); + if (value == null) { + throw new IllegalArgumentException("Missing value for option " + option); + } + return value; + } + + boolean hasFlag(String flag) { + return flags.contains(flag.toLowerCase(Locale.ROOT)); + } + + String firstOrderFlag() { + for (String flag : flags) { + if (RESERVED_FLAGS.contains(flag)) { + continue; + } + String candidate = flag.startsWith("--") + ? flag.substring(2).toUpperCase(Locale.ROOT) + : flag.toUpperCase(Locale.ROOT); + try { + OrderType.valueOf(candidate); + return flag; + } catch (IllegalArgumentException ignored) { + // ignore unknown flags that are not EBICS order types + } + } + return null; + } + + String inputPath() { + return inputPath; + } + + String outputPath() { + return outputPath; + } + + String startDate() { + return startDate; + } + + String endDate() { + return endDate; + } + } +} diff --git a/src/test/java/org/kopi/ebics/client/ParameterizedEbicsClientLauncherTest.java b/src/test/java/org/kopi/ebics/client/ParameterizedEbicsClientLauncherTest.java new file mode 100644 index 00000000..5efaee4a --- /dev/null +++ b/src/test/java/org/kopi/ebics/client/ParameterizedEbicsClientLauncherTest.java @@ -0,0 +1,47 @@ +package org.kopi.ebics.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ParameterizedEbicsClientLauncherTest { + @Test + void parsesFlagsAndInputOutputOptions() { + var parsed = ParameterizedEbicsClientLauncher.ParsedArguments.parse( + new String[]{ "--create", "--ini", "--sta", "-o", "sta.xml", "-s", "2026-01-01" } + ); + + assertTrue(parsed.hasFlag("--create")); + assertTrue(parsed.hasFlag("--ini")); + assertEquals("--sta", parsed.firstOrderFlag()); + assertEquals("sta.xml", parsed.outputPath()); + assertEquals("2026-01-01", parsed.startDate()); + } + + @Test + void ignoresReservedFlagsWhenResolvingOrder() { + var parsed = ParameterizedEbicsClientLauncher.ParsedArguments.parse( + new String[]{ "--create", "--ini", "--hpb" } + ); + + assertNull(parsed.firstOrderFlag()); + } + + @Test + void rejectsMissingOptionValue() { + IllegalArgumentException exception = assertThrows( + IllegalArgumentException.class, + () -> ParameterizedEbicsClientLauncher.ParsedArguments.parse(new String[]{ "-o" }) + ); + assertTrue(exception.getMessage().contains("Missing value for option -o")); + } + + @Test + void normalizeHandlesBlankValues() { + assertNull(ParameterizedEbicsClientLauncher.normalize(" ")); + assertEquals("value", ParameterizedEbicsClientLauncher.normalize(" value ")); + } +} From abd0b8d26412698d6ac42c62d5f2ecce4d4eeaf3 Mon Sep 17 00:00:00 2001 From: Maic Stohr Date: Wed, 8 Apr 2026 22:14:24 +0200 Subject: [PATCH 2/2] ci: skip dependency graph submission for fork PRs --- .github/workflows/maven.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 6031fcd7..fa7dfd95 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -8,7 +8,8 @@ on: jobs: build: - + permissions: + contents: write runs-on: ubuntu-latest steps: @@ -24,4 +25,5 @@ jobs: # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: Update dependency graph + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6