From de5a36739125376bd5b88a4628649337ce426770 Mon Sep 17 00:00:00 2001 From: Karl Kauc Date: Fri, 15 May 2026 15:22:55 +0200 Subject: [PATCH 1/3] Phase 1 foundation: multi-version samples, schema fetcher, repo index - Restructure FundsXML_Files into // layout - Add 4.2.9 transactions/documents/regulatory(EFT)/signed samples - Add 4.1.0 and 4.0.0 positions samples (4.0.0 has no ControlData/Version) - All 7 samples XSD-valid against official release schema - tools/fetch-schema.sh: proxy-aware fetcher (handles GitHub 302 + relative xmldsig-core-schema.xsd import for 4.2.9+) - Per-example READMEs with version badge + validated-against URL - Top README use-case index + structure tree; EXAMPLE_README_TEMPLATE - All content in English Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/EXAMPLE_README_TEMPLATE.md | 48 +++++ .gitignore | 11 ++ .../4.0.0/positions/Equity-Fund_Positions.xml | 164 ++++++++++++++++ FundsXML_Files/4.0.0/positions/README.md | 33 ++++ .../4.1.0/positions/Equity-Fund_Positions.xml | 164 ++++++++++++++++ FundsXML_Files/4.1.0/positions/README.md | 30 +++ .../4.2.9/documents/Fund_Documents.xml | 78 ++++++++ FundsXML_Files/4.2.9/documents/README.md | 30 +++ .../{ => positions}/Mixed-Fund_Positions.xml | 0 .../4.2.9/{ => positions}/README.md | 0 .../4.2.9/regulatory/EFT_Regulatory.xml | 55 ++++++ FundsXML_Files/4.2.9/regulatory/README.md | 34 ++++ FundsXML_Files/4.2.9/signed/README.md | 31 +++ .../4.2.9/signed/Signed_Fund_Skeleton.xml | 65 +++++++ .../4.2.9/transactions/Fund_Transactions.xml | 181 ++++++++++++++++++ FundsXML_Files/4.2.9/transactions/README.md | 35 ++++ FundsXML_Files/README.md | 41 +++- README.md | 68 ++++--- tools/fetch-schema.sh | 58 ++++++ 19 files changed, 1098 insertions(+), 28 deletions(-) create mode 100644 .github/EXAMPLE_README_TEMPLATE.md create mode 100644 FundsXML_Files/4.0.0/positions/Equity-Fund_Positions.xml create mode 100644 FundsXML_Files/4.0.0/positions/README.md create mode 100644 FundsXML_Files/4.1.0/positions/Equity-Fund_Positions.xml create mode 100644 FundsXML_Files/4.1.0/positions/README.md create mode 100644 FundsXML_Files/4.2.9/documents/Fund_Documents.xml create mode 100644 FundsXML_Files/4.2.9/documents/README.md rename FundsXML_Files/4.2.9/{ => positions}/Mixed-Fund_Positions.xml (100%) rename FundsXML_Files/4.2.9/{ => positions}/README.md (100%) create mode 100644 FundsXML_Files/4.2.9/regulatory/EFT_Regulatory.xml create mode 100644 FundsXML_Files/4.2.9/regulatory/README.md create mode 100644 FundsXML_Files/4.2.9/signed/README.md create mode 100644 FundsXML_Files/4.2.9/signed/Signed_Fund_Skeleton.xml create mode 100644 FundsXML_Files/4.2.9/transactions/Fund_Transactions.xml create mode 100644 FundsXML_Files/4.2.9/transactions/README.md create mode 100755 tools/fetch-schema.sh diff --git a/.github/EXAMPLE_README_TEMPLATE.md b/.github/EXAMPLE_README_TEMPLATE.md new file mode 100644 index 0000000..0622345 --- /dev/null +++ b/.github/EXAMPLE_README_TEMPLATE.md @@ -0,0 +1,48 @@ + +# — + +![Version](https://img.shields.io/badge/FundsXML--blue) ![status](https://img.shields.io/badge/status-runnable-brightgreen) + +| Property | Value | +|----------|-------| +| **What** | One sentence: what this example demonstrates | +| **FundsXML version(s)** | e.g. 4.2.9 (works on 4.1.0/4.0.0 too — note differences) | +| **Validated against** | `https://github.com/fundsxml/schema/releases/download//FundsXML.xsd` | +| **Stacks** | CLI / Python / Java / .NET / PowerShell (list what is provided) | + +## Purpose + +Why this exists and when an enterprise integrator would use it. + +## Prerequisites + +- Tooling/runtime versions +- `tools/fetch-schema.sh ` if schema is needed +- Network/proxy note when the official schema URL must be reached + +## Run + +Provide a runnable command per applicable stack: + +```bash +# CLI +... +``` +```bash +# Python +... +``` + + +## Expected output + +What a correct run produces (and what failure looks like). + +## Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| ... | ... | ... | diff --git a/.gitignore b/.gitignore index ceb2b98..495c548 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,12 @@ CLAUDE.md + +# Locally fetched XSD releases (via tools/fetch-schema.sh) — not committed, +# the source of truth is the official GitHub release. +.schema-cache/ + +# Generated reports / outputs +*.report.html +report.html +full_report.html +validation_report.xml +out/ diff --git a/FundsXML_Files/4.0.0/positions/Equity-Fund_Positions.xml b/FundsXML_Files/4.0.0/positions/Equity-Fund_Positions.xml new file mode 100644 index 0000000..b5cd732 --- /dev/null +++ b/FundsXML_Files/4.0.0/positions/Equity-Fund_Positions.xml @@ -0,0 +1,164 @@ + + + + + FUNDSXML_400_POS_1 + 2024-06-28T00:00:00 + 2024-06-28 + + AT + EURAM + Erste Asset Management GmbH + Asset Manager + + INITIAL + + + + + 529900T8BM49AURSDO55 + + + Erste Equity Europe Classic + + EUR + true + + + + 2024-06-28 + OFFICIAL + + 40000000 + + + + + + 2024-06-28 + + + ID_001 + + NL0010273215 + + EUR + + 16000000 + + 40.00 + + 17000.00 + + 941.18 + + + + + ID_002 + + FR0000120578 + + EUR + + 14000000 + + 35.00 + + 140000.00 + + 100.00 + + + + + ID_003 + + DE0007164600 + + EUR + + 10000000 + + 25.00 + + 62500.00 + + 160.00 + + + + + + + + + + + + ID_001 + + NL0010273215 + + EUR + NL + ASML HOLDING N.V. + EQ + + + + + 724500Y6P2B0MRLY2T56 + + ASML HOLDING N.V. + + + + + + ID_002 + + FR0000120578 + + EUR + FR + SANOFI S.A. + EQ + + + + + 549300E9PC51EN656011 + + SANOFI S.A. + + + + + + ID_003 + + DE0007164600 + + EUR + DE + SAP SE + EQ + + + + + 529900D6BF99LW9R2E68 + + SAP SE + + + + + + diff --git a/FundsXML_Files/4.0.0/positions/README.md b/FundsXML_Files/4.0.0/positions/README.md new file mode 100644 index 0000000..a9ec1e1 --- /dev/null +++ b/FundsXML_Files/4.0.0/positions/README.md @@ -0,0 +1,33 @@ +# FundsXML 4.0.0 — Equity-Fund Positions Example + +![Version](https://img.shields.io/badge/FundsXML-4.0.0-blue) ![validated](https://img.shields.io/badge/XSD-valid-brightgreen) + +| Property | Value | +|----------|-------| +| **File** | `Equity-Fund_Positions.xml` | +| **Schema version** | FundsXML 4.0.0 | +| **Validated against** | `https://github.com/fundsxml/schema/releases/download/4.0.0/FundsXML.xsd` | +| **Purpose** | Oldest supported release, version comparison | + +## Contents + +Content-identical to the 4.1.0 example (3 equity positions), but adapted to the +4.0.0 schema rules. + +## Version differences (important!) + +- ❗ **4.0.0 `ControlData` has NO `` element** — that was introduced in + 4.1.0. The version here is only visible via `xsi:noNamespaceSchemaLocation` + and the header comment. An inserted `` would break validation + (`Element 'Version': This element is not expected`). +- The 4.0.0 release ships a self-contained `FundsXML.xsd`. +- Despite header differences, the positions/asset structure is stable across + versions (backward compatibility). + +## Validation + +```bash +tools/fetch-schema.sh 4.0.0 +xmllint --noout --schema .schema-cache/4.0.0/FundsXML.xsd \ + FundsXML_Files/4.0.0/positions/Equity-Fund_Positions.xml +``` diff --git a/FundsXML_Files/4.1.0/positions/Equity-Fund_Positions.xml b/FundsXML_Files/4.1.0/positions/Equity-Fund_Positions.xml new file mode 100644 index 0000000..509ded1 --- /dev/null +++ b/FundsXML_Files/4.1.0/positions/Equity-Fund_Positions.xml @@ -0,0 +1,164 @@ + + + + + FUNDSXML_410_POS_1 + 2024-06-28T00:00:00 + 4.1.0 + 2024-06-28 + + AT + EURAM + Erste Asset Management GmbH + Asset Manager + + INITIAL + + + + + 529900T8BM49AURSDO55 + + + Erste Equity Europe Core + + EUR + true + + + + 2024-06-28 + OFFICIAL + + 40000000 + + + + + + 2024-06-28 + + + ID_001 + + NL0010273215 + + EUR + + 16000000 + + 40.00 + + 17000.00 + + 941.18 + + + + + ID_002 + + FR0000120578 + + EUR + + 14000000 + + 35.00 + + 140000.00 + + 100.00 + + + + + ID_003 + + DE0007164600 + + EUR + + 10000000 + + 25.00 + + 62500.00 + + 160.00 + + + + + + + + + + + + ID_001 + + NL0010273215 + + EUR + NL + ASML HOLDING N.V. + EQ + + + + + 724500Y6P2B0MRLY2T56 + + ASML HOLDING N.V. + + + + + + ID_002 + + FR0000120578 + + EUR + FR + SANOFI S.A. + EQ + + + + + 549300E9PC51EN656011 + + SANOFI S.A. + + + + + + ID_003 + + DE0007164600 + + EUR + DE + SAP SE + EQ + + + + + 529900D6BF99LW9R2E68 + + SAP SE + + + + + + diff --git a/FundsXML_Files/4.1.0/positions/README.md b/FundsXML_Files/4.1.0/positions/README.md new file mode 100644 index 0000000..de603a8 --- /dev/null +++ b/FundsXML_Files/4.1.0/positions/README.md @@ -0,0 +1,30 @@ +# FundsXML 4.1.0 — Equity-Fund Positions Example + +![Version](https://img.shields.io/badge/FundsXML-4.1.0-blue) ![validated](https://img.shields.io/badge/XSD-valid-brightgreen) + +| Property | Value | +|----------|-------| +| **File** | `Equity-Fund_Positions.xml` | +| **Schema version** | FundsXML 4.1.0 | +| **Validated against** | `https://github.com/fundsxml/schema/releases/download/4.1.0/FundsXML.xsd` | +| **Purpose** | Compact positions example on an older, still-valid version | + +## Contents + +Pure equity fund with 3 positions (ASML, Sanofi, SAP), summing to 100 %, +EUR 40m NAV. Deliberately small to make the version comparison easy. + +## Version differences + +- **4.1.0** has — like 4.2.9 — a `ControlData/Version` element. +- The **4.1.0 release** ships only a self-contained `FundsXML.xsd` + (no separate `xmldsig-core-schema.xsd` import as from 4.2.9). +- FundsXML is backward compatible: this structure also validates against 4.2.9. + +## Validation + +```bash +tools/fetch-schema.sh 4.1.0 +xmllint --noout --schema .schema-cache/4.1.0/FundsXML.xsd \ + FundsXML_Files/4.1.0/positions/Equity-Fund_Positions.xml +``` diff --git a/FundsXML_Files/4.2.9/documents/Fund_Documents.xml b/FundsXML_Files/4.2.9/documents/Fund_Documents.xml new file mode 100644 index 0000000..d9c2322 --- /dev/null +++ b/FundsXML_Files/4.2.9/documents/Fund_Documents.xml @@ -0,0 +1,78 @@ + + + + + FUNDSXML_429_DOC_1 + 2025-10-02T00:00:00 + 4.2.9 + 2025-10-01 + + AT + EURAM + Erste Asset Management GmbH + Asset Manager + + INITIAL + + + + + Factsheet + + 2025-09 + de + + AT + DE + + + + 529900T8BM49AURSDO55 + + Erste Responsible Stock Global + EUR + + Factsheet Erste Responsible Stock Global 09/2025 + factsheet_errsg_202509_de.pdf + true + application/pdf + 2025-09-30 + + AT + EURAM + Erste Asset Management GmbH + Asset Manager + + https://www.erste-am.at/factsheets/errsg_202509_de.pdf + + + + PRIIPS-KID + + 3 + en + + AT + + + + 529900T8BM49AURSDO55 + + Erste Responsible Stock Global + EUR + + PRIIPS KID EUR R01 + priips_kid_AT0000A2QM66_en.pdf + true + application/pdf + 2025-01-02 + 2025-12-31 + 20 + JVBERi0xLjQKJSBQUklJUFMgS0lEIHN0dWIK + + + diff --git a/FundsXML_Files/4.2.9/documents/README.md b/FundsXML_Files/4.2.9/documents/README.md new file mode 100644 index 0000000..c548eac --- /dev/null +++ b/FundsXML_Files/4.2.9/documents/README.md @@ -0,0 +1,30 @@ +# FundsXML 4.2.9 — Documents Example + +![Version](https://img.shields.io/badge/FundsXML-4.2.9-blue) ![validated](https://img.shields.io/badge/XSD-valid-brightgreen) + +| Property | Value | +|----------|-------| +| **File** | `Fund_Documents.xml` | +| **Schema version** | FundsXML 4.2.9 | +| **Validated against** | `https://github.com/fundsxml/schema/releases/download/4.2.9/FundsXML.xsd` | +| **Purpose** | Reference or embed fund documents | + +## Contents + +Top-level `` with two `Document` entries: + +| Type (ListedType) | Delivery | Note | +|-------------------|----------|------| +| `Factsheet` | `DocumentURL` (link) | public PDF, linked | +| `PRIIPS-KID` | `BinaryData` (base64) | embedded PDF stub, with `ExpirationDate` | + +Required fields per document: `Type`, `Language`, `Format`. Link to the fund +via `Document/Fund/Identifiers/LEI`. + +## Validation + +```bash +tools/fetch-schema.sh 4.2.9 +xmllint --noout --schema .schema-cache/4.2.9/FundsXML.xsd \ + FundsXML_Files/4.2.9/documents/Fund_Documents.xml +``` diff --git a/FundsXML_Files/4.2.9/Mixed-Fund_Positions.xml b/FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml similarity index 100% rename from FundsXML_Files/4.2.9/Mixed-Fund_Positions.xml rename to FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml diff --git a/FundsXML_Files/4.2.9/README.md b/FundsXML_Files/4.2.9/positions/README.md similarity index 100% rename from FundsXML_Files/4.2.9/README.md rename to FundsXML_Files/4.2.9/positions/README.md diff --git a/FundsXML_Files/4.2.9/regulatory/EFT_Regulatory.xml b/FundsXML_Files/4.2.9/regulatory/EFT_Regulatory.xml new file mode 100644 index 0000000..88bcf32 --- /dev/null +++ b/FundsXML_Files/4.2.9/regulatory/EFT_Regulatory.xml @@ -0,0 +1,55 @@ + + + + + FUNDSXML_429_REG_1 + 2025-10-02T00:00:00 + 4.2.9 + 2025-10-01 + + AT + EURAM + Erste Asset Management GmbH + Asset Manager + + INITIAL + + + + + + + + V1 + 2025-10-02T00:00:00 + 2025-07-01 + 2025-09-30 + M + + + Erste Asset Management GmbH + 529900T8BM49AURSDO55 + L + M + + + Erste Asset Management GmbH + 529900T8BM49AURSDO55 + L + + + + AT0000A2QM66 + 1 + Erste Responsible Stock Global EUR R01 + 1284 + + + + + + diff --git a/FundsXML_Files/4.2.9/regulatory/README.md b/FundsXML_Files/4.2.9/regulatory/README.md new file mode 100644 index 0000000..fcb3f67 --- /dev/null +++ b/FundsXML_Files/4.2.9/regulatory/README.md @@ -0,0 +1,34 @@ +# FundsXML 4.2.9 — Regulatory Reporting Example (EFT) + +![Version](https://img.shields.io/badge/FundsXML-4.2.9-blue) ![validated](https://img.shields.io/badge/XSD-valid-brightgreen) + +| Property | Value | +|----------|-------| +| **File** | `EFT_Regulatory.xml` | +| **Schema version** | FundsXML 4.2.9 | +| **Validated against** | `https://github.com/fundsxml/schema/releases/download/4.2.9/FundsXML.xsd` | +| **Purpose** | Supervisory reporting via `RegulatoryReportings` | + +## Contents + +`RegulatoryReportings/DirectReporting/EFTs/EFT` (European Feeder/Flow Template). +Deliberately chosen because it is the **most compact** of the regulatory +FundsXML structures (EMT/EET/PRIIPS/TPT are considerably larger and will follow +as their own examples in later phases). + +Mandatory blocks included: + +- `DataSetInformation` → `ReportInformationAndScope` (version, generation/period + dates, `ReferenceTargetMarket`) +- `SubmitterEntityInformation` and `ManufacturerEntityInformation` + (name, identifier, identifier type, position in the distribution chain) +- `GeneralFinancialInstrumentInformation` (identification, name, + `TotalNumberOfTransactions`) + +## Validation + +```bash +tools/fetch-schema.sh 4.2.9 +xmllint --noout --schema .schema-cache/4.2.9/FundsXML.xsd \ + FundsXML_Files/4.2.9/regulatory/EFT_Regulatory.xml +``` diff --git a/FundsXML_Files/4.2.9/signed/README.md b/FundsXML_Files/4.2.9/signed/README.md new file mode 100644 index 0000000..730ec97 --- /dev/null +++ b/FundsXML_Files/4.2.9/signed/README.md @@ -0,0 +1,31 @@ +# FundsXML 4.2.9 — Signed File (Skeleton) + +![Version](https://img.shields.io/badge/FundsXML-4.2.9-blue) ![validated](https://img.shields.io/badge/XSD-valid-brightgreen) ![Signature](https://img.shields.io/badge/XMLDSig-placeholder-orange) + +| Property | Value | +|----------|-------| +| **File** | `Signed_Fund_Skeleton.xml` | +| **Schema version** | FundsXML 4.2.9 | +| **Validated against** | `https://github.com/fundsxml/schema/releases/download/4.2.9/FundsXML.xsd` | +| **Purpose** | Structure of an enveloped-XMLDSig-signed FundsXML file | + +## Contents + +`ds:Signature` (namespace `http://www.w3.org/2000/09/xmldsig#`) is the **last +optional child** of ``. From release 4.2.9 on, `FundsXML.xsd` imports +`xmldsig-core-schema.xsd` for this (see `tools/fetch-schema.sh`). + +> ⚠️ **Placeholder:** `DigestValue` and `SignatureValue` are schema-valid base64 +> strings but **not cryptographically verifiable**. Real signing and +> verification (Apache Santuario / .NET `SignedXml` / `xmlsec1` / Python +> `signxml`) follows in **Phase 3** under `XML_Signature/`. + +Algorithms used (enveloped signature): C14N 2001-03-15, RSA-SHA256, SHA-256. + +## Validation + +```bash +tools/fetch-schema.sh 4.2.9 # also fetches xmldsig-core-schema.xsd +xmllint --noout --schema .schema-cache/4.2.9/FundsXML.xsd \ + FundsXML_Files/4.2.9/signed/Signed_Fund_Skeleton.xml +``` diff --git a/FundsXML_Files/4.2.9/signed/Signed_Fund_Skeleton.xml b/FundsXML_Files/4.2.9/signed/Signed_Fund_Skeleton.xml new file mode 100644 index 0000000..6f74a97 --- /dev/null +++ b/FundsXML_Files/4.2.9/signed/Signed_Fund_Skeleton.xml @@ -0,0 +1,65 @@ + + + + + FUNDSXML_429_SIGNED_1 + 2025-10-02T00:00:00 + 4.2.9 + 2025-10-01 + + AT + EURAM + Erste Asset Management GmbH + Asset Manager + + INITIAL + + + + + 529900T8BM49AURSDO55 + + + Erste Responsible Stock Global + + EUR + true + + + + 2025-10-01 + OFFICIAL + + 125000000 + + + + + + + + + + + + + + + + UExBQ0VIT0xERVItRElHRVNU + + + UExBQ0VIT0xERVItU0lHTkFUVVJFLVZBTFVFLU5PVC1DUllQVE9HUkFQSElDQUxMWS1WQUxJRA== + + erste-am-funds-signing-2025 + + + diff --git a/FundsXML_Files/4.2.9/transactions/Fund_Transactions.xml b/FundsXML_Files/4.2.9/transactions/Fund_Transactions.xml new file mode 100644 index 0000000..cca1daa --- /dev/null +++ b/FundsXML_Files/4.2.9/transactions/Fund_Transactions.xml @@ -0,0 +1,181 @@ + + + + + FUNDSXML_429_TXN_1 + 2025-10-02T00:00:00 + 4.2.9 + 2025-10-01 + + AT + EURAM + Erste Asset Management GmbH + Asset Manager + + INITIAL + + + + + 529900T8BM49AURSDO55 + + + Erste Responsible Stock Global + + EUR + true + + + + 2025-10-01 + OFFICIAL + + 17500000 + + + + + + 2025-10-01 + + + ID_001 + + US0378331005 + + USD + + 9375000 + + 53.57 + + 50000.00 + + 202.50 + + + + + ID_002 + + NL0010273215 + + EUR + + 8125000 + + 46.43 + + 8750.00 + + 928.57 + + + + + + + TXN_2025_0001 + ID_001 + + US0378331005 + + USD + BUY + EUR + 2025-09-29 + 2025-10-01 + 10000.00 + + 200.00 + + + 1851000 + + Purchase Apple Inc. + + + TXN_2025_0002 + ID_002 + + NL0010273215 + + EUR + SELL + EUR + 2025-09-30 + 2025-10-02 + 1250.00 + + 930.00 + + + 1162500 + + Partial sale ASML Holding + + + TXN_2025_0003 + EUR + CASH + 2025-10-01 + 2025-10-01 + 500000.00 + + 500000 + + Subscription inflow + + + + + + + + + + ID_001 + + US0378331005 + + USD + US + APPLE INC. + EQ + + + + + HWUPKR0MPOU8FGXBT394 + + APPLE INC. + + + + + + ID_002 + + NL0010273215 + + EUR + NL + ASML HOLDING N.V. + EQ + + + + + 724500Y6P2B0MRLY2T56 + + ASML HOLDING N.V. + + + + + + diff --git a/FundsXML_Files/4.2.9/transactions/README.md b/FundsXML_Files/4.2.9/transactions/README.md new file mode 100644 index 0000000..7408c5c --- /dev/null +++ b/FundsXML_Files/4.2.9/transactions/README.md @@ -0,0 +1,35 @@ +# FundsXML 4.2.9 — Transactions Example + +![Version](https://img.shields.io/badge/FundsXML-4.2.9-blue) ![validated](https://img.shields.io/badge/XSD-valid-brightgreen) + +| Property | Value | +|----------|-------| +| **File** | `Fund_Transactions.xml` | +| **Schema version** | FundsXML 4.2.9 | +| **Validated against** | `https://github.com/fundsxml/schema/releases/download/4.2.9/FundsXML.xsd` | +| **Purpose** | Demonstrate portfolio transactions (buys/sells/cash) | + +## Contents + +`FundDynamicData/Portfolios/Portfolio/Transactions` with three `Transaction` +records: + +| TransactionID | Kind | Asset (IDREF) | Nominal/Units | Description | +|---------------|------|---------------|---------------|-------------| +| TXN_2025_0001 | BUY | ID_001 (Apple) | 10,000 | Equity purchase | +| TXN_2025_0002 | SELL | ID_002 (ASML) | 1,250 | Partial equity sale | +| TXN_2025_0003 | CASH | – | 500,000 | Cash inflow (subscription) | + +`AssetUniqueID` is an `xs:IDREF` referencing `Asset/UniqueID` in +`AssetMasterData` — the same linking mechanism used by positions. + +## Validation + +```bash +tools/fetch-schema.sh 4.2.9 +xmllint --noout --schema .schema-cache/4.2.9/FundsXML.xsd \ + FundsXML_Files/4.2.9/transactions/Fund_Transactions.xml +``` + +See [`XSD_Validation/`](../../../XSD_Validation/) for invocations in Python, +Java, .NET, PowerShell and CLI. diff --git a/FundsXML_Files/README.md b/FundsXML_Files/README.md index 36197df..d1860d4 100644 --- a/FundsXML_Files/README.md +++ b/FundsXML_Files/README.md @@ -116,15 +116,30 @@ FundsXML documents should be validated against the official XSD schema: ### Download Schema +Validation always targets the **official release** of the schema: + +``` +https://github.com/fundsxml/schema/releases/download//FundsXML.xsd +``` + +Two enterprise-relevant caveats are handled by the helper `tools/fetch-schema.sh`: + +1. That URL returns an HTTP 302 redirect; simple HTTP clients (libxml2 / + xmllint) do not follow it, so the schema must be fetched first (curl honours + `https_proxy`/`HTTPS_PROXY` for locked-down networks). +2. From release 4.2.9 on, `FundsXML.xsd` imports `xmldsig-core-schema.xsd` via a + relative path — both files must sit in the same directory. + ```bash -# Get the official schema for version 4.2.9 -curl -O https://github.com/fundsxml/schema/releases/download/4.2.9/FundsXML.xsd +# Fetches FundsXML.xsd (+ xmldsig-core-schema.xsd when needed) into .schema-cache// +tools/fetch-schema.sh 4.2.9 ``` ### Validate with xmllint (macOS/Linux) ```bash -xmllint --schema FundsXML.xsd FundsXML_Files/4.2.9/Mixed-Fund_Positions.xml --noout +xmllint --noout --schema .schema-cache/4.2.9/FundsXML.xsd \ + FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml ``` ### Validate with Saxon @@ -194,9 +209,23 @@ Console.WriteLine("Validation complete"); ## Available Samples -| Version | File | Description | -|---------|------|-------------| -| [4.2.9](./4.2.9/) | Mixed-Fund_Positions.xml | Comprehensive example with 21 diverse positions | +Samples are organized by **version** and **use case**: `FundsXML_Files///`. +Each leaf directory has its own README with a version badge and the exact +schema URL it was validated against. + +| Version | Use case | File | Description | +|---------|----------|------|-------------| +| [4.2.9](./4.2.9/positions/) | positions | `Mixed-Fund_Positions.xml` | Comprehensive, 21 diverse positions, 13 asset types | +| [4.2.9](./4.2.9/transactions/) | transactions | `Fund_Transactions.xml` | BUY/SELL/CASH, `AssetUniqueID` IDREF linking | +| [4.2.9](./4.2.9/documents/) | documents | `Fund_Documents.xml` | Factsheet (URL) + PRIIPS-KID (embedded base64) | +| [4.2.9](./4.2.9/regulatory/) | regulatory | `EFT_Regulatory.xml` | `RegulatoryReportings/DirectReporting/EFTs` | +| [4.2.9](./4.2.9/signed/) | signed | `Signed_Fund_Skeleton.xml` | Enveloped `ds:Signature` (placeholder, Phase 3) | +| [4.1.0](./4.1.0/positions/) | positions | `Equity-Fund_Positions.xml` | Compact equity fund, older valid version | +| [4.0.0](./4.0.0/positions/) | positions | `Equity-Fund_Positions.xml` | Oldest release — **no `ControlData/Version`** | + +> **Version visibility:** From 4.1.0 on, every file carries `ControlData/Version`. +> For **4.0.0** that element does not exist — the version there is only +> recognizable via `xsi:noNamespaceSchemaLocation` and the header comment. ## External Resources diff --git a/README.md b/README.md index 1ec7be0..ec8bc6e 100644 --- a/README.md +++ b/README.md @@ -18,33 +18,57 @@ FundsXML is an industry-standard XML format for exchanging fund and investment d ## What This Repository Provides -| Component | Description | Location | -|-----------|-------------|----------| -| Sample Files | FundsXML example documents with diverse asset types | [FundsXML_Files/](./FundsXML_Files/) | -| Schematron Rules | Business rule validation using ISO Schematron | [Schematron_DataQuality_Checks/](./Schematron_DataQuality_Checks/) | -| XSLT Reports | HTML data quality report generators | [XSLT_DataQuality_Checks/](./XSLT_DataQuality_Checks/) | +This repository is being grown into a comprehensive **enterprise FundsXML +reference**. The table below maps use cases to technologies and example +locations. Items marked _(planned)_ are on the roadmap (see +`.claude/plans/` / project plan). + +| Use case | Technology | Location | Status | +|----------|-----------|----------|--------| +| Sample data (positions, transactions, documents, regulatory, signed) | XML, 3 versions | [FundsXML_Files/](./FundsXML_Files/) | ✅ | +| XSD validation | CLI, Python, Java, .NET, PowerShell | [XSD_Validation/](./XSD_Validation/) | ✅ | +| Schematron business rules | ISO Schematron + SchXslt | [Schematron_DataQuality_Checks/](./Schematron_DataQuality_Checks/) | ✅ | +| Schematron invocation | CLI, Python, Java, .NET | [Schematron_DataQuality_Checks/Basic_Checks/invocation/](./Schematron_DataQuality_Checks/Basic_Checks/) | ✅ | +| Data-quality reports | XSLT 1.0 / 2.0 | [XSLT_DataQuality_Checks/](./XSLT_DataQuality_Checks/) | ✅ | +| Company-internal DQ rules | XSLT 2.0 | [XSLT_DataQuality_Checks/Custom_Internal_Checks/](./XSLT_DataQuality_Checks/) | ✅ | +| Factsheet (HTML/PDF) & CSV export | XSLT, XSL-FO/FOP | [XSLT_Transformations/](./XSLT_Transformations/) | ✅ | +| Transformation invocation | CLI, Python, Java, .NET, Node | [XSLT_Transformations/invocation/](./XSLT_Transformations/) | ✅ | +| Schema fetch (proxy-aware) | Bash | [tools/fetch-schema.sh](./tools/fetch-schema.sh) | ✅ | +| CI (validate all samples) | GitHub Actions | [.github/workflows/ci.yml](./.github/workflows/) | ✅ | +| XQuery examples | BaseX/Saxon, Python, Java, .NET | `XQuery_Examples/` | _(planned)_ | +| XML signature sign/verify | Apache Santuario, .NET, xmlsec1, signxml | `XML_Signature/` | _(planned)_ | +| Database load ↔ generate | Oracle/SQL Server/Postgres (code only) | `Database_Integration/` | _(planned)_ | +| Large-file/stream processing | StAX/SAX/lxml iterparse | `Large_File_Processing/` | _(planned)_ | ## Repository Structure ``` -FundsXML-Examples/ -├── README.md # This file +fundsxml_examples/ +├── README.md # This file (index above) ├── LICENSE # Apache 2.0 +├── tools/fetch-schema.sh # Proxy-aware official-XSD fetcher │ -├── FundsXML_Files/ # Sample FundsXML documents -│ └── 4.2.9/ -│ └── Mixed-Fund_Positions.xml # Comprehensive example with 21 positions +├── FundsXML_Files/ # Sample documents, per version & use-case +│ ├── 4.2.9/{positions,transactions,documents,regulatory,signed}/ +│ ├── 4.1.0/positions/ +│ └── 4.0.0/positions/ │ -├── Schematron_DataQuality_Checks/ # Schematron validation rules -│ └── Basic_Checks/ -│ └── basic_checks.sch # 7 validation patterns, 40+ rules +├── XSD_Validation/ # Validation per stack +│ └── {cli,python,java,dotnet,powershell}/ │ -└── XSLT_DataQuality_Checks/ # XSLT transformation stylesheets - ├── Basic_Checks/ - │ └── basic_checks.xslt # XSLT 2.0 - 5 check sections - └── Enhanced_Check/ - ├── FundsXML_CompleteDQReport_HTML.xsl # XSLT 1.0 - 10-section dashboard - └── FundsXML Complete Data Quality Report.pdf # Sample output +├── Schematron_DataQuality_Checks/Basic_Checks/ +│ ├── basic_checks.sch # 7 patterns, 40+ rules +│ └── invocation/ # CLI, Python, Java, .NET +│ +├── XSLT_DataQuality_Checks/ +│ ├── Basic_Checks/ Enhanced_Check/ # existing reports +│ └── Custom_Internal_Checks/ # company-internal DQ rules +│ +├── XSLT_Transformations/ # Factsheet (HTML/PDF), CSV export +│ └── {Factsheet,CSV_Export,invocation}/ +│ +├── tests/fixtures/invalid/ # Deliberately broken negative fixtures +└── .github/workflows/ci.yml # XSD + Schematron over all samples ``` ## Quick Start @@ -63,7 +87,7 @@ sudo apt install libsaxonhe-java choco install saxonhe # Generate a data quality report -saxon -s:FundsXML_Files/4.2.9/Mixed-Fund_Positions.xml \ +saxon -s:FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml \ -xsl:XSLT_DataQuality_Checks/Basic_Checks/basic_checks.xslt \ -o:report.html ``` @@ -76,7 +100,7 @@ sudo apt install xsltproc # Generate enhanced report (XSLT 1.0 compatible) xsltproc XSLT_DataQuality_Checks/Enhanced_Check/FundsXML_CompleteDQReport_HTML.xsl \ - FundsXML_Files/4.2.9/Mixed-Fund_Positions.xml > report.html + FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml > report.html ``` ### Option 3: Python @@ -91,7 +115,7 @@ python -c " from lxml import etree xslt = etree.parse('XSLT_DataQuality_Checks/Enhanced_Check/FundsXML_CompleteDQReport_HTML.xsl') transform = etree.XSLT(xslt) -doc = etree.parse('FundsXML_Files/4.2.9/Mixed-Fund_Positions.xml') +doc = etree.parse('FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml') result = transform(doc) with open('report.html', 'wb') as f: f.write(etree.tostring(result, pretty_print=True)) diff --git a/tools/fetch-schema.sh b/tools/fetch-schema.sh new file mode 100755 index 0000000..c70fc9f --- /dev/null +++ b/tools/fetch-schema.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# fetch-schema.sh — Fetch the official FundsXML XSD release from GitHub. +# +# WHY THIS SCRIPT EXISTS +# ---------------------- +# Validation is done against the canonical schema URL: +# +# https://github.com/fundsxml/schema/releases/download//FundsXML.xsd +# +# That URL returns HTTP 302 to objects.githubusercontent.com. Processors with a +# simple HTTP client (e.g. libxml2 / xmllint) do NOT follow redirects and fail. +# In addition, from release 4.2.9 onward `FundsXML.xsd` imports +# `xmldsig-core-schema.xsd` via a RELATIVE path — both files must therefore sit +# in the same directory, otherwise XSD compilation fails +# ("{http://www.w3.org/2000/09/xmldsig#}Signature does not resolve"). +# +# Enterprise reality: on locked-down networks the download goes through an HTTP +# proxy (curl honours https_proxy / HTTPS_PROXY). No hand-maintained XML catalog +# is used — the source of truth stays the official release. +# +# Usage: +# tools/fetch-schema.sh [target-dir] +# tools/fetch-schema.sh 4.2.9 +# tools/fetch-schema.sh 4.1.0 /tmp/xsd +# +# Output: path to the local FundsXML.xsd on stdout (usable by scripts). +set -euo pipefail + +VERSION="${1:?Usage: fetch-schema.sh [target-dir]}" +TARGET="${2:-.schema-cache/${VERSION}}" +BASE="https://github.com/fundsxml/schema/releases/download/${VERSION}" + +mkdir -p "$TARGET" + +fetch() { + local name="$1" + local out="${TARGET}/${name}" + if [[ -s "$out" ]]; then + echo "cached: $out" >&2 + return 0 + fi + echo "fetch: ${BASE}/${name} -> $out" >&2 + # -L follows the GitHub redirect; --fail aborts on HTTP error. + curl -sSL --fail -m 60 "${BASE}/${name}" -o "$out" +} + +fetch "FundsXML.xsd" + +# From 4.2.9 on, FundsXML.xsd imports xmldsig-core-schema.xsd via a relative +# path — without that file the schema does not compile +# ("{http://www.w3.org/2000/09/xmldsig#}Signature does not resolve"). +# Older releases (4.1.0, 4.0.0) are self-contained and do not reference it. +# Fetch the sibling only when it is actually imported. +if grep -q 'xmldsig-core-schema\.xsd' "${TARGET}/FundsXML.xsd"; then + fetch "xmldsig-core-schema.xsd" +fi + +echo "${TARGET}/FundsXML.xsd" From dbb0ca48e71717d00c90f10c0d3aa18fd3d2b170 Mon Sep 17 00:00:00 2001 From: Karl Kauc Date: Fri, 15 May 2026 15:50:58 +0200 Subject: [PATCH 2/3] XSD validation + Schematron invocation per stack; fix ruleset shadowing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit XSD_Validation/: CLI, Python, Java (verified here), .NET, PowerShell (reference). All target the official released schema via tools/fetch-schema.sh. Schematron: tools/fetch-tools.sh fetches Saxon + self-contained SchXslt CLI. invocation/ runs basic_checks.sch via CLI + native Java (verified), Python (saxonche) and .NET (reference); shared svrl-summary.py defines pass/fail. Ruleset fix: the percentage-sum rule (and a Position rule) were dead code — shadowed by an earlier same-context rule in the same pattern (ISO Schematron first-match-per-pattern). Split into percentage-validations / position-currency-validations / position-direction-validations so they fire. Verified: 120%-sum -> ERROR; canonical sample -> 0 errors. Canonical sample reconciled to truly pass all ERROR rules: Fund NAV set to 112,500,000 (actual position-value sum), percentages recomputed to sum to 100%, share classes given TotalAssetValues/SharesOutstanding consistent with NavPrice. 12 advisory warnings remain (documented). Negative fixtures: xsd-invalid (bad decimal) and schematron-invalid (percentages sum to 120%). .lib/ gitignored. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 4 + .../4.2.9/positions/Mixed-Fund_Positions.xml | 65 +- FundsXML_Files/4.2.9/positions/README.md | 23 +- .../Basic_Checks/README.md | 20 +- .../Basic_Checks/basic_checks.sch | 43 +- .../Basic_Checks/invocation/README.md | 62 ++ .../invocation/SchematronValidate.cs | 100 +++ .../invocation/SchematronValidate.csproj | 15 + .../invocation/SchematronValidate.java | 77 ++ .../Basic_Checks/invocation/run-schematron.sh | 34 + .../Basic_Checks/invocation/svrl-summary.py | 63 ++ .../invocation/validate_schematron.py | 72 ++ XSD_Validation/README.md | 57 ++ XSD_Validation/cli/validate.sh | 41 + XSD_Validation/dotnet/XsdValidate.cs | 94 ++ XSD_Validation/dotnet/XsdValidate.csproj | 12 + XSD_Validation/java/XsdValidate.java | 82 ++ .../powershell/Validate-FundsXml.ps1 | 83 ++ XSD_Validation/python/validate.py | 63 ++ tests/fixtures/invalid/README.md | 23 + .../invalid/schematron-invalid_Positions.xml | 847 ++++++++++++++++++ .../invalid/xsd-invalid_Positions.xml | 847 ++++++++++++++++++ tools/fetch-tools.sh | 48 + 23 files changed, 2740 insertions(+), 35 deletions(-) create mode 100644 Schematron_DataQuality_Checks/Basic_Checks/invocation/README.md create mode 100644 Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.cs create mode 100644 Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.csproj create mode 100644 Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.java create mode 100755 Schematron_DataQuality_Checks/Basic_Checks/invocation/run-schematron.sh create mode 100644 Schematron_DataQuality_Checks/Basic_Checks/invocation/svrl-summary.py create mode 100644 Schematron_DataQuality_Checks/Basic_Checks/invocation/validate_schematron.py create mode 100644 XSD_Validation/README.md create mode 100755 XSD_Validation/cli/validate.sh create mode 100644 XSD_Validation/dotnet/XsdValidate.cs create mode 100644 XSD_Validation/dotnet/XsdValidate.csproj create mode 100644 XSD_Validation/java/XsdValidate.java create mode 100644 XSD_Validation/powershell/Validate-FundsXml.ps1 create mode 100644 XSD_Validation/python/validate.py create mode 100644 tests/fixtures/invalid/README.md create mode 100644 tests/fixtures/invalid/schematron-invalid_Positions.xml create mode 100644 tests/fixtures/invalid/xsd-invalid_Positions.xml create mode 100755 tools/fetch-tools.sh diff --git a/.gitignore b/.gitignore index 495c548..d938041 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ CLAUDE.md # the source of truth is the official GitHub release. .schema-cache/ +# Locally fetched Java toolchain jars (via tools/fetch-tools.sh) — external +# dependencies from Maven Central, not repo source. +.lib/ + # Generated reports / outputs *.report.html report.html diff --git a/FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml b/FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml index 457a8f3..e81d234 100644 --- a/FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml +++ b/FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml @@ -40,7 +40,7 @@ 2025-10-01 OFFICIAL - 125000000 + 112500000 @@ -57,7 +57,7 @@ 9375000 - 7.50 + 8.33 50000.00 @@ -74,7 +74,7 @@ 8125000 - 6.50 + 7.22 8750.00 @@ -91,7 +91,7 @@ 6250000 - 5.00 + 5.56 6200000 @@ -108,7 +108,7 @@ 6250000 - 5.00 + 5.56 6300000.00 @@ -125,7 +125,7 @@ 10000000 - 8.00 + 8.89 200000.00 @@ -142,7 +142,7 @@ 12500000 - 10.00 + 11.11 25000.00 @@ -159,7 +159,7 @@ 1875000 - 1.50 + 1.67 150000.00 @@ -176,7 +176,7 @@ 1875000 - 1.50 + 1.67 250000.00 @@ -193,7 +193,7 @@ 3125000 - 2.50 + 2.78 15625.00 @@ -210,7 +210,7 @@ 3125000 - 2.50 + 2.78 25000.00 @@ -227,7 +227,7 @@ 1250000 - 1.00 + 1.11 @@ -241,7 +241,7 @@ 1250000 - 1.00 + 1.11 @@ -255,7 +255,7 @@ 2500000 - 2.00 + 2.22 50 @@ -269,7 +269,7 @@ 2500000 - 2.00 + 2.22 20 @@ -280,7 +280,7 @@ 5000000 - 4.00 + 4.44 @@ -289,7 +289,7 @@ 4375000 - 3.50 + 3.89 @@ -298,7 +298,7 @@ 6250000 - 5.00 + 5.56 @@ -307,7 +307,7 @@ 5625000 - 4.50 + 5.00 @@ -316,7 +316,7 @@ 3750000 - 3.00 + 3.33 @@ -325,7 +325,7 @@ 10625000 - 8.50 + 9.44 @@ -334,7 +334,7 @@ 6875000 - 5.50 + 6.11 @@ -360,6 +360,17 @@ 142.87 + + + 2025-10-01 + OFFICIAL + + 78937500 + 85722000 + + 600000 + + @@ -378,6 +389,16 @@ 134.25 + + + 2025-10-01 + OFFICIAL + + 33562500 + + 250000 + + diff --git a/FundsXML_Files/4.2.9/positions/README.md b/FundsXML_Files/4.2.9/positions/README.md index 17efce2..e3d88a5 100644 --- a/FundsXML_Files/4.2.9/positions/README.md +++ b/FundsXML_Files/4.2.9/positions/README.md @@ -2,6 +2,15 @@ This directory contains a comprehensive FundsXML sample document demonstrating all major asset types and structural elements. +> **Reconciliation note (2026-05):** the sample was reconciled so it genuinely +> passes every ERROR-level check (it previously did not). Changes: Fund +> `TotalNetAssetValue` set to **112,500,000 EUR** (the actual sum of the 21 +> position values, was 125,000,000); each `TotalPercentage` recomputed from the +> position value so they sum to exactly 100 %; both share classes given +> `TotalAssetValues` (NAV + `SharesOutstanding`) consistent with their +> `NavPrice`. The **XML file is authoritative**; any older per-row figures below +> are illustrative. Schematron now reports **0 errors** (12 advisory warnings). + ## File Overview | Property | Value | @@ -21,7 +30,7 @@ This directory contains a comprehensive FundsXML sample document demonstrating a | LEI | 529900T8BM49AURSDO55 | | Base Currency | EUR | | NAV Date | 2025-10-01 | -| Total Net Asset Value | 125,000,000 EUR | +| Total Net Asset Value | 112,500,000 EUR | | Data Supplier | Erste Asset Management GmbH | | Country | Austria (AT) | | Inception Date | 2015-03-15 | @@ -85,7 +94,8 @@ This sample file includes examples of all major FundsXML asset types: | ID_020 | - | Vienna Office Building | RE | 10,625,000 | 8.50% | | ID_021 | - | Call Money Raiffeisen | CM | 6,875,000 | 5.50% | -**Total**: 125,000,000 EUR (100%) +**Total**: 112,500,000 EUR (100%) — per-row values/percentages above are +illustrative; the XML file is authoritative (see Reconciliation note). ## XML Structure Walkthrough @@ -292,8 +302,13 @@ This sample file is useful for: ### Data Quality Considerations -This sample is designed to **pass all validation checks**: -- All percentages sum to exactly 100% +This sample **passes all ERROR-level validation checks** (verified against +`basic_checks.sch` via SchXslt — 0 errors; 12 advisory warnings remain, see the +Schematron README): +- All position percentages sum to exactly 100% +- Sum of position values equals Fund `TotalNetAssetValue` (112,500,000 EUR) +- Sum of share-class NAVs equals Fund total NAV; each share-class + `NavPrice` ties to `NAV / SharesOutstanding` - All positions have values in fund currency (EUR) - All required identifiers are present (LEI, ISIN where applicable) - NAV dates are consistent diff --git a/Schematron_DataQuality_Checks/Basic_Checks/README.md b/Schematron_DataQuality_Checks/Basic_Checks/README.md index 1fee6f0..7b4cba0 100644 --- a/Schematron_DataQuality_Checks/Basic_Checks/README.md +++ b/Schematron_DataQuality_Checks/Basic_Checks/README.md @@ -8,10 +8,28 @@ This directory contains comprehensive Schematron validation rules for FundsXML d |----------|-------| | **File** | `basic_checks.sch` | | **Query Binding** | XSLT 2.0 (`queryBinding="xslt2"`) | -| **Patterns** | 7 validation patterns | +| **Patterns** | 9 validation patterns | | **Total Rules** | 40+ assertions and reports | | **Purpose** | Comprehensive FundsXML data quality validation | +## Known ruleset fix + +ISO Schematron matches each node against only the **first** `rule` whose +`context` matches **within a pattern**. The `portfolio-validations` pattern +previously held two `Fund[…]` rules (position-value sum *and* percentage sum); +every real fund matched the first, so the **percentage-sum rule was dead code +and never fired** (likewise a `Position` rule shadowed the multi-currency +direction rule). These were split into their own patterns — +`percentage-validations`, `position-currency-validations`, +`position-direction-validations` — so each rule actually executes. Verified: a +document with percentages summing to 120 % now produces an ERROR; the canonical +sample (summing to 100 %) does not. + +Run it across stacks via [`invocation/`](invocation/) (CLI, native Java, +Python/saxonche, .NET). The canonical sample yields **0 errors + 12 advisory +warnings** (the broad `ShareClass` rule also matches `AssetDetails/ShareClass`; +derivative assets without exposure info). + ## Requirements **XSLT 2.0 processor required** due to `queryBinding="xslt2"`. diff --git a/Schematron_DataQuality_Checks/Basic_Checks/basic_checks.sch b/Schematron_DataQuality_Checks/Basic_Checks/basic_checks.sch index 701e81e..3d70fed 100644 --- a/Schematron_DataQuality_Checks/Basic_Checks/basic_checks.sch +++ b/Schematron_DataQuality_Checks/Basic_Checks/basic_checks.sch @@ -123,23 +123,46 @@ Difference: - + + + + + Portfolio Percentage Allocation + - + - ERROR: Portfolio position percentages sum to % + ERROR: Portfolio position percentages sum to % instead of 100%. Difference: % - + WARNING: Small deviation in percentage sum. Total: % - + + + + + Position Currency Coverage + @@ -148,14 +171,18 @@ Available currencies: - + + + + Position Value Direction Consistency + - + - ERROR: Position has mixed value directions across currencies. + ERROR: Position has mixed value directions across currencies. All values must be either positive or negative. diff --git a/Schematron_DataQuality_Checks/Basic_Checks/invocation/README.md b/Schematron_DataQuality_Checks/Basic_Checks/invocation/README.md new file mode 100644 index 0000000..338e9b0 --- /dev/null +++ b/Schematron_DataQuality_Checks/Basic_Checks/invocation/README.md @@ -0,0 +1,62 @@ +# Schematron Invocation + +Run `basic_checks.sch` against a FundsXML document from four stacks. Same +SVRL semantics and pass/fail definition everywhere. + +## Why a Saxon-class processor is required + +`basic_checks.sch` declares `queryBinding="xslt2"`. ISO Schematron is applied by +**compiling it to XSLT** (SchXslt does this) and running the result to produce an +**SVRL** report. That needs an XSLT 2.0 engine — Saxon. `xmllint`/`xsltproc`/ +`lxml` (XSLT 1.0 only) **cannot** run this ruleset. + +`tools/fetch-tools.sh` fetches the SchXslt CLI jar (a self-contained runner that +bundles its own Saxon) into `.lib/`. + +## SVRL semantics & pass/fail + +`svrl-summary.py` is the single source of truth, shared by every stack and CI: + +| SVRL element | Meaning | +|--------------|---------| +| `svrl:failed-assert` `role="error"` | ERROR | +| `svrl:failed-assert` `role="warning"` | WARNING | +| `svrl:successful-report` | a `` fired (WARNING/INFO) | + +`--fail-on error` (default) exits 1 only on ERROR-role failures; `--fail-on any` +exits 1 on any failed-assert (including warnings). + +> The canonical sample currently yields **0 errors + 12 warnings**. The +> warnings are advisory (4× the broad `ShareClass` rule also matching +> `AssetDetails/ShareClass`; 8× derivative assets without exposure info). The +> percentage-sum rule is now active — see +> [`../README.md`](../README.md#known-ruleset-fix). + +## Stacks + +| Stack | File | Runnable on this box | +|-------|------|----------------------| +| CLI | [`run-schematron.sh`](run-schematron.sh) | ✅ verified | +| Java (native) | [`SchematronValidate.java`](SchematronValidate.java) | ✅ verified (SchXslt Java API) | +| Python | [`validate_schematron.py`](validate_schematron.py) | needs `pip install saxonche` | +| .NET/C# | [`SchematronValidate.cs`](SchematronValidate.cs) | needs .NET SDK + `SaxonHE` package | +| shared | [`svrl-summary.py`](svrl-summary.py) | ✅ classifier used by all + CI | + +Java classpath (the CLI jar already bundles Saxon — do **not** add the +standalone `Saxon-HE` jar, the two need different `org.xmlresolver` APIs): + +```bash +tools/fetch-tools.sh +CP=.lib/schxslt-cli-1.10.1.jar:.lib/commons-cli-1.5.0.jar:.lib/slf4j-api-1.7.32.jar:.lib/slf4j-nop-1.7.32.jar +javac -cp "$CP" -d /tmp/scv Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.java +java -cp "$CP:/tmp/scv" SchematronValidate Schematron_DataQuality_Checks/Basic_Checks/basic_checks.sch \ + FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml +``` + +## Quick check (positive + negative) + +```bash +RS=Schematron_DataQuality_Checks/Basic_Checks/invocation/run-schematron.sh +$RS FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml # exit 0 (0 errors) +$RS tests/fixtures/invalid/schematron-invalid_Positions.xml # exit 1 (percentage 120%) +``` diff --git a/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.cs b/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.cs new file mode 100644 index 0000000..95cc10f --- /dev/null +++ b/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.cs @@ -0,0 +1,100 @@ +// Schematron validation in .NET / C# via Saxon for .NET (Saxonica SaxonHE). +// +// dotnet add package SaxonHE # Saxon 12.x for .NET +// dotnet run --project Schematron_DataQuality_Checks/Basic_Checks/invocation \ +// -- ../basic_checks.sch document.xml +// Exit: 0 = no error-role failed-assert, 1 = at least one, 2 = setup error. +// +// basic_checks.sch uses queryBinding="xslt2"; Saxon supplies the XSLT 3.0 +// engine. The SchXslt pipeline stylesheets are reused from the SchXslt CLI jar +// (tools/fetch-tools.sh puts it in .lib/): the whole xslt/ tree is extracted so +// the pipeline's relative imports resolve, then compile .sch -> SVRL stylesheet +// -> apply to instance -> SVRL, then classify (same logic as svrl-summary.py). +// +// NOTE: reference implementation — not executed in this environment (no .NET +// SDK). The flow mirrors the verified Python/CLI paths exactly. + +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Xml; +using Saxon.Api; + +internal static class SchematronValidate +{ + private const string Svrl = "http://purl.oclc.org/dsdl/svrl"; + + private static int Main(string[] args) + { + if (args.Length < 2) + { + Console.Error.WriteLine( + "usage: SchematronValidate [--fail-on error|any]"); + return 2; + } + string sch = args[0], xml = args[1]; + string failOn = args.SkipWhile(a => a != "--fail-on") + .Skip(1).FirstOrDefault() ?? "error"; + + string repoRoot = Path.GetFullPath(Path.Combine( + AppContext.BaseDirectory, "..", "..", "..", "..", "..", "..")); + string cliJar = Path.Combine(repoRoot, ".lib", "schxslt-cli-1.10.1.jar"); + if (!File.Exists(cliJar)) + { + Console.Error.WriteLine("SchXslt jar missing; run: tools/fetch-tools.sh"); + return 2; + } + + string tmp = Directory.CreateTempSubdirectory().FullName; + using (var zip = ZipFile.OpenRead(cliJar)) + foreach (var e in zip.Entries.Where(e => e.FullName.StartsWith("xslt/") + && !e.FullName.EndsWith("/"))) + { + string dest = Path.Combine(tmp, e.FullName); + Directory.CreateDirectory(Path.GetDirectoryName(dest)!); + e.ExtractToFile(dest, true); + } + + var processor = new Processor(false); + var comp = processor.NewXsltCompiler(); + + string compiled = Path.Combine(tmp, "compiled.xsl"); + string svrl = Path.Combine(tmp, "report.svrl"); + + // 1) Schematron -> SVRL stylesheet + Transform(comp, Path.Combine(tmp, "xslt/2.0/pipeline-for-svrl.xsl"), + sch, compiled, processor); + // 2) instance -> SVRL + Transform(comp, compiled, xml, svrl, processor); + + var doc = new XmlDocument(); + doc.Load(svrl); + var ns = new XmlNamespaceManager(doc.NameTable); + ns.AddNamespace("svrl", Svrl); + + int errors = 0, warnings = 0; + foreach (XmlElement fa in doc.SelectNodes("//svrl:failed-assert", ns)!) + { + string role = fa.GetAttribute("role").ToLowerInvariant(); + string text = (fa.SelectSingleNode("svrl:text", ns)?.InnerText ?? "") + .Trim(); + if (role == "error") { errors++; Console.WriteLine("ERROR " + text); } + else { warnings++; Console.WriteLine("WARNING " + text); } + } + Console.WriteLine($"\nsummary: {errors} error(s), {warnings} warning(s)"); + + if (failOn == "any" && (errors > 0 || warnings > 0)) return 1; + return errors > 0 ? 1 : 0; + } + + private static void Transform(XsltCompiler comp, string xsl, string src, + string outFile, Processor p) + { + var exe = comp.Compile(new Uri(Path.GetFullPath(xsl))); + var t = exe.Load30(); + using var os = File.Create(outFile); + t.Transform(new Uri(Path.GetFullPath(src)), + p.NewSerializer(os)); + } +} diff --git a/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.csproj b/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.csproj new file mode 100644 index 0000000..9ecac82 --- /dev/null +++ b/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + SchematronValidate + FundsXml.Schematron + + + + + diff --git a/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.java b/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.java new file mode 100644 index 0000000..00483f8 --- /dev/null +++ b/Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.java @@ -0,0 +1,77 @@ +// Native-Java Schematron validation using the SchXslt Java API. +// +// Compile + run against the SchXslt CLI classpath (it bundles Saxon): +// tools/fetch-tools.sh # populates .lib/ +// CP=.lib/schxslt-cli-1.10.1.jar:.lib/commons-cli-1.5.0.jar:\ +// .lib/slf4j-api-1.7.32.jar:.lib/slf4j-nop-1.7.32.jar +// javac -cp "$CP" -d /tmp Schematron_DataQuality_Checks/Basic_Checks/invocation/SchematronValidate.java +// java -cp "$CP:/tmp" SchematronValidate basic_checks.sch document.xml +// +// Exit: 0 = no error-role failed-assert, 1 = at least one, 2 = setup error. +// +// Why this classpath: basic_checks.sch uses queryBinding="xslt2". SchXslt +// compiles the Schematron to XSLT 2.0 and Saxon (bundled in the SchXslt CLI +// jar) executes it. Do NOT add the standalone Saxon-HE jar — its org.xmlresolver +// API differs from the one SchXslt bundles and the two conflict. + +import java.io.File; +import javax.xml.transform.stream.StreamSource; +import name.dmaus.schxslt.Schematron; +import name.dmaus.schxslt.Result; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +public class SchematronValidate { + + private static final String SVRL_NS = "http://purl.oclc.org/dsdl/svrl"; + + public static void main(String[] args) { + if (args.length != 2) { + System.err.println("usage: SchematronValidate "); + System.exit(2); + } + try { + Schematron schematron = + new Schematron(new StreamSource(new File(args[0]))); + Result result = + schematron.validate(new StreamSource(new File(args[1]))); + Document svrl = result.getValidationReport(); + + int errors = 0, warnings = 0, reports = 0; + NodeList fa = svrl.getElementsByTagNameNS(SVRL_NS, "failed-assert"); + for (int i = 0; i < fa.getLength(); i++) { + Element e = (Element) fa.item(i); + String role = e.getAttribute("role"); + String text = textOf(e); + if ("error".equalsIgnoreCase(role)) { + errors++; + System.out.println("ERROR " + text); + } else { + warnings++; + System.out.println("WARNING " + text); + } + } + NodeList sr = + svrl.getElementsByTagNameNS(SVRL_NS, "successful-report"); + for (int i = 0; i < sr.getLength(); i++) { + reports++; + System.out.println("REPORT " + textOf((Element) sr.item(i))); + } + + System.out.printf("%nsummary: %d error(s), %d warning(s), " + + "%d report(s)%n", errors, warnings, reports); + System.exit(errors > 0 ? 1 : 0); + } catch (Exception ex) { + System.err.println("setup/processing error: " + ex.getMessage()); + System.exit(2); + } + } + + private static String textOf(Element parent) { + NodeList t = parent.getElementsByTagNameNS(SVRL_NS, "text"); + return t.getLength() > 0 + ? t.item(0).getTextContent().trim().replaceAll("\\s+", " ") + : ""; + } +} diff --git a/Schematron_DataQuality_Checks/Basic_Checks/invocation/run-schematron.sh b/Schematron_DataQuality_Checks/Basic_Checks/invocation/run-schematron.sh new file mode 100755 index 0000000..79e7765 --- /dev/null +++ b/Schematron_DataQuality_Checks/Basic_Checks/invocation/run-schematron.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Schematron validation via the command line (SchXslt + Saxon). +# +# Usage: run-schematron.sh [--fail-on error|any] +# Exit: 0 = pass, 1 = fail (per --fail-on), 2 = setup error +# +# basic_checks.sch uses queryBinding="xslt2", so an XSLT 2.0 processor (Saxon) +# is required. SchXslt compiles the Schematron to XSLT and applies it, emitting +# an SVRL report which svrl-summary.py then classifies. +set -euo pipefail + +XML="${1:?usage: run-schematron.sh [--fail-on error|any]}" +shift || true + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +SCH="${REPO_ROOT}/Schematron_DataQuality_Checks/Basic_Checks/basic_checks.sch" +LIB="${REPO_ROOT}/.lib" + +if [[ ! -f "${LIB}/schxslt-cli-1.10.1.jar" ]]; then + echo "Java toolchain not present; fetching (Saxon + SchXslt)..." >&2 + "${REPO_ROOT}/tools/fetch-tools.sh" >/dev/null +fi + +# SchXslt-only classpath. The CLI jar bundles its own Saxon; do NOT add the +# standalone Saxon-HE jar here (different org.xmlresolver API → breakage). +CP="${LIB}/schxslt-cli-1.10.1.jar:${LIB}/commons-cli-1.5.0.jar:${LIB}/slf4j-api-1.7.32.jar:${LIB}/slf4j-nop-1.7.32.jar" +SVRL="$(mktemp)" +trap 'rm -f "$SVRL"' EXIT + +java -cp "$CP" name.dmaus.schxslt.cli.Application \ + -s "$SCH" -d "$XML" -o "$SVRL" >/dev/null 2>&1 || true + +python3 "${REPO_ROOT}/Schematron_DataQuality_Checks/Basic_Checks/invocation/svrl-summary.py" \ + "$SVRL" "$@" diff --git a/Schematron_DataQuality_Checks/Basic_Checks/invocation/svrl-summary.py b/Schematron_DataQuality_Checks/Basic_Checks/invocation/svrl-summary.py new file mode 100644 index 0000000..5bd7c63 --- /dev/null +++ b/Schematron_DataQuality_Checks/Basic_Checks/invocation/svrl-summary.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Summarize an SVRL report and set an exit code. + +SVRL (Schematron Validation Report Language) semantics used here: + svrl:failed-assert with role="error" -> ERROR + svrl:failed-assert with role="warning" -> WARNING + svrl:successful-report -> WARNING/INFO (a fired) + +Usage: svrl-summary.py [--fail-on error|any] + --fail-on error : exit 1 only if any ERROR-role failed-assert (default) + --fail-on any : exit 1 if any failed-assert (incl. warnings) + +Shared by the per-stack invocation scripts and by CI so the pass/fail +definition is identical everywhere. +""" +import sys +import xml.etree.ElementTree as ET + +SVRL = "{http://purl.oclc.org/dsdl/svrl}" + + +def main() -> int: + args = sys.argv[1:] + fail_on = "error" + if "--fail-on" in args: + i = args.index("--fail-on") + fail_on = args[i + 1] + del args[i:i + 2] + if len(args) != 1: + print("usage: svrl-summary.py [--fail-on error|any]", + file=sys.stderr) + return 2 + + root = ET.parse(args[0]).getroot() + errors, warnings, reports = [], [], [] + for fa in root.iter(f"{SVRL}failed-assert"): + role = (fa.get("role") or "").lower() + text = " ".join((fa.findtext(f"{SVRL}text") or "").split()) + (errors if role == "error" else warnings).append((role, text)) + for sr in root.iter(f"{SVRL}successful-report"): + text = " ".join((sr.findtext(f"{SVRL}text") or "").split()) + reports.append(text) + + for role, text in errors: + print(f"ERROR {text}") + for role, text in warnings: + print(f"WARNING {text}") + for text in reports: + print(f"REPORT {text}") + + print(f"\nsummary: {len(errors)} error(s), " + f"{len(warnings)} warning failed-assert(s), " + f"{len(reports)} report(s)") + + if fail_on == "any" and (errors or warnings): + return 1 + if errors: + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Schematron_DataQuality_Checks/Basic_Checks/invocation/validate_schematron.py b/Schematron_DataQuality_Checks/Basic_Checks/invocation/validate_schematron.py new file mode 100644 index 0000000..0a33217 --- /dev/null +++ b/Schematron_DataQuality_Checks/Basic_Checks/invocation/validate_schematron.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Schematron validation in Python via saxonche (Saxon's Python API). + +Usage: python validate_schematron.py [--fail-on ...] +Exit: 0 = no error-role failed-assert, 1 = at least one, 2 = setup error + +basic_checks.sch uses queryBinding="xslt2", so an XSLT 2.0 processor is +required. `lxml` only does XSLT 1.0 and CANNOT run this ruleset — saxonche +(`pip install saxonche`) embeds Saxon's XSLT 3.0 engine. + +The SchXslt pipeline stylesheets are reused straight out of the SchXslt CLI jar +(`tools/fetch-tools.sh` puts it in .lib/). The whole `xslt/` tree is extracted +to a temp dir so the pipeline's relative xsl:import/include resolve, then: + 1) compile the .sch into an SVRL stylesheet, + 2) apply it to the instance to get SVRL, + 3) classify with the shared svrl-summary.py. +""" +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path + +HERE = Path(__file__).resolve().parent +REPO_ROOT = HERE.parents[2] +CLI_JAR = REPO_ROOT / ".lib" / "schxslt-cli-1.10.1.jar" + + +def main() -> int: + if len(sys.argv) < 3: + print("usage: validate_schematron.py " + "[--fail-on error|any]", file=sys.stderr) + return 2 + sch, xml = sys.argv[1], sys.argv[2] + + try: + from saxonche import PySaxonProcessor + except ImportError: + print("saxonche not installed. Run: pip install saxonche\n" + "(lxml cannot be used here — basic_checks.sch is XSLT 2.0.)", + file=sys.stderr) + return 2 + + if not CLI_JAR.is_file(): + print("SchXslt jar missing; run: tools/fetch-tools.sh", file=sys.stderr) + return 2 + + with tempfile.TemporaryDirectory() as tmp: + tmp = Path(tmp) + with zipfile.ZipFile(CLI_JAR) as z: + for n in z.namelist(): + if n.startswith("xslt/"): + z.extract(n, tmp) + pipeline = tmp / "xslt" / "2.0" / "pipeline-for-svrl.xsl" + compiled = tmp / "compiled.xsl" + svrl = tmp / "report.svrl" + + with PySaxonProcessor(license=False) as proc: + xslt = proc.new_xslt30_processor() + xslt.transform_to_file(source_file=sch, + stylesheet_file=str(pipeline), + output_file=str(compiled)) + xslt.transform_to_file(source_file=xml, + stylesheet_file=str(compiled), + output_file=str(svrl)) + + return subprocess.call([sys.executable, str(HERE / "svrl-summary.py"), + str(svrl), *sys.argv[3:]]) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/XSD_Validation/README.md b/XSD_Validation/README.md new file mode 100644 index 0000000..1e43945 --- /dev/null +++ b/XSD_Validation/README.md @@ -0,0 +1,57 @@ +# XSD Validation + +Validate a FundsXML document against the **official released schema** in five +enterprise stacks. Same input, same result everywhere. + +## The schema source + +Validation always targets the official release: + +``` +https://github.com/fundsxml/schema/releases/download//FundsXML.xsd +``` + +This is **not** a hand-maintained catalog — it is the canonical released schema. +Two realities every example must deal with: + +1. **HTTP 302 redirect.** The GitHub URL redirects to + `objects.githubusercontent.com`. Processors with a naive HTTP client + (libxml2/xmllint) do not follow it. On locked-down enterprise networks the + download also goes through an HTTP proxy. +2. **Relative import.** From release 4.2.9 on, `FundsXML.xsd` imports + `xmldsig-core-schema.xsd` via a *relative* path; both files must sit together. + +`tools/fetch-schema.sh ` resolves both (proxy-aware via curl) and +materializes the released schema into `.schema-cache//`. The examples +validate against that materialized release. Run it once up front: + +```bash +tools/fetch-schema.sh 4.2.9 +``` + +## Security + +Every example disables external entity resolution / DTD loading +(`FEATURE_SECURE_PROCESSING`, `resolve_entities=False`, `XmlResolver=null`, +`-nonet`) — FundsXML never needs them and they are a classic XXE vector. + +## Stacks + +| Stack | Script | API | Runnable on this box | +|-------|--------|-----|----------------------| +| CLI | [`cli/validate.sh`](cli/validate.sh) | `xmllint` (+ Saxon note) | ✅ | +| Python | [`python/validate.py`](python/validate.py) | `lxml.etree.XMLSchema` | ✅ | +| Java | [`java/XsdValidate.java`](java/XsdValidate.java) | `javax.xml.validation` | ✅ (single-file, JDK 11+) | +| .NET/C# | [`dotnet/XsdValidate.cs`](dotnet/XsdValidate.cs) | `XmlSchemaSet` | needs .NET SDK | +| PowerShell | [`powershell/Validate-FundsXml.ps1`](powershell/Validate-FundsXml.ps1) | `System.Xml.Schema` | needs PowerShell | + +Convention: each takes ` `, exits `0` on valid, `1` on +invalid, prints errors to stderr. + +## Quick check (positive + negative) + +```bash +tools/fetch-schema.sh 4.2.9 +XSD_Validation/cli/validate.sh 4.2.9 FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml # exit 0 +XSD_Validation/cli/validate.sh 4.2.9 tests/fixtures/invalid/xsd-invalid_Positions.xml # exit 1 +``` diff --git a/XSD_Validation/cli/validate.sh b/XSD_Validation/cli/validate.sh new file mode 100755 index 0000000..015cc87 --- /dev/null +++ b/XSD_Validation/cli/validate.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# XSD validation via the command line (xmllint). +# +# Usage: XSD_Validation/cli/validate.sh +# Exit: 0 = valid, 1 = invalid, 2 = usage/setup error +# +# Validates against the official released schema, materialized locally by +# tools/fetch-schema.sh (handles the GitHub 302 redirect and the relative +# xmldsig-core-schema.xsd import that FundsXML 4.2.9+ requires). +set -euo pipefail + +VERSION="${1:?usage: validate.sh }" +XML="${2:?usage: validate.sh }" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +SCHEMA="${REPO_ROOT}/.schema-cache/${VERSION}/FundsXML.xsd" + +if [[ ! -f "$SCHEMA" ]]; then + echo "schema not cached; fetching official release ${VERSION}..." >&2 + "${REPO_ROOT}/tools/fetch-schema.sh" "$VERSION" >/dev/null +fi + +# --nonet: never hit the network during validation (XXE / entity hardening). +# The schema was already fetched explicitly above. +if xmllint --noout --nonet --schema "$SCHEMA" "$XML" 2>/tmp/xmllint.$$; then + echo "VALID: $XML (FundsXML $VERSION)" + rm -f /tmp/xmllint.$$ + exit 0 +else + echo "INVALID: $XML (FundsXML $VERSION)" >&2 + cat /tmp/xmllint.$$ >&2 + rm -f /tmp/xmllint.$$ + exit 1 +fi + +# --- Alternative: Saxon (XSD 1.1, follows HTTP redirects natively) ---------- +# Saxon-EE/HE can validate and DOES follow the 302, but the relative +# xmldsig-core-schema.xsd import still needs both files side by side, so the +# fetched local copy is used here too: +# java -cp saxon-he.jar com.saxonica.Validate \ +# -xsd:"$SCHEMA" -s:"$XML" diff --git a/XSD_Validation/dotnet/XsdValidate.cs b/XSD_Validation/dotnet/XsdValidate.cs new file mode 100644 index 0000000..5f15d86 --- /dev/null +++ b/XSD_Validation/dotnet/XsdValidate.cs @@ -0,0 +1,94 @@ +// XSD validation in .NET / C# via System.Xml.Schema. +// +// Run as a single-file program (no project needed) with the .NET SDK: +// dotnet run --project XSD_Validation/dotnet -- +// or compile XsdValidate.cs into any console app. Exit: 0 valid, 1 invalid, +// 2 usage/setup error. +// +// Validates against the official released schema, materialized locally by +// tools/fetch-schema.sh (handles the GitHub 302 redirect and the relative +// xmldsig-core-schema.xsd import that FundsXML 4.2.9+ requires). +// +// Security: XmlResolver = null on the reader closes XXE / external-entity +// vectors. An XmlUrlResolver is used ONLY to resolve the schema set's local +// relative xmldsig import, never for instance documents. + +using System; +using System.IO; +using System.Xml; +using System.Xml.Schema; + +internal static class XsdValidate +{ + private static int Main(string[] args) + { + if (args.Length != 2) + { + Console.Error.WriteLine("usage: XsdValidate "); + return 2; + } + + string version = args[0]; + string xmlFile = args[1]; + + string repoRoot = Path.GetFullPath( + Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..")); + string schemaPath = Path.Combine( + repoRoot, ".schema-cache", version, "FundsXML.xsd"); + + if (!File.Exists(schemaPath)) + { + Console.Error.WriteLine( + $"schema not cached; run: tools/fetch-schema.sh {version}"); + return 2; + } + + var schemas = new XmlSchemaSet + { + // Needed only so the schema's relative xmldsig-core-schema.xsd + // import (4.2.9+) resolves from the same directory. + XmlResolver = new XmlUrlResolver() + }; + schemas.Add(null, schemaPath); + + bool failed = false; + var settings = new XmlReaderSettings + { + ValidationType = ValidationType.Schema, + Schemas = schemas, + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null // harden the instance document against XXE + }; + settings.ValidationFlags |= XmlSchemaValidationFlags.ReportValidationWarnings; + settings.ValidationEventHandler += (_, e) => + { + if (e.Severity == XmlSeverityType.Error) + { + failed = true; + Console.Error.WriteLine( + $" line {e.Exception.LineNumber}: {e.Message}"); + } + }; + + try + { + using var reader = XmlReader.Create(xmlFile, settings); + while (reader.Read()) { } + } + catch (XmlException ex) + { + Console.Error.WriteLine($" {ex.Message}"); + failed = true; + } + + if (failed) + { + Console.Error.WriteLine( + $"INVALID: {xmlFile} (FundsXML {version})"); + return 1; + } + + Console.WriteLine($"VALID: {xmlFile} (FundsXML {version})"); + return 0; + } +} diff --git a/XSD_Validation/dotnet/XsdValidate.csproj b/XSD_Validation/dotnet/XsdValidate.csproj new file mode 100644 index 0000000..84d7946 --- /dev/null +++ b/XSD_Validation/dotnet/XsdValidate.csproj @@ -0,0 +1,12 @@ + + + + Exe + net8.0 + disable + disable + XsdValidate + FundsXml.XsdValidation + + diff --git a/XSD_Validation/java/XsdValidate.java b/XSD_Validation/java/XsdValidate.java new file mode 100644 index 0000000..44c47a3 --- /dev/null +++ b/XSD_Validation/java/XsdValidate.java @@ -0,0 +1,82 @@ +// XSD validation in native Java (no JAXB) via javax.xml.validation. +// +// Single-file program — run directly with a modern JDK (11+): +// java XSD_Validation/java/XsdValidate.java +// Exit: 0 = valid, 1 = invalid, 2 = usage/setup error +// +// Validates against the official released schema, materialized locally by +// tools/fetch-schema.sh (handles the GitHub 302 redirect and the relative +// xmldsig-core-schema.xsd import that FundsXML 4.2.9+ requires). +// +// Security: FEATURE_SECURE_PROCESSING on, external DTD/schema access denied, +// XXE vectors closed. FundsXML needs no external entities. + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.xml.XMLConstants; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; +import org.xml.sax.ErrorHandler; +import org.xml.sax.SAXParseException; + +public class XsdValidate { + + public static void main(String[] args) throws Exception { + if (args.length != 2) { + System.err.println("usage: XsdValidate "); + System.exit(2); + } + String version = args[0]; + String xmlFile = args[1]; + + Path repoRoot = Paths.get(System.getProperty("user.dir")); + File schema = repoRoot.resolve(".schema-cache").resolve(version) + .resolve("FundsXML.xsd").toFile(); + if (!schema.isFile()) { + System.err.println("schema not cached; run: tools/fetch-schema.sh " + version); + System.exit(2); + } + + SchemaFactory factory = + SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + // Allow only local file access so the relative xmldsig-core-schema.xsd + // import (4.2.9+) resolves; block http/external fetches. + factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file"); + factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + + Schema fundsXmlSchema = factory.newSchema(schema); + Validator validator = fundsXmlSchema.newValidator(); + validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file"); + + final boolean[] failed = {false}; + validator.setErrorHandler(new ErrorHandler() { + public void warning(SAXParseException e) { } + public void error(SAXParseException e) { report(e); } + public void fatalError(SAXParseException e) { report(e); } + private void report(SAXParseException e) { + failed[0] = true; + System.err.println(" line " + e.getLineNumber() + ": " + + e.getMessage()); + } + }); + + try { + validator.validate(new StreamSource(new File(xmlFile))); + } catch (SAXParseException e) { + failed[0] = true; + System.err.println(" line " + e.getLineNumber() + ": " + + e.getMessage()); + } + + if (failed[0]) { + System.err.println("INVALID: " + xmlFile + " (FundsXML " + version + ")"); + System.exit(1); + } + System.out.println("VALID: " + xmlFile + " (FundsXML " + version + ")"); + } +} diff --git a/XSD_Validation/powershell/Validate-FundsXml.ps1 b/XSD_Validation/powershell/Validate-FundsXml.ps1 new file mode 100644 index 0000000..f42a535 --- /dev/null +++ b/XSD_Validation/powershell/Validate-FundsXml.ps1 @@ -0,0 +1,83 @@ +<# +.SYNOPSIS + XSD validation in PowerShell via System.Xml.Schema. + +.DESCRIPTION + Validates a FundsXML document against the official released schema, + materialized locally by tools/fetch-schema.sh (handles the GitHub 302 + redirect and the relative xmldsig-core-schema.xsd import that FundsXML + 4.2.9+ requires). + + Security: the instance document is read with XmlResolver = $null and + DtdProcessing = Prohibit to close XXE / external-entity vectors. A + URL resolver is used only for the schema set's local relative import. + + Works in Windows PowerShell 5.1 and PowerShell 7+. + +.PARAMETER Version + FundsXML version, e.g. 4.2.9 + +.PARAMETER XmlFile + Path to the FundsXML instance document. + +.EXAMPLE + pwsh XSD_Validation/powershell/Validate-FundsXml.ps1 4.2.9 ` + FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml + +.OUTPUTS + Exit code 0 = valid, 1 = invalid, 2 = usage/setup error. +#> +param( + [Parameter(Mandatory = $true)][string]$Version, + [Parameter(Mandatory = $true)][string]$XmlFile +) + +$ErrorActionPreference = 'Stop' + +$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..' '..') +$schemaPath = Join-Path $repoRoot ".schema-cache/$Version/FundsXML.xsd" + +if (-not (Test-Path $schemaPath)) { + Write-Error "schema not cached; run: tools/fetch-schema.sh $Version" + exit 2 +} + +$schemas = New-Object System.Xml.Schema.XmlSchemaSet +# Needed only so the schema's relative xmldsig-core-schema.xsd import (4.2.9+) +# resolves from the same directory. +$schemas.XmlResolver = New-Object System.Xml.XmlUrlResolver +[void]$schemas.Add($null, $schemaPath) + +$settings = New-Object System.Xml.XmlReaderSettings +$settings.ValidationType = [System.Xml.ValidationType]::Schema +$settings.Schemas = $schemas +$settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit +$settings.XmlResolver = $null # harden instance document against XXE + +$script:failed = $false +$handler = [System.Xml.Schema.ValidationEventHandler] { + param($sender, $e) + if ($e.Severity -eq [System.Xml.Schema.XmlSeverityType]::Error) { + $script:failed = $true + Write-Host (" line {0}: {1}" -f $e.Exception.LineNumber, $e.Message) + } +} +$settings.add_ValidationEventHandler($handler) + +try { + $reader = [System.Xml.XmlReader]::Create($XmlFile, $settings) + while ($reader.Read()) { } + $reader.Dispose() +} +catch [System.Xml.XmlException] { + Write-Host (" {0}" -f $_.Exception.Message) + $script:failed = $true +} + +if ($script:failed) { + Write-Error "INVALID: $XmlFile (FundsXML $Version)" + exit 1 +} + +Write-Host "VALID: $XmlFile (FundsXML $Version)" +exit 0 diff --git a/XSD_Validation/python/validate.py b/XSD_Validation/python/validate.py new file mode 100644 index 0000000..3f18b0a --- /dev/null +++ b/XSD_Validation/python/validate.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""XSD validation in Python via lxml. + +Usage: python XSD_Validation/python/validate.py +Exit: 0 = valid, 1 = invalid, 2 = usage/setup error + +Validates against the official released schema, materialized locally by +tools/fetch-schema.sh (handles the GitHub 302 redirect and the relative +xmldsig-core-schema.xsd import that FundsXML 4.2.9+ requires). + +Security: the XML parser is hardened against XXE / entity-expansion +(no_network=True, resolve_entities=False, no DTD load). FundsXML needs none +of those features. +""" +import subprocess +import sys +from pathlib import Path + +from lxml import etree + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def ensure_schema(version: str) -> Path: + schema = REPO_ROOT / ".schema-cache" / version / "FundsXML.xsd" + if not schema.is_file(): + print(f"schema not cached; fetching official release {version}...", + file=sys.stderr) + subprocess.run([str(REPO_ROOT / "tools" / "fetch-schema.sh"), version], + check=True, stdout=subprocess.DEVNULL) + return schema + + +def main() -> int: + if len(sys.argv) != 3: + print("usage: validate.py ", file=sys.stderr) + return 2 + version, xml_path = sys.argv[1], sys.argv[2] + + schema_path = ensure_schema(version) + + # Hardened parser: no network, no entity resolution, no huge-tree blowups. + safe = etree.XMLParser(no_network=True, resolve_entities=False, + load_dtd=False, huge_tree=False) + + # The schema itself is parsed with network access so its relative + # xmldsig-core-schema.xsd import (4.2.9+) resolves from the same dir. + schema_doc = etree.parse(str(schema_path)) + schema = etree.XMLSchema(schema_doc) + + doc = etree.parse(xml_path, parser=safe) + if schema.validate(doc): + print(f"VALID: {xml_path} (FundsXML {version})") + return 0 + + print(f"INVALID: {xml_path} (FundsXML {version})", file=sys.stderr) + for err in schema.error_log: + print(f" line {err.line}: {err.message}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/fixtures/invalid/README.md b/tests/fixtures/invalid/README.md new file mode 100644 index 0000000..f153df7 --- /dev/null +++ b/tests/fixtures/invalid/README.md @@ -0,0 +1,23 @@ +# Negative Test Fixtures + +Deliberately broken copies of `FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml`. +Used by the negative path of the per-stack examples and by CI. The committed +positive sample must always pass; these must always fail their respective check. + +| File | Mutation | Fails | Still passes | +|------|----------|-------|--------------| +| `xsd-invalid_Positions.xml` | ID_001 `TotalValue/Amount` set to the non-numeric string `NINE-MILLION` | **XSD** (`xs:decimal` violation) | — | +| `schematron-invalid_Positions.xml` | ID_001 `TotalPercentage` `8.33` → `28.33`, so position percentages sum to 120% | **Schematron** (`percentage-validations` pattern, ERROR) | XSD (structurally valid) | + +The Schematron fixture targets the percentage-sum rule, which is now in its own +`percentage-validations` pattern (previously dead code — see +`Schematron_DataQuality_Checks/Basic_Checks/README.md`). + +Regenerate after the canonical sample changes: + +```bash +sed 's#9375000#NINE-MILLION#' \ + FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml > tests/fixtures/invalid/xsd-invalid_Positions.xml +sed 's#8.33#28.33#' \ + FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml > tests/fixtures/invalid/schematron-invalid_Positions.xml +``` diff --git a/tests/fixtures/invalid/schematron-invalid_Positions.xml b/tests/fixtures/invalid/schematron-invalid_Positions.xml new file mode 100644 index 0000000..980b455 --- /dev/null +++ b/tests/fixtures/invalid/schematron-invalid_Positions.xml @@ -0,0 +1,847 @@ + + + + FUNDSXML_FILE_1 + 2025-10-01T00:00:00 + 4.2.9 + 2025-10-01 + + AT + EURAM + Erste Asset Management GmbH + Asset Manager + + INITIAL + + + + + 529900T8BM49AURSDO55 + + + Erste Responsible Stock Global + + EUR + true + + 2015-03-15 + + + Ongoing Costs + 2025-02-01 + 1.80 + + + + + + + 2025-10-01 + OFFICIAL + + 112500000 + + + + + + 2025-10-01 + + + ID_001 + + US0378331005 + + USD + + 9375000 + + 28.33 + + 50000.00 + + 202.50 + + + + + ID_002 + + NL0010273215 + + EUR + + 8125000 + + 7.22 + + 8750.00 + + 928.57 + + + + + ID_003 + + DE0001102424 + + EUR + + 6250000 + + 5.56 + + 6200000 + + 100.806 + + + + + ID_004 + + XS2444622110 + + EUR + + 6250000 + + 5.56 + + 6300000.00 + + 99.206 + + + + + ID_005 + + IE00B11XZB12 + + EUR + + 10000000 + + 8.89 + + 200000.00 + + 50.00 + + + + + ID_006 + + IE00B5BMR087 + + EUR + + 12500000 + + 11.11 + + 25000.00 + + 500.00 + + + + + ID_007 + + DE000PN738Q3 + + EUR + + 1875000 + + 1.67 + + 150000.00 + + 12.50 + + + + + ID_008 + + DE000SQ962F7 + + EUR + + 1875000 + + 1.67 + + 250000.00 + + 7.50 + + + + + ID_009 + + DE000XM03HC3 + + EUR + + 3125000 + + 2.78 + + 15625.00 + + 200.00 + + + + + ID_010 + + DE000DFM0XG2 + + EUR + + 3125000 + + 2.78 + + 25000.00 + + 125.00 + + + + + ID_011 + + DE000C0B6XV0 + + EUR + + 1250000 + + 1.11 + + + + ID_012 + + DE000C0B6YC8 + + EUR + + 1250000 + + 1.11 + + + + ID_013 + + DE000C0B6ZR4 + + EUR + + 2500000 + + 2.22 + + 50 + + + + ID_014 + + XS2123456789 + + EUR + + 2500000 + + 2.22 + + 20 + + + + ID_015 + EUR + + 5000000 + + 4.44 + + + + ID_016 + EUR + + 4375000 + + 3.89 + + + + ID_017 + EUR + + 6250000 + + 5.56 + + + + ID_018 + EUR + + 5625000 + + 5.00 + + + + ID_019 + EUR + + 3750000 + + 3.33 + + + + ID_020 + EUR + + 10625000 + + 9.44 + + + + ID_021 + EUR + + 6875000 + + 6.11 + + + + + + + + + + + AT0000A2QM74 + + + Erste Responsible Stock Global USD R01 + + USD + + + C + 2025-10-01 + USD + OFFICIAL + 142.87 + + + + + 2025-10-01 + OFFICIAL + + 78937500 + 85722000 + + 600000 + + + + + + AT0000A2QM66 + + + Erste Responsible Stock Global EUR R01 + + EUR + + + C + 2025-10-01 + EUR + OFFICIAL + 134.25 + + + + + 2025-10-01 + OFFICIAL + + 33562500 + + 250000 + + + + + + + + + + ID_001 + + US0378331005 + + USD + US + APPLE INC. + EQ + + + + + HWUPKR0MPOU8FGXBT394 + + APPLE INC. + + + + + + ID_002 + + NL0010273215 + + EUR + NL + ASML HOLDING N.V. + EQ + + + + + 724500Y6P2B0MRLY2T56 + + ASML HOLDING N.V. + + + + + + ID_003 + + DE0001102424 + + EUR + DE + Germany, Federal Republic of 1.70% 15/Aug/2032 + BO + + + false + + + 529900T24R02QROQ3G03 + + Germany, Federal Republic of + + 2032-08-15 + + fix + 1.70 + + + + + + ID_004 + + XS2444622110 + + EUR + DE + Siemens Financieringsmaatschappij N.V. 0.75% 12/Feb/2030 + BO + + + false + + + 549300KL4K15T62YKV69 + + Siemens Financieringsmaatschappij N.V. + + 2030-02-12 + + fix + 0.75 + + + + + + ID_005 + + IE00B11XZB12 + + EUR + IE + PIMCO GIS Global Bond Fund E Class EUR (Acc) + SC + + + + + 635400MD3E0L5P9ZC956 + + PIMCO Global Advisors (Ireland) Limited + + + + + + ID_006 + + IE00B5BMR087 + + EUR + IE + Vanguard S&P 500 UCITS ETF EUR (Acc) + SC + + + + + 2138006E5Q6H983A2A43 + + The Vanguard Group (Ireland) Limited + + + + + + ID_007 + + DE000PN738Q3 + + EUR + FR + BNP Paribas Call Warrant on LVMH 2026 + WA + + + + + R0MI4MICVL8SP8AU2V54 + + BNP Paribas S.A. + + 100 + 2026-12-18 + CALL + + 800 + + + + + + ID_008 + + DE000SQ962F7 + + EUR + FR + Societe Generale Put Warrant on SAP 2026 + WA + + + + + O2RNE8IBXP4R0TD8PU41 + + Societe Generale S.A. + + 10 + 2026-06-19 + PUT + + 150 + + + + + + ID_009 + + DE000XM03HC3 + + EUR + DE + X-markets Index Certificate on DAX + CE + + + Index + + + + + ID_010 + + DE000DFM0XG2 + + EUR + DE + DZ BANK Bonus Certificate on Euro Stoxx 50 + CE + + + Security + XEUR + + + + + ID_011 + + DE000C0B6XV0 + + EUR + DE + Call Option on Allianz SE + OP + + + + + + ID_012 + + DE000C0B6YC8 + + EUR + DE + Put Option on Volkswagen AG + OP + + + + + + ID_013 + + DE000C0B6ZR4 + + EUR + DE + EURO STOXX 50 Index Future Dec 2025 + FU + + + IF + 10 + 2025-12-19 + XEUR + + + + + ID_014 + + XS2123456789 + + EUR + EUR/USD Currency Future Mar 2026 + FU + + + CF + 125000 + 2026-03-16 + XCME + + + + + ID_015 + EUR + FX Forward EUR/USD Dec 2025 + FX + + + EUR + 5000000 + USD + 5400000 + 2025-12-30 + + + 7LTWFZYKSA4V22TE2Y10 + + Goldman Sachs Bank Europe SE + + + + + + ID_016 + EUR + FX Forward EUR/GBP Jan 2026 + FX + + + EUR + 4375000 + GBP + 3718750 + 2026-01-30 + + + G5GSEF7VJP5I7OUK5573 + + HSBC Bank PLC + + + + + + ID_017 + EUR + EUR 5Y Interest Rate Swap + SW + + + Interestrateswap + 2030-10-01 + + + 549300V65Q6MY42G2B26 + + J.P. Morgan SE + + + + SELL + EUR + 6250000 + 30/360 + + + BUY + EUR + 6250000 +Fixed Rate + + + + + + + ID_018 + EUR + Cross Currency Swap EUR/CHF + SW + + + Currencyswap + 2028-10-01 + + + BFM8T61CT2L1QCEMIK50 + + UBS Europe SE + + + + + + ID_019 + EUR + Repo German Government Bond 2.50% 2027 + RP + + + BSB + 2025-10-01 + 100 + 2025-10-08 + + + 851WYGNLU20A61V15G44 + + Commerzbank AG + + XFRA + Bond + + None + None + + + + + ID_020 + EUR + Office Building Vienna Donaucity + RE + + + + + + ID_021 + EUR + Call Money Raiffeisen Bank International + CM + + + 2025-09-15 + 2025-10-15 + + + 894500CK24P2281L1O36 + + Raiffeisen Bank International AG + + + + + + diff --git a/tests/fixtures/invalid/xsd-invalid_Positions.xml b/tests/fixtures/invalid/xsd-invalid_Positions.xml new file mode 100644 index 0000000..0d42e6b --- /dev/null +++ b/tests/fixtures/invalid/xsd-invalid_Positions.xml @@ -0,0 +1,847 @@ + + + + FUNDSXML_FILE_1 + 2025-10-01T00:00:00 + 4.2.9 + 2025-10-01 + + AT + EURAM + Erste Asset Management GmbH + Asset Manager + + INITIAL + + + + + 529900T8BM49AURSDO55 + + + Erste Responsible Stock Global + + EUR + true + + 2015-03-15 + + + Ongoing Costs + 2025-02-01 + 1.80 + + + + + + + 2025-10-01 + OFFICIAL + + 112500000 + + + + + + 2025-10-01 + + + ID_001 + + US0378331005 + + USD + + NINE-MILLION + + 8.33 + + 50000.00 + + 202.50 + + + + + ID_002 + + NL0010273215 + + EUR + + 8125000 + + 7.22 + + 8750.00 + + 928.57 + + + + + ID_003 + + DE0001102424 + + EUR + + 6250000 + + 5.56 + + 6200000 + + 100.806 + + + + + ID_004 + + XS2444622110 + + EUR + + 6250000 + + 5.56 + + 6300000.00 + + 99.206 + + + + + ID_005 + + IE00B11XZB12 + + EUR + + 10000000 + + 8.89 + + 200000.00 + + 50.00 + + + + + ID_006 + + IE00B5BMR087 + + EUR + + 12500000 + + 11.11 + + 25000.00 + + 500.00 + + + + + ID_007 + + DE000PN738Q3 + + EUR + + 1875000 + + 1.67 + + 150000.00 + + 12.50 + + + + + ID_008 + + DE000SQ962F7 + + EUR + + 1875000 + + 1.67 + + 250000.00 + + 7.50 + + + + + ID_009 + + DE000XM03HC3 + + EUR + + 3125000 + + 2.78 + + 15625.00 + + 200.00 + + + + + ID_010 + + DE000DFM0XG2 + + EUR + + 3125000 + + 2.78 + + 25000.00 + + 125.00 + + + + + ID_011 + + DE000C0B6XV0 + + EUR + + 1250000 + + 1.11 + + + + ID_012 + + DE000C0B6YC8 + + EUR + + 1250000 + + 1.11 + + + + ID_013 + + DE000C0B6ZR4 + + EUR + + 2500000 + + 2.22 + + 50 + + + + ID_014 + + XS2123456789 + + EUR + + 2500000 + + 2.22 + + 20 + + + + ID_015 + EUR + + 5000000 + + 4.44 + + + + ID_016 + EUR + + 4375000 + + 3.89 + + + + ID_017 + EUR + + 6250000 + + 5.56 + + + + ID_018 + EUR + + 5625000 + + 5.00 + + + + ID_019 + EUR + + 3750000 + + 3.33 + + + + ID_020 + EUR + + 10625000 + + 9.44 + + + + ID_021 + EUR + + 6875000 + + 6.11 + + + + + + + + + + + AT0000A2QM74 + + + Erste Responsible Stock Global USD R01 + + USD + + + C + 2025-10-01 + USD + OFFICIAL + 142.87 + + + + + 2025-10-01 + OFFICIAL + + 78937500 + 85722000 + + 600000 + + + + + + AT0000A2QM66 + + + Erste Responsible Stock Global EUR R01 + + EUR + + + C + 2025-10-01 + EUR + OFFICIAL + 134.25 + + + + + 2025-10-01 + OFFICIAL + + 33562500 + + 250000 + + + + + + + + + + ID_001 + + US0378331005 + + USD + US + APPLE INC. + EQ + + + + + HWUPKR0MPOU8FGXBT394 + + APPLE INC. + + + + + + ID_002 + + NL0010273215 + + EUR + NL + ASML HOLDING N.V. + EQ + + + + + 724500Y6P2B0MRLY2T56 + + ASML HOLDING N.V. + + + + + + ID_003 + + DE0001102424 + + EUR + DE + Germany, Federal Republic of 1.70% 15/Aug/2032 + BO + + + false + + + 529900T24R02QROQ3G03 + + Germany, Federal Republic of + + 2032-08-15 + + fix + 1.70 + + + + + + ID_004 + + XS2444622110 + + EUR + DE + Siemens Financieringsmaatschappij N.V. 0.75% 12/Feb/2030 + BO + + + false + + + 549300KL4K15T62YKV69 + + Siemens Financieringsmaatschappij N.V. + + 2030-02-12 + + fix + 0.75 + + + + + + ID_005 + + IE00B11XZB12 + + EUR + IE + PIMCO GIS Global Bond Fund E Class EUR (Acc) + SC + + + + + 635400MD3E0L5P9ZC956 + + PIMCO Global Advisors (Ireland) Limited + + + + + + ID_006 + + IE00B5BMR087 + + EUR + IE + Vanguard S&P 500 UCITS ETF EUR (Acc) + SC + + + + + 2138006E5Q6H983A2A43 + + The Vanguard Group (Ireland) Limited + + + + + + ID_007 + + DE000PN738Q3 + + EUR + FR + BNP Paribas Call Warrant on LVMH 2026 + WA + + + + + R0MI4MICVL8SP8AU2V54 + + BNP Paribas S.A. + + 100 + 2026-12-18 + CALL + + 800 + + + + + + ID_008 + + DE000SQ962F7 + + EUR + FR + Societe Generale Put Warrant on SAP 2026 + WA + + + + + O2RNE8IBXP4R0TD8PU41 + + Societe Generale S.A. + + 10 + 2026-06-19 + PUT + + 150 + + + + + + ID_009 + + DE000XM03HC3 + + EUR + DE + X-markets Index Certificate on DAX + CE + + + Index + + + + + ID_010 + + DE000DFM0XG2 + + EUR + DE + DZ BANK Bonus Certificate on Euro Stoxx 50 + CE + + + Security + XEUR + + + + + ID_011 + + DE000C0B6XV0 + + EUR + DE + Call Option on Allianz SE + OP + + + + + + ID_012 + + DE000C0B6YC8 + + EUR + DE + Put Option on Volkswagen AG + OP + + + + + + ID_013 + + DE000C0B6ZR4 + + EUR + DE + EURO STOXX 50 Index Future Dec 2025 + FU + + + IF + 10 + 2025-12-19 + XEUR + + + + + ID_014 + + XS2123456789 + + EUR + EUR/USD Currency Future Mar 2026 + FU + + + CF + 125000 + 2026-03-16 + XCME + + + + + ID_015 + EUR + FX Forward EUR/USD Dec 2025 + FX + + + EUR + 5000000 + USD + 5400000 + 2025-12-30 + + + 7LTWFZYKSA4V22TE2Y10 + + Goldman Sachs Bank Europe SE + + + + + + ID_016 + EUR + FX Forward EUR/GBP Jan 2026 + FX + + + EUR + 4375000 + GBP + 3718750 + 2026-01-30 + + + G5GSEF7VJP5I7OUK5573 + + HSBC Bank PLC + + + + + + ID_017 + EUR + EUR 5Y Interest Rate Swap + SW + + + Interestrateswap + 2030-10-01 + + + 549300V65Q6MY42G2B26 + + J.P. Morgan SE + + + + SELL + EUR + 6250000 + 30/360 + + + BUY + EUR + 6250000 +Fixed Rate + + + + + + + ID_018 + EUR + Cross Currency Swap EUR/CHF + SW + + + Currencyswap + 2028-10-01 + + + BFM8T61CT2L1QCEMIK50 + + UBS Europe SE + + + + + + ID_019 + EUR + Repo German Government Bond 2.50% 2027 + RP + + + BSB + 2025-10-01 + 100 + 2025-10-08 + + + 851WYGNLU20A61V15G44 + + Commerzbank AG + + XFRA + Bond + + None + None + + + + + ID_020 + EUR + Office Building Vienna Donaucity + RE + + + + + + ID_021 + EUR + Call Money Raiffeisen Bank International + CM + + + 2025-09-15 + 2025-10-15 + + + 894500CK24P2281L1O36 + + Raiffeisen Bank International AG + + + + + + diff --git a/tools/fetch-tools.sh b/tools/fetch-tools.sh new file mode 100755 index 0000000..9eaa120 --- /dev/null +++ b/tools/fetch-tools.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# fetch-tools.sh — Fetch the Java XML toolchain jars used by the examples. +# +# Why: this repo's Schematron uses queryBinding="xslt2" and the Basic/Custom +# XSLT reports are XSLT 2.0 — both need a Saxon (XSLT 2.0/3.0) processor, which +# is not part of a base OS install. SchXslt compiles ISO Schematron to XSLT. +# +# Two independent classpaths are produced, on purpose: +# * SAXON_CP — Saxon-HE + xmlresolver, for XSLT 2.0 transforms. +# * SCHXSLT_CP — the SchXslt CLI (a self-contained jar that already bundles +# its own Saxon) + commons-cli + slf4j. Mixing the standalone +# Saxon-HE-12.5 into this classpath breaks SchXslt: the two +# Saxons need different org.xmlresolver APIs. +# +# All jars come from Maven Central (curl honours https_proxy / HTTPS_PROXY for +# locked-down enterprise networks). Jars land in .lib/ (gitignored) — they are +# external dependencies, not repo source. +# +# Usage: tools/fetch-tools.sh +# Output: SAXON_CP=... and SCHXSLT_CP=... (consumed by the invocation scripts). +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LIB="${REPO_ROOT}/.lib" +mkdir -p "$LIB" +M2="https://repo1.maven.org/maven2" + +fetch() { + local url="$1" out="$2" + if [[ -s "$out" ]]; then echo "cached: $out" >&2; return 0; fi + echo "fetch: $url" >&2 + curl -sSL --fail -m 120 "$url" -o "$out" +} + +# --- Saxon-HE (XSLT 2.0/3.0) ------------------------------------------------- +fetch "${M2}/net/sf/saxon/Saxon-HE/12.5/Saxon-HE-12.5.jar" "${LIB}/Saxon-HE-12.5.jar" +# Saxon 12.x requires org.xmlresolver at runtime (CatalogResourceResolver). +fetch "${M2}/org/xmlresolver/xmlresolver/5.2.2/xmlresolver-5.2.2.jar" "${LIB}/xmlresolver-5.2.2.jar" +fetch "${M2}/org/xmlresolver/xmlresolver/5.2.2/xmlresolver-5.2.2-data.jar" "${LIB}/xmlresolver-5.2.2-data.jar" + +# --- SchXslt CLI (self-contained: bundles its own Saxon + XSLT) -------------- +fetch "${M2}/name/dmaus/schxslt/cli/1.10.1/cli-1.10.1.jar" "${LIB}/schxslt-cli-1.10.1.jar" +fetch "${M2}/commons-cli/commons-cli/1.5.0/commons-cli-1.5.0.jar" "${LIB}/commons-cli-1.5.0.jar" +fetch "${M2}/org/slf4j/slf4j-api/1.7.32/slf4j-api-1.7.32.jar" "${LIB}/slf4j-api-1.7.32.jar" +fetch "${M2}/org/slf4j/slf4j-nop/1.7.32/slf4j-nop-1.7.32.jar" "${LIB}/slf4j-nop-1.7.32.jar" + +echo "SAXON_CP=${LIB}/Saxon-HE-12.5.jar:${LIB}/xmlresolver-5.2.2.jar:${LIB}/xmlresolver-5.2.2-data.jar" +echo "SCHXSLT_CP=${LIB}/schxslt-cli-1.10.1.jar:${LIB}/commons-cli-1.5.0.jar:${LIB}/slf4j-api-1.7.32.jar:${LIB}/slf4j-nop-1.7.32.jar" From 4eb18ee86090a9feb3611bebb1b0f50fada3616d Mon Sep 17 00:00:00 2001 From: Karl Kauc Date: Fri, 15 May 2026 15:57:13 +0200 Subject: [PATCH 3/3] XSLT showcase (custom DQ, factsheet HTML/PDF, CSV) + CI pipeline XSLT_DataQuality_Checks/Custom_Internal_Checks/: XSLT 2.0 house rules (AssetType whitelist, ID convention, concentration limit, OTC LEI) -> HTML. XSLT_Transformations/: factsheet_html.xslt, factsheet_fo.xslt (XSL-FO -> FOP PDF), positions_csv.xslt (RFC-4180). invocation/ runs them via Saxon CLI and native Java s9api (both verified) plus Python/Node references. .github/workflows/ci.yml: fetches official schemas + Java toolchain, XSD- validates all 7 samples, asserts both negative fixtures fail, runs Schematron (canonical passes / fixture fails), smoke-tests all transforms, regression- runs the legacy XSLT 1.0 report. All steps simulated green locally. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 93 ++++++++++++ .gitignore | 4 + .../Custom_Internal_Checks/README.md | 34 +++++ .../custom_internal_checks.xslt | 141 ++++++++++++++++++ .../CSV_Export/positions_csv.xslt | 44 ++++++ .../Factsheet/factsheet_fo.xslt | 102 +++++++++++++ .../Factsheet/factsheet_html.xslt | 97 ++++++++++++ XSLT_Transformations/README.md | 48 ++++++ .../invocation/RunTransform.java | 49 ++++++ .../invocation/run-transform.sh | 29 ++++ .../invocation/run_transform.py | 39 +++++ 11 files changed, 680 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 XSLT_DataQuality_Checks/Custom_Internal_Checks/README.md create mode 100644 XSLT_DataQuality_Checks/Custom_Internal_Checks/custom_internal_checks.xslt create mode 100644 XSLT_Transformations/CSV_Export/positions_csv.xslt create mode 100644 XSLT_Transformations/Factsheet/factsheet_fo.xslt create mode 100644 XSLT_Transformations/Factsheet/factsheet_html.xslt create mode 100644 XSLT_Transformations/README.md create mode 100644 XSLT_Transformations/invocation/RunTransform.java create mode 100755 XSLT_Transformations/invocation/run-transform.sh create mode 100644 XSLT_Transformations/invocation/run_transform.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..54defb2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,93 @@ +name: FundsXML CI + +# Validates every committed sample against the official released XSD and the +# Schematron ruleset, runs the XSLT transforms as a smoke test, and asserts the +# negative fixtures still fail. Positive samples must stay green. +# +# Security note: no untrusted GitHub event data (issue/PR/commit text) is used +# in any run: step, so there is no workflow-injection surface here. + +on: + push: + branches: ["**"] + pull_request: + workflow_dispatch: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install tooling + run: sudo apt-get update -qq && sudo apt-get install -y -qq libxml2-utils xsltproc + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Fetch official schemas (4.2.9, 4.1.0, 4.0.0) + run: | + for v in 4.2.9 4.1.0 4.0.0; do tools/fetch-schema.sh "$v"; done + + - name: XSD - all positive samples must validate + run: | + set -e + declare -A S=( + ["4.2.9"]="positions/Mixed-Fund_Positions transactions/Fund_Transactions documents/Fund_Documents regulatory/EFT_Regulatory signed/Signed_Fund_Skeleton" + ["4.1.0"]="positions/Equity-Fund_Positions" + ["4.0.0"]="positions/Equity-Fund_Positions" + ) + for v in "${!S[@]}"; do + for f in ${S[$v]}; do + echo "XSD $v/$f" + xmllint --noout --nonet --schema ".schema-cache/$v/FundsXML.xsd" \ + "FundsXML_Files/$v/$f.xml" + done + done + + - name: XSD - negative fixture must FAIL + run: | + if xmllint --noout --nonet --schema .schema-cache/4.2.9/FundsXML.xsd \ + tests/fixtures/invalid/xsd-invalid_Positions.xml 2>/dev/null; then + echo "::error::xsd-invalid fixture unexpectedly validated"; exit 1 + fi + echo "xsd-invalid correctly rejected" + + - name: Fetch Java toolchain (Saxon + SchXslt) + run: tools/fetch-tools.sh + + - name: Schematron - canonical sample passes, fixture fails + run: | + RS=Schematron_DataQuality_Checks/Basic_Checks/invocation/run-schematron.sh + "$RS" FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml + if "$RS" tests/fixtures/invalid/schematron-invalid_Positions.xml; then + echo "::error::schematron-invalid fixture unexpectedly passed"; exit 1 + fi + echo "schematron-invalid correctly failed" + + - name: Smoke - XSLT transforms produce non-empty output + run: | + set -e + RT=XSLT_Transformations/invocation/run-transform.sh + SRC=FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml + "$RT" XSLT_DataQuality_Checks/Custom_Internal_Checks/custom_internal_checks.xslt "$SRC" out_dq.html + "$RT" XSLT_Transformations/Factsheet/factsheet_html.xslt "$SRC" out_fs.html + "$RT" XSLT_Transformations/Factsheet/factsheet_fo.xslt "$SRC" out_fs.fo + "$RT" XSLT_Transformations/CSV_Export/positions_csv.xslt "$SRC" out_pos.csv + xmllint --noout out_fs.fo + for f in out_dq.html out_fs.html out_pos.csv; do + test -s "$f" || { echo "::error::$f is empty"; exit 1; } + done + test "$(wc -l < out_pos.csv)" -ge 2 + + - name: Regression - legacy XSLT 1.0 report still runs + run: | + xsltproc XSLT_DataQuality_Checks/Enhanced_Check/FundsXML_CompleteDQReport_HTML.xsl \ + FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml > out_enh.html + test -s out_enh.html diff --git a/.gitignore b/.gitignore index d938041..77c3020 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,7 @@ report.html full_report.html validation_report.xml out/ +out_*.html +out_*.fo +out_*.csv +*.svrl diff --git a/XSLT_DataQuality_Checks/Custom_Internal_Checks/README.md b/XSLT_DataQuality_Checks/Custom_Internal_Checks/README.md new file mode 100644 index 0000000..29c94cd --- /dev/null +++ b/XSLT_DataQuality_Checks/Custom_Internal_Checks/README.md @@ -0,0 +1,34 @@ +# Custom Internal DQ Checks + +![XSLT](https://img.shields.io/badge/XSLT-2.0-blue) ![status](https://img.shields.io/badge/status-verified-brightgreen) + +Example **company-internal** house rules layered on top of XSD + Schematron +validation — the kind of checks an asset manager adds beyond the FundsXML +standard. XSLT 2.0, self-contained HTML report. + +| Rule | Description | Default | +|------|-------------|---------| +| R1 | AssetType whitelist — only approved asset types held | `EQ BO SC OP FU FX SW WA CE AC RP RE CM` (param `allowedAssetTypes`) | +| R2 | Position `UniqueID` convention — `ID_<≥3 digits>` | — | +| R3 | Concentration limit — no single position over the cap | 20% (param `concentrationLimitPct`) | +| R4 | OTC derivative (FX, SW) must carry a counterparty LEI | — | + +On the canonical 4.2.9 sample all four rules **PASS** (verified). + +## Run + +```bash +tools/fetch-tools.sh +XSLT_Transformations/invocation/run-transform.sh \ + XSLT_DataQuality_Checks/Custom_Internal_Checks/custom_internal_checks.xslt \ + FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml custom_dq.html + +# Tighten the concentration limit to 5% to see R3 fail: +XSLT_Transformations/invocation/run-transform.sh \ + XSLT_DataQuality_Checks/Custom_Internal_Checks/custom_internal_checks.xslt \ + FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml custom_dq.html \ + "concentrationLimitPct=5" +``` + +XSLT 2.0 → use Saxon (see [`../../XSLT_Transformations/`](../../XSLT_Transformations/) +for per-stack invocation). `xsltproc`/`lxml` (XSLT 1.0) cannot run it. diff --git a/XSLT_DataQuality_Checks/Custom_Internal_Checks/custom_internal_checks.xslt b/XSLT_DataQuality_Checks/Custom_Internal_Checks/custom_internal_checks.xslt new file mode 100644 index 0000000..31da497 --- /dev/null +++ b/XSLT_DataQuality_Checks/Custom_Internal_Checks/custom_internal_checks.xslt @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + FundsXML Custom Internal DQ Report + + + +

