Repository navigation
Architecture
Status: proposed, except where an ADR is referenced.
| Aspect | Choice | Notes |
|---|---|---|
| Language | Java | The libraries this tool is built on are Java. |
| Baseline | Java 25 |
ADR-005. The shared ph-parent-pom currently defaults java.version to 17, so this project overrides that property. |
| Build | Maven | Consistent with the rest of the ecosystem; parent ph-parent-pom. |
| CLI framework | picocli |
ADR-006. Already used by norma-cli, kaltblut-cli, ph-redact-cli, the en16931-*-cli tools and phoss-peppol-mcp (4.7.7). |
| Logging | SLF4J, with a runtime binding in the distribution only | Log output goes to stderr. Default level is quiet; --verbose/--debug raise it. |
| Licence | Apache License 2.0 | ADR-008. Consistent with the rest of the ecosystem, and the licence every planned dependency already uses. |
Build note, verified: several peppol-commons modules generate sources with JAXB/wsimport into
target/generated-sources/xjc and need mvn generate-sources before IDE use. Worth stating in CONTRIBUTING so
a first checkout does not look broken.
The tool is deliberately a thin command line layer over existing libraries. Verified against the local sources:
| Need | Library / module |
|---|---|
| Identifiers, code list enums, Seat ID / SPID handling |
peppol-commons → peppol-id (with edelivery-id, peppol-id-datatypes; also brings ph-security from 13.1.0 for the certificate-based Seat ID extraction in SPIDHelper) |
| Identifier value checks, country derivation, GLN/GTIN check digits |
peppol-id-checks, which already depends on com.helger.masterdata:ph-masterdata — the latter provides GTINValidator (GLN, GTIN-8/12/13/14, SSCC) and IBAN handling |
| SML descriptors, networks, trust stores, CA checkers |
peppol-commons → peppol-commons (ESML, EPeppolNetwork, PeppolTrustStores, PeppolTrustedCA) |
| SMP read/write, DNS discovery, SMP JSON |
peppol-commons → peppol-smp-client (brings ph-dns + dnsjava). DNSSEC validation of the U-NAPTR lookups needs peppol-commons 13.1.2 (setDnsSecValidation) over ph-web/ph-dns 11.4.7 (com.helger.dns.dnssec) |
| SMP core, signature verification |
peppol-commons → smp-client-base. It also carries the OASIS BDXR SMP 1.0/2.0 clients, which this project does not use — see ADR-007
|
| SML management (SOAP 1.1, mTLS) |
peppol-commons → peppol-sml-client. Note SML 1.3.0 §4.2.2/§4.3.2: every SML call needs a client certificate issued under the Peppol root CA, so this whole area is unusable without configured key material — which is why it sits in M3 and why peppol config validate should check the keystore loads before a write is attempted. |
| Business cards |
peppol-commons → peppol-directory-businesscard (data model v1/v2/v3) |
| Directory indexing |
com.helger.peppol.directory:peppol-directory-client 1.0.0 (PDClient) — mTLS, indexer interface |
| Directory search |
com.helger.peppol.directory:peppol-directory-searchclient — PDSearchClient incl. searchAllPages, PDSearchClient, PDSearchQuery, PDSearchRateLimitException. No ph-config or key store dependency, since the search API is public |
| Directory search model |
com.helger.peppol.directory:peppol-directory-searchapi 1.0.0 — PDResultListMarshaller, EPDSearchAPIField, CPDSearchAPI, and the JAXB types ResultListType / MatchType / EntityType in com.helger.pd.searchapi.v1; depends only on ph-xml/ph-jaxb
|
| MLS / SBDH |
peppol-commons → peppol-mls, peppol-sbdh. Not peppol-mlr — MLR is end of life, see Rejected features
|
| Phorm validation |
com.helger:phorm-client (PhormClient, PhormValidationResult) |
| VESID parsing |
ph-diver (DVRCoordinate) |
| Local document type detection |
ddd-model (DocumentDetailsDeterminator, DocumentDetails) — a compile dependency of phorm-client, so available either way |
| Validation report model |
phive-result — resolved: this is the artifactId, confirmed as a compile dependency of phorm-client
|
Only add a dependency outside this set when nothing in it does the job. In particular: no second HTTP client, no second JSON library, no second collections library.
> TODO(verify): the full transitive dependency set and its licences once the module list is fixed — the SOAP/JAX-WS stack behind peppol-sml-client and the dnsjava dependency are the ones most likely to surprise on size or on Java 25.
Four modules, mirroring the -core / -cli split already used by kaltblut and norma:
| Module | Contains | Depends on |
|---|---|---|
peppol-cli-core |
All the actual work: identifier operations, discovery, SMP/SML/Directory/Phorm access, certificate checks, code list access. Returns typed result objects. No picocli, no System.out, no exit codes. | The libraries above |
peppol-cli-config |
Config resolution, file discovery, profiles, secret handling, redaction. | ph-config |
peppol-cli-output |
Renderers: the text renderer and the JSON envelope renderer. Turns core result objects into output. | ph-json |
peppol-cli-commands |
The picocli command tree. Parses arguments, calls core, hands results to output, maps failures to exit codes. Thin by design. | all of the above, picocli |
The point of the split is that the interesting logic is testable without a command line, and that the JSON contract lives in one module rather than being spread across command classes.
> TODO(decide): is four modules over-engineered for a tool this size? A single module with clean internal packages would build faster and is easier to navigate. The counter-argument is that peppol-cli-core is potentially reusable, and that the boundary is what stops output formatting leaking into logic.
Main class following the ecosystem's convention (com.helger.kaltblut.cli.KaltblutMain): com.helger.peppol.cli.PeppolCliMain.
> TODO(verify): confirm the intended base package and groupId (see [Naming](Naming)).
- Command results are objects, not print statements. Every core operation returns a typed result; rendering happens once, in the output module. This is what makes the text and JSON outputs consistent by construction.
- One exception-to-exit-code mapping, in the commands module. Core throws typed exceptions; nothing else decides an exit code.
-
--configbeforeConfigFactory. As noted in Configuration,ConfigFactoryreadsconfig.fileduring static initialisation, so the CLI builds its ownMultiConfigurationValueProviderrather than relying on that timing. -
Network profile values come from the libraries, via
EPeppolNetworkandESML— not from constants in this project. -
Startup cost is a feature requirement. Avoid eager classloading of the JAXB and SOAP stacks for commands
that do not need them; a
peppol pid validateshould not pay forpeppol-sml-client.
| Artefact | Purpose | Notes |
|---|---|---|
| Fat JAR | The single-file fallback that works everywhere |
maven-shade-plugin with a mainClass, as in kaltblut-cli and norma-cli. Watch for SPI/META-INF/services merging — peppol-commons uses SPIs for identifier validators and transport profiles, so the shade plugin needs ServicesResourceTransformer, or those registrations are silently lost. > TODO(verify): confirm that transformer is configured; this is a classic and quiet shading bug.
|
| Distribution archive |
.tar.gz and .zip with bin/peppol, bin/peppol.cmd, lib/, completion scripts, LICENSE and README |
maven-assembly-plugin. Launcher resolves JAVA_HOME, checks the Java version and prints a clear message if it is below 25. |
jlink image |
Self-contained archive with a bundled runtime | Removes the Java 25 prerequisite at the cost of size and per-platform builds. > TODO(decide): worth it, or is "install a JRE" acceptable?
|
| Container image | For CI use and for anyone without a Java runtime | In scope — see the Docker section below. Multi-stage build, JRE-only runtime stage, multi-arch. |
| Native image | Deliberately deferred — Open Questions #1. |
Ordered by effort. The pattern in every case is: one signed, versioned release archive plus its SHA-256, and each channel points at it.
| Channel | Effort | Notes |
|---|---|---|
| GitHub Releases | low | The source of truth every other channel consumes. Attach archives plus a SHA256SUMS file. |
| Maven Central | low | The fat JAR, consistent with the other projects. |
| Docker | low | Docker Hub under phelger/, matching phelger/phoss-ap. Arguably the single easiest way for a CI pipeline to use the tool, since it needs no Java runtime on the agent. See below. |
| Homebrew (own tap) | low–medium | The realistic first step. See below. |
| SDKMAN! | medium | Purpose-built for JVM CLI tools and a natural fit for this audience. > TODO(verify): SDKMAN's current vendor onboarding requirements.
|
| Scoop / WinGet | medium | Depends on the Windows support decision, Open Questions #8. |
| Homebrew core | high | Only worth attempting once the tool is established. See below. |
| Linux distro packages | high | Best left to distro maintainers; provide a clean tarball and they can. |
A container is a first-class channel here, not an afterthought: a CI agent can use the tool with no Java runtime installed, which removes the Java 25 prerequisite entirely for the audience most likely to script against it.
Image: phelger/peppol-cli on Docker Hub, following the existing phelger/phoss-ap convention.
> TODO(decide): also publish to GHCR (ghcr.io/phax/peppol-cli)? It costs one more push step and some users prefer it.
Build, matching the ecosystem's existing Dockerfiles (phorm, phase4-peppol-standalone): multi-stage, with
the Apache 2.0 licence header comment at the top. The runtime stage needs only a JRE — phorm already builds on a
JRE 25 base image, so current bases are available. A JRE-only or jlink-trimmed runtime stage keeps the image
small; > TODO(decide): eclipse-temurin:25-alpine, a jlink-produced runtime, or a distroless base?
Container-specific design points, which differ from the server images in this ecosystem because this is a short-lived CLI:
-
ENTRYPOINTis the executable,CMDis--help. That makesdocker run --rm phelger/peppol-cli smp lookup …read as the command it is, and a baredocker runself-documents. - Run as a non-root user. The tool writes nothing except its cache, and that only when asked.
-
Configuration by environment variable is the natural container idiom, and it is free:
ph-configalready mapspeppol.cli.networktoPEPPOL_CLI_NETWORK, sodocker run -e PEPPOL_CLI_NETWORK=prod …works with no extra code. Mounting a config directory read-only (-v ~/.config/peppol-cli:/config:ro) is the alternative for people with many settings. - Never bake key material into the image. Keystores are mounted read-only at run time. This must be stated explicitly in the documentation, because it is exactly the mistake people make.
-
Do not allocate a TTY when piping.
docker run -tcorrupts a piped stream, so--output json | jqrequiresdocker run --rmwithout-t. Colour and progress already switch off automatically when stdout is not a TTY, so the default behaviour is correct — but the footgun is common enough to document with a worked example. -
Exit codes pass through
docker rununchanged, which is what makes the container usable in CI. Our range (0–7, see CLI Conventions) stays clear of Docker's own reserved 125–127, so a pipeline can always tell a tool verdict from a container failure. -
Multi-arch
linux/amd64andlinux/arm64via buildx. A pure-JVM image is architecture-agnostic in content, so this is only about the base image. -
Tags:
X.Y.Z,X.Yandlatest. Documentation should recommend pinning an exact version in CI and treatlatestas a convenience for interactive use only. -
Document a shell alias, so people are not typing a long
docker runline repeatedly:alias peppol='docker run --rm -e PEPPOL_CLI_NETWORK -v "$HOME/.config/peppol-cli:/config:ro" phelger/peppol-cli'
- Image size is where a native image would pay off most — a native binary in a minimal base would be a fraction of a JVM image. That is a genuine argument for Open Questions #1, and a reason to keep the Dockerfile's runtime stage easy to swap.
A packaging test should run the built image and assert a known command's output and exit code, exactly as for the fat JAR — see Testing Strategy.
Two distinct paths, and they should not be confused.
Step 1 — own tap (recommended starting point). A repository named homebrew-tap under the same GitHub
account makes the tool installable as:
brew tap phax/tap
brew install peppol-cli
# or in one step
brew install phax/tap/peppol-cli
A tap has no notability requirements, no review queue and no gatekeeper, and it can be published the same day as the first release. What it takes:
- A public repository
homebrew-tap(thehomebrew-prefix is what makesbrew tap phax/tapwork). - A formula
Formula/peppol-cli.rbwith:desc,homepage,urlpointing at the release archive, itssha256,license "Apache-2.0", adepends_on "openjdk", aninstallblock that puts the JAR underlibexecand creates a wrapper inbin, atest doblock, and alivecheckblock sobrew livecheckcan spot new versions. - A release step that computes the SHA-256, updates
urlandsha256in the formula and commits it — trivially automatable from the release workflow.
Java-specific formula points to get right:
- The JAR must not be put directly in
bin. The convention is to install it intolibexecand generate a small wrapper script that invokesjava -jar, soopenjdkis found via Homebrew rather than the user'sPATH. Homebrew provides a helper for exactly this —> TODO(verify): the current helper name and signature (bin.write_jar_script, as used by existing Java formulae) against the Homebrew Formula Cookbook, rather than relying on memory. -
The Java 25 requirement is the main open risk.
depends_on "openjdk"tracks Homebrew's current default JDK, which is a moving target, and versioned formulae (openjdk@21,openjdk@17) only exist for some versions.> TODO(verify): whether Homebrew's openjdk formula is currently at 25 or later, and whether an openjdk@25 exists. If neither, the choice is to relax the baseline for the Homebrew build, ship a jlink image as a cask, or wait. - Completion scripts install via
bash_completion.install/zsh_completion.install/fish_completion.install, which is whypeppol completion <shell>should be able to write them at build time. - Both
arm64andx86_64macOS must work. A pure-Java formula is architecture-independent, which is one real advantage of not going native first.
Step 2 — homebrew-core (later, optional). Getting into homebrew-core means brew install peppol-cli with no
tap, but the bar is much higher:
-
Notability. homebrew-core declines formulae for projects without a track record; the documented guideline
is a threshold of GitHub stars, forks and watchers.
> TODO(verify): the current thresholds in Homebrew's "Acceptable Formulae" document — they have changed over time and should not be quoted from memory. - A stable, versioned release (not a snapshot, not a git HEAD build), with a stable archive URL.
- An open source licence Homebrew accepts — Apache 2.0 qualifies.
- No duplication of an existing formula, and it must not be trivially replaceable by a one-line script.
- It must pass
brew audit --strict --online --newand the CI build (brew test-bot), which includes a meaningfultest doblock. A good test here: runpeppol pid validateon a known-good identifier and assert the output, since that needs no network. -
Maintenance obligation. Once in core, every release should be followed by a
brew bump-formula-pr. That is an ongoing commitment, which is the real reason to start with a tap.
Practical sequence: ship releases → publish the tap → let usage accumulate → only then consider core.
The full plan — six test tiers from hermetic unit tests to manually-approved live write tests, fixture discipline, and the golden-file tests that make the JSON contract enforceable — is in Testing Strategy. It is planned as part of development from M1, not added afterwards.
Two architectural consequences worth stating here, because they constrain the module split above:
-
peppol-cli-coremust be usable without a command line and without the network, or T1 and T2 are impossible. That is the main justification for keeping it separate frompeppol-cli-commands. - The stubs (SMP, DNS resolver, Phorm) belong in a shared test-support module, so every tier can reuse them.
Several features in the Feature Catalogue need capabilities the libraries do not currently have — a GLN check-digit validator, a Directory search client, a reusable Phorm client, runtime code list access for diffing, and per-user config discovery. They are catalogued with their verified evidence and their proper home in Library Gaps. Most belong upstream rather than in this project.
peppol-cli — licensed under the Apache License, Version 2.0.
Not an official OpenPeppol tool. This is an independent open source project and is neither endorsed by nor affiliated with OpenPeppol AISBL. "Peppol" is a trademark of OpenPeppol AISBL.
Status: planning
Overview
Design
Decisions
Reference