Skip to content

Library Gaps

Philip Helger edited this page Sep 28, 2026 · 19 revisions

Library Gaps

Things peppol-cli needs that the existing libraries do not currently provide. Each was verified by reading the source; nothing here is inferred from documentation or from memory.

The Where it belongs column matters: most of these are better fixed upstream than worked around in the CLI, because other consumers need them too. A gap fixed in peppol-commons helps phoss-smp and phoss-ap as well; the same code hidden inside peppol-cli helps nobody else.

# Gap Verified finding Where it belongs
1 GLN check-digit validation is not wired up (much smaller than first assessed) The algorithm does exist: com.helger.masterdata.ean.GTINValidator.isValidGLN(String) in ph-masterdata — GLN is treated as GTIN-13, checked with the standard GS1 alternating 1/3 weighted mod-10 digit via AbstractUPCEAN.calcChecksum. The same class also covers GTIN-8/12/14 and SSCC. And peppol-id-checks already declares a direct dependency on com.helger.masterdata:ph-masterdata. What is missing is only the wiring: a grep across peppol-commons for GTINValidator / isValidGLN returns nothing, so no participant identifier validator calls it. Registered SPI validators remain just two — Norwegian mod-11 and Belgian mod-97. peppol-id-checks, as a ParticipantIdentifierValidatorGLN SPI implementation that strips the 0088: prefix and delegates to GTINValidator.isValidGLN. No new dependency, no new algorithm — genuinely a small piece of work. > TODO(verify): whether the same treatment is wanted for ICD 0209 (GS1) and any other GTIN-based scheme, since GTINValidator already covers those lengths.
2 Directory: only the search HTTP caller is missing Nearly everything is already published. com.helger:phoss-directory-client (PDClient) covers the indexer interface — isServiceGroupRegistered, addServiceGroupToIndex, deleteServiceGroupFromIndex — with mTLS config in PDClientConfiguration. com.helger:phoss-directory-searchapi is a complete, schema-validated response model: the XSD directory-search-result-list-v1.xsd, PDResultListMarshaller, CPDSearchAPI, and JAXB types ResultListType / MatchType / EntityType / IDType / NameType / ContactType in com.helger.pd.searchapi.v1. Its dependencies are ph-xml / ph-jaxb only — deliberately no HTTP client. And SMPBusinessCardProvider shows business cards come from the participant's SMP at {smpHostURI}businesscard/{participantId}. Absent is only an HTTP caller for the search endpoint, whose full surface is known: /search/1.0/{xml|json}, the twelve EPDSearchField names, rpi/rpc paging, the 1000-result cap and HTTP 429 with Retry-After. A thin HTTP caller that returns the existing ResultListType. Best added to phoss-directory-client, which already owns the Directory client role and is published; then peppol-cli writes no Directory protocol code at all. > TODO(decide): extend phoss-directory-client with a search client, or keep search in peppol-cli? Extending it is the better outcome. > TODO(verify): the operator syntax for search fields (ESearchOperator) over the REST API.
3 Phorm client library com.helger:phorm is the server WAR; there is no phorm-client module and no published client artefact. The only real caller is phoss-ap's PhormDocumentVerifier, which is coupled to phoss-ap-api and phoss-ap-basic and so cannot be reused as-is. Two copy-paste examples live in Phorm's own test tree. A small shared phorm-client module — consumed by both phoss-ap and peppol-cli — is the better outcome than a third hand-rolled HTTP client. It would also give the API a single documented client-side contract.
4 Only the certificate-field read is missing (smaller than first assessed) Everything downstream of the Seat ID string already exists in peppol-id's SPIDHelper: getMainIDFromSeatID decodes a Seat ID (P + 2-letter country + 6-digit Main ID) into its Main ID, and isValidSPID / getMainID / getUseCaseID / getServiceProviderSuffix handle the full Service Provider ID form MainID[-UseCaseID[.ServiceProviderSuffix]] (all @since 12.6.1). What is absent is only pulling the Seat ID string out of an X509Certificate — a grep shows REGEX_SEAT_ID is referenced solely from SPIDHelper, so nothing reads a certificate. A small helper, plausibly in peppol-commons' com.helger.peppol.security package, that extracts the Seat ID from the certificate and hands it to SPIDHelper. > TODO(verify): which certificate field carries the Seat ID (subject CN component?), from the Peppol PKI documentation. This is the last unknown — once answered, the feature is a two-line composition.
5 CA generation flag has to be future-proofed blind PeppolTrustStores has Config2018 (G2, @Deprecated since 12.3.8, JKS) and Config2025 (G3, current, PKCS12), with PeppolTrustedCA wired to Config2025 only. There is no G4 or "next" constant, so peppol cert verify --generation has exactly one meaningful value today. Nothing to add upstream yet, but the CLI must design the flag so that a future generation is not a breaking change — and needs to know whether one is coming. > TODO(verify): is a G4 generation announced or in preparation? That determines whether --generation should accept a generation name now or be deferred entirely until there is a choice to make.
6 Per-user config file discovery ph-config resolves the classpath, the working directory and whatever config.file points at. It has no notion of XDG base directories or %APPDATA%. ph-config. Every CLI in this ecosystem needs it — norma-cli, kaltblut-cli, ph-redact-cli, the en16931-*-cli tools — so solving it once upstream beats a fourth private copy. A concrete design is proposed at the end of this page.
7 Config value provenance peppol config show should print where each value came from. Sources carry a priority, but it is unclear how much origin information ConfiguredValue / IConfigurationSource expose. ph-config, if the information is not already reachable. > TODO(verify): read ConfiguredValue and IConfigurationSource to establish what is available before assuming this is a gap.
8 Config secret handling ph-config has no notion of a secret value, no redaction and no file-permission checking. Arguably the CLI's own concern, but a "this key is secret" marker in ph-config would let every consumer redact consistently instead of each maintaining its own key-name pattern. > TODO(decide): upstream or local?