Custom Internal Data-Quality Report

+

+ Document: + · Version: + · Generated: +

+ + +

R1 — AssetType whitelist

+ + + +

PASS — all asset types within the approved set + ().

+
+ +

FAIL — asset(s) with a non-approved type.

+ + + + + + +
UniqueIDNameAssetType
+
+
+ + +

R2 — Position UniqueID convention (ID_<digits>)

+ + + +

PASS — all position IDs follow the convention.

+
+ +

FAIL — non-conforming ID(s).

+ + +
UniqueID
+
+
+ + +

R3 — Concentration limit (max % per position)

+ + + +

PASS — no position exceeds the concentration limit.

+
+ +

FAIL — position(s) over the limit.

+ + + + + +
UniqueIDTotalPercentage
%
+
+
+ + +

R4 — OTC derivative counterparty LEI (FX, SW)

+ + + +

PASS — every OTC derivative has a counterparty LEI.

+
+ +

FAIL — OTC asset(s) missing counterparty LEI.

+ + + + + + +
UniqueIDNameAssetType
+
+
+ +
+

+ Generated by custom_internal_checks.xslt (XSLT 2.0). House rules only — + run alongside XSD and Schematron validation, not instead of them. +

+ + +
+
diff --git a/XSLT_Transformations/CSV_Export/positions_csv.xslt b/XSLT_Transformations/CSV_Export/positions_csv.xslt new file mode 100644 index 0000000..95152b8 --- /dev/null +++ b/XSLT_Transformations/CSV_Export/positions_csv.xslt @@ -0,0 +1,44 @@ + + + + + + + + + + + + + FundLEI,DocumentID,UniqueID,ISIN,AssetName,AssetType,Currency,ValueFundCcy,Percentage + + + + + + + + diff --git a/XSLT_Transformations/Factsheet/factsheet_fo.xslt b/XSLT_Transformations/Factsheet/factsheet_fo.xslt new file mode 100644 index 0000000..63deefc --- /dev/null +++ b/XSLT_Transformations/Factsheet/factsheet_fo.xslt @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + LEI · + Base currency · + NAV date + + + Key figures + + Total net assets: +   · + Positions: · + Share classes: + + + Top 10 holdings + + + + + + + + + # + ISIN / ID + Name + Value + Wt. + + + + + + + + + + + + + + + + + + + + + Generated from FundsXML by factsheet_fo.xslt (XSLT 2.0 -> XSL-FO -> Apache FOP). Illustrative — not investment advice. + + + + + + diff --git a/XSLT_Transformations/Factsheet/factsheet_html.xslt b/XSLT_Transformations/Factsheet/factsheet_html.xslt new file mode 100644 index 0000000..12ae325 --- /dev/null +++ b/XSLT_Transformations/Factsheet/factsheet_html.xslt @@ -0,0 +1,97 @@ + + + + + + + + + + + + + Factsheet — <xsl:value-of select="$fund/Names/OfficialName"/> + + + +
+

+
+ LEI · + Base currency · + NAV date +
+
+ +
+
Total Net Assets
+
 
+
Positions
+
+
Share classes
+
+
Ongoing costs
+
%
+
+ +

Share classes

+ + + + + + + + +
ISINNameCcyNAV price
+ +

Top 10 holdings

+ + + + + + + + + + + + + + + +
#ISIN / IDNameValue ()Weight
+ +

+ Generated from FundsXML by factsheet_html.xslt (XSLT 2.0). Illustrative — not investment advice. +

+ + +
+
diff --git a/XSLT_Transformations/README.md b/XSLT_Transformations/README.md new file mode 100644 index 0000000..9e92d95 --- /dev/null +++ b/XSLT_Transformations/README.md @@ -0,0 +1,48 @@ +# XSLT Transformations + +Company-facing transformations of FundsXML positions data. All stylesheets are +**XSLT 2.0** — run with Saxon (`xsltproc`/`lxml` are XSLT 1.0 and will not work). +`tools/fetch-tools.sh` provides Saxon-HE in `.lib/`. + +| Output | Stylesheet | Notes | +|--------|-----------|-------| +| HTML factsheet | [`Factsheet/factsheet_html.xslt`](Factsheet/factsheet_html.xslt) | header, KPIs, share classes, top-10 holdings | +| PDF factsheet | [`Factsheet/factsheet_fo.xslt`](Factsheet/factsheet_fo.xslt) | emits XSL-FO → render with Apache FOP: `fop -fo out.fo -pdf out.pdf` | +| Positions CSV | [`CSV_Export/positions_csv.xslt`](CSV_Export/positions_csv.xslt) | RFC-4180 quoting; `delimiter` param | + +Company-internal DQ rules live next door in +[`../XSLT_DataQuality_Checks/Custom_Internal_Checks/`](../XSLT_DataQuality_Checks/Custom_Internal_Checks/). + +## Run (per stack) + +| Stack | Entry point | Status | +|-------|-------------|--------| +| CLI (Saxon) | [`invocation/run-transform.sh`](invocation/run-transform.sh) | ✅ verified | +| Java (s9api, no JAXB) | [`invocation/RunTransform.java`](invocation/RunTransform.java) | ✅ verified | +| Python | [`invocation/run_transform.py`](invocation/run_transform.py) | needs `pip install saxonche` | +| Node.js | see below | needs `npm i xslt3` (saxon-js) | + +```bash +# CLI — HTML factsheet +XSLT_Transformations/invocation/run-transform.sh \ + XSLT_Transformations/Factsheet/factsheet_html.xslt \ + FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml factsheet.html + +# CLI — CSV with semicolon delimiter (parameter pass-through) +XSLT_Transformations/invocation/run-transform.sh \ + XSLT_Transformations/CSV_Export/positions_csv.xslt \ + FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml positions.csv "delimiter=;" + +# PDF — two steps (Saxon then Apache FOP) +XSLT_Transformations/invocation/run-transform.sh \ + XSLT_Transformations/Factsheet/factsheet_fo.xslt \ + FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml factsheet.fo +fop -fo factsheet.fo -pdf factsheet.pdf + +# Node.js (SaxonJS / xslt3) — XSLT 3.0 engine runs these XSLT 2.0 sheets: +npx xslt3 -xsl:XSLT_Transformations/CSV_Export/positions_csv.xslt \ + -s:FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml -o:positions.csv +``` + +All transforms work unchanged on the 4.1.0 / 4.0.0 samples (backward +compatibility; no XML namespace in FundsXML 4.x). diff --git a/XSLT_Transformations/invocation/RunTransform.java b/XSLT_Transformations/invocation/RunTransform.java new file mode 100644 index 0000000..49e82ce --- /dev/null +++ b/XSLT_Transformations/invocation/RunTransform.java @@ -0,0 +1,49 @@ +// Run an XSLT 2.0 stylesheet via the Saxon s9api (native Java, no JAXB). +// +// tools/fetch-tools.sh +// SCP=.lib/Saxon-HE-12.5.jar:.lib/xmlresolver-5.2.2.jar:.lib/xmlresolver-5.2.2-data.jar +// javac -cp "$SCP" -d /tmp/rt XSLT_Transformations/invocation/RunTransform.java +// java -cp "$SCP:/tmp/rt" RunTransform [k=v ...] +// Exit: 0 success, 2 setup error. +// +// The repo's stylesheets are XSLT 2.0; Saxon supplies the engine. s9api is +// Saxon's idiomatic Java API — no JAXB involved. + +import java.io.File; +import net.sf.saxon.s9api.Processor; +import net.sf.saxon.s9api.QName; +import net.sf.saxon.s9api.Serializer; +import net.sf.saxon.s9api.XdmAtomicValue; +import net.sf.saxon.s9api.XsltCompiler; +import net.sf.saxon.s9api.XsltExecutable; +import net.sf.saxon.s9api.XsltTransformer; + +public class RunTransform { + public static void main(String[] args) { + if (args.length < 3) { + System.err.println("usage: RunTransform [k=v ...]"); + System.exit(2); + } + try { + Processor proc = new Processor(false); + XsltCompiler comp = proc.newXsltCompiler(); + XsltExecutable exe = comp.compile(new javax.xml.transform.stream.StreamSource(new File(args[0]))); + XsltTransformer t = exe.load(); + t.setSource(new javax.xml.transform.stream.StreamSource(new File(args[1]))); + for (int i = 3; i < args.length; i++) { + int eq = args[i].indexOf('='); + if (eq > 0) { + t.setParameter(new QName(args[i].substring(0, eq)), + new XdmAtomicValue(args[i].substring(eq + 1))); + } + } + Serializer out = proc.newSerializer(new File(args[2])); + t.setDestination(out); + t.transform(); + System.out.println("wrote " + args[2]); + } catch (Exception e) { + System.err.println("error: " + e.getMessage()); + System.exit(2); + } + } +} diff --git a/XSLT_Transformations/invocation/run-transform.sh b/XSLT_Transformations/invocation/run-transform.sh new file mode 100755 index 0000000..9ca0642 --- /dev/null +++ b/XSLT_Transformations/invocation/run-transform.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# Run any XSLT 2.0 stylesheet in this repo via Saxon-HE (CLI). +# +# Usage: run-transform.sh [name=value ...] +# Exit: 0 on success, 2 on setup error, Saxon's code otherwise. +# +# The Custom_Internal_Checks / Factsheet / CSV stylesheets are XSLT 2.0, so an +# XSLT 2.0/3.0 engine (Saxon) is required — xsltproc/lxml (XSLT 1.0) will not +# work. tools/fetch-tools.sh provides Saxon-HE in .lib/. +set -euo pipefail + +XSL="${1:?usage: run-transform.sh [params...]}" +IN="${2:?input xml required}" +OUT="${3:?output path required}" +shift 3 || true + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +LIB="${REPO_ROOT}/.lib" +if [[ ! -f "${LIB}/Saxon-HE-12.5.jar" ]]; then + echo "Saxon not present; fetching..." >&2 + "${REPO_ROOT}/tools/fetch-tools.sh" >/dev/null +fi + +# Saxon 12.x needs xmlresolver on the classpath (CatalogResourceResolver). +SCP="${LIB}/Saxon-HE-12.5.jar:${LIB}/xmlresolver-5.2.2.jar:${LIB}/xmlresolver-5.2.2-data.jar" + +java -cp "$SCP" net.sf.saxon.Transform \ + -s:"$IN" -xsl:"$XSL" -o:"$OUT" "$@" +echo "wrote $OUT" diff --git a/XSLT_Transformations/invocation/run_transform.py b/XSLT_Transformations/invocation/run_transform.py new file mode 100644 index 0000000..d23b520 --- /dev/null +++ b/XSLT_Transformations/invocation/run_transform.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Run an XSLT 2.0 stylesheet via saxonche (Saxon's Python API). + +Usage: python run_transform.py [k=v ...] +Exit: 0 success, 2 setup error. + +The repo's stylesheets are XSLT 2.0. `lxml` only supports XSLT 1.0 and cannot +run them — `pip install saxonche` provides Saxon's XSLT 3.0 engine. +""" +import sys + + +def main() -> int: + if len(sys.argv) < 4: + print("usage: run_transform.py [k=v ...]", + file=sys.stderr) + return 2 + xsl, src, out = sys.argv[1:4] + params = dict(p.split("=", 1) for p in sys.argv[4:] if "=" in p) + + try: + from saxonche import PySaxonProcessor + except ImportError: + print("saxonche not installed. Run: pip install saxonche", + file=sys.stderr) + return 2 + + with PySaxonProcessor(license=False) as proc: + xslt = proc.new_xslt30_processor() + for k, v in params.items(): + xslt.set_parameter(k, proc.make_string_value(v)) + xslt.transform_to_file(source_file=src, stylesheet_file=xsl, + output_file=out) + print(f"wrote {out}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())