| 9 | No short, stable aliases for document type identifiers | The code list provides display names (getCommonName(), the XML name attribute), but 16 of the 321 document type display names in v9.7 are duplicated, and display names are not guaranteed stable across code list versions. There is no curated short-name table anywhere in peppol-commons. | Arguably peppol-commons, since the problem is not CLI-specific — phoss-smp's UI and any other tool faces the same unusable 200-character identifiers. A curated alias table there would benefit everything. See Open Questions #9. | | 10 | Migration key pattern is more permissive than the spec | CSMLDefault.MIGRATION_CODE_PATTERN accepts _ (underscore) as one of the required special characters. SML 1.3.0 §2.2 enumerates the permitted set explicitly — @ # $ % ( ) [ ] { } * ^ - ! ~ | + = with ASCII codes — and underscore is not in it. So peppol-commons would accept a migration key that the spec does not permit, and an SML implementation validating strictly would reject it. | peppol-commons, a one-character fix. Note the spec also contradicts itself: §2.2 requires at least two special characters, while §3.2.6 describes the MigrationKey as "a string of characters and numbers only, with a maximum length of 24 characters". §2.2 is the detailed normative statement and is what peppol-commons implements. > TODO(verify): worth raising with OpenPeppol so §3.2.6 is corrected. |

Non-blocking findings worth fixing anyway

These came out of the research and are not peppol-cli requirements, but they will mislead whoever implements it.

  1. peppol-commons README is out of date. The usage examples around lines 270–290 use PeppolURLProvider.INSTANCE and ESML.DIGIT_TEST; neither exists in 13.0.1-SNAPSHOT. The current equivalents are PeppolNaptrURLProvider.INSTANCE and ESML.PEPPOL_TEST / ESML.PEPPOL_PRODUCTION. ESML's own class Javadoc still lists the removed DIGIT_PRODUCTION / DIGIT_TEST constants.
  2. The MD5 misconception. The participant hash is SHA-256 + Base32 with padding stripped (AbstractBDXLURLProvider); the MD5-based PeppolURLProvider was removed. Stale documentation saying MD5 is an easy way for an implementer — or an AI assistant — to get this wrong, and it is worth correcting at the source.
  3. Phorm: GET /api/get/vesids is effectively unauthenticated. In ApiGetAllVESIDs.java the token check is behind a disabled guard:
    if (false)
      if (!verifyAuthOrSetForbidden (aRequestScope, aUnifiedResponse, sLogPrefix))
        return;
    Every other API endpoint enforces X-Token. This may well be deliberate — it makes the endpoint usable as a connectivity probe, which peppol-cli would benefit from — but a dead if (false) does not say whether it is intended. Worth either removing or replacing with a comment explaining the intent. Flagging it because it is a security-relevant asymmetry, not because the CLI needs it changed.
  4. Generated sources. peppol-id-datatypes, peppol-smp-datatypes, peppol-sbdh and peppol-sml-client need mvn generate-sources before IDE use. A CONTRIBUTING note would save a new contributor an hour.

How these gaps affect planning

Gap Affected feature Consequence if not closed
1 peppol id check-value Without the wiring, GLN identifiers get syntax-only validation and the command silently under-delivers. With it, the check is ~10 lines.
2 peppol dir search Only a thin HTTP caller is needed; response parsing and the indexer side already exist. Comfortably M2.
3 peppol validate * A hand-rolled client duplicates phoss-ap's logic, so a Phorm API change breaks two places.
4 peppol cert seat-id Only the certificate-field read is needed; SPIDHelper does the decoding. Risk is limited to reading the wrong field.
6 Config discovery Every CLI in the ecosystem keeps reinventing it.

Proposal: per-user configuration discovery in ph-config

A concrete sketch for gap #6, written to fit ph-config as it is rather than to redesign it. Everything referenced below was read in ph-commons/ph-config and ph-commons/ph-base.

Shape: an opt-in augmenting helper, not a change to the default provider

ConfigFactory already has the right precedent — addProfilePropertiesSources (MultiConfigurationValueProvider, String...), a static helper that adds sources to a caller-supplied provider and returns how many it added. A discovery helper should mirror that shape exactly.

createDefaultValueProvider () must not change. If per-user discovery became a default, server applications — phoss-SMP, phoss-AP, Phorm — would silently start reading ~/.config, which is wrong for a daemon and a security question besides. Opt-in keeps the change purely additive.

Proposed API

// New class, e.g. com.helger.config.source.userdir.UserConfigDirHelper

// Platform-aware directory resolution. Reads no configuration itself, so it is
// safe to call before any configuration exists.
@Nullable public static File getUserConfigDir (@NonNull @Nonempty String sAppName);
@Nullable public static File getUserCacheDir  (@NonNull @Nonempty String sAppName);

// The augmenting helper, mirroring ConfigFactory.addProfilePropertiesSources
@Nonnegative
public static int addUserConfigSources (@NonNull MultiConfigurationValueProvider aMCSVP,
                                       @NonNull @Nonempty String sAppName,
                                       @NonNull String... aBaseNames);

A CLI then does:

final MultiConfigurationValueProvider aVP = ConfigFactory.createValueProviderSystemOnly ();
UserConfigDirHelper.addUserConfigSources (aVP, "peppol-cli", "credentials", "config");
// ... plus the explicit --config file and the bundled reference.properties
final IConfig aConfig = Config.create (aVP);

Directory resolution

Built on what already exists in ph-base: SystemHelper.getOperatingSystem () returning an EOperatingSystem with isWindowsBased () / isMacOSBased () / isUnixBased (), plus SystemProperties.getUserHome (). No new dependency. A grep confirms ph-commons has no XDG or %APPDATA% handling today, so nothing is duplicated.

Platform Config directory Cache directory
Linux / BSD $XDG_CONFIG_HOME/<app> else ~/.config/<app> $XDG_CACHE_HOME/<app> else ~/.cache/<app>
macOS ~/.config/<app> by default, ~/Library/Application Support/<app> selectable ~/Library/Caches/<app>
Windows %APPDATA%\<app> %LOCALAPPDATA%\<app>\cache

The macOS choice is contentious: Apple's convention is ~/Library/Application Support, while developer tools overwhelmingly use ~/.config. Rather than ph-config legislating, make it a parameter with ~/.config as the default, so each application decides and the library stays out of the argument.

An explicit override should also be honoured — a config.user.dir key read through ConfigFactory.getSystemConfig () so it works as both a system property and an environment variable, exactly as config.file already does.

Priorities

These are all RESOURCE-type sources, so EConfigSourceType needs no change — only new constants slotted between the explicit config.file (200) and the classpath-bundled files (190 and below):

Source Proposed priority Rationale
config.file (existing) 200 An explicit --config must always win.
First user base name (e.g. credentials) RESOURCE.getDefaultPriority () - 2 = 198 Secrets override ordinary settings.
Second user base name (e.g. config) RESOURCE.getDefaultPriority () - 4 = 196 The user's own settings.
private-application.properties (existing) 190 Unchanged.
application.properties (existing) 180 Unchanged.
reference.properties (existing) 1 Shipped defaults.

Deriving each from EConfigSourceType.RESOURCE.getDefaultPriority (), as the existing constants already do, keeps the arithmetic in one place. Varargs order sets priority, decreasing by 2 per name, so a caller can pass any number of base names without the library inventing a constant for each.

Format handling: reuse, do not add

Resolve the extension through ConfigurationSourceResourceTypeRegistry.getInstance ().getFactoryOfFileExtensionOrFallback (...), exactly as ConfigFactory already does for config.file. Then:

  • .properties works immediately;
  • .json works as soon as ph-config-json is on the classpath, through its existing SPI registrar;
  • any future format registered via IConfigurationSourceResourceTypeRegistrarSPI works with no further change.

So the helper should probe each base name across the registered extensions rather than hard-coding .properties — for base name config, try config.properties then config.json, first match wins, and log a warning when more than one exists, so a stale leftover cannot silently shadow the intended file.

Deliberately out of scope for ph-config

  • Creating directories or files, and setting permissions. The helper discovers and reads; chmod 600 and first-run scaffolding belong to the application. A configuration library should not be in the file-creation business.
  • Redaction and secret marking. Related but separate — see gap #8.
  • A permission check could be offered as a standalone utility returning a status (e.g. "group- or world-readable") so a CLI can warn, but it must stay off the loading path: a server must never fail to start because a file mode looks untidy.

Why upstream

norma-cli, kaltblut-cli, ph-redact-cli and the en16931-*-cli tools all face this, and peppol-cli would be the fourth or fifth private implementation. The helper is on the order of 150 lines, adds no dependency, changes no existing behaviour, and removes a recurring papercut for every CLI in the ecosystem.

> TODO(decide): ph-config, or ph-base as a general platform-directories helper that ph-config merely consumes? The directory resolution itself has nothing to do with configuration, so ph-base is arguably the more honest home.

Clone this wiki locally