Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,26 @@ jobs:
xmllint --noout --nonet --schema .schema-cache/4.2.9/FundsXML.xsd chunks/chunk-0001.xml
python3 $P/delta_diff.py big.xml big.xml # identical -> exit 0

- name: Data binding / JSON - round-trip + native Java binding
run: |
set -e
python3 -m pip install --quiet lxml
SRC=FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml
python3 Data_Binding_JSON/python/fundsxml_json.py roundtrip "$SRC" rj.xml
xmllint --noout --nonet --schema .schema-cache/4.2.9/FundsXML.xsd rj.xml
python3 - "$SRC" rj.xml <<'PY'
import re, sys
o, r = (open(p).read() for p in sys.argv[1:3])
nav = lambda x: re.search(r'<TotalNetAssetValue>\s*<Amount ccy="EUR">([0-9.]+)', x).group(1)
npos = lambda x: len(re.findall(r'<Position>', x))
assert abs(float(nav(o)) - float(nav(r))) < 0.01, (nav(o), nav(r))
assert npos(o) == npos(r), (npos(o), npos(r))
print("JSON round-trip preserved:", nav(r), npos(r))
PY
javac -d /tmp/nb Data_Binding_JSON/java/NativeBinding.java
java -cp /tmp/nb NativeBinding "$SRC" | tee nb.txt
grep -q '^Positions : 21$' nb.txt

- name: Regression - legacy XSLT 1.0 report still runs
run: |
xsltproc XSLT_DataQuality_Checks/Enhanced_Check/FundsXML_CompleteDQReport_HTML.xsl \
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@ big.xml
chunks/
agg.txt
aggj.txt
nb.txt
rj.xml
fund.json
*.db
*.svrl
51 changes: 51 additions & 0 deletions Data_Binding_JSON/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Data Binding & JSON

![status](https://img.shields.io/badge/JSON%20round--trip%20%2B%20Java%20binding-verified-brightgreen)

Two recurring enterprise needs: exposing FundsXML to **JSON** APIs, and
**binding** FundsXML to typed objects.

## FundsXML ⇄ JSON (verified)

[`python/fundsxml_json.py`](python/fundsxml_json.py) — `to-json` / `from-json`
/ `roundtrip` (stdlib + lxml). Stable JSON shape
(`document` / `fund` / `shareClasses` / `positions` / `assets`); `from-json`
produces **XSD-valid** FundsXML 4.2.9. Lossless for the positions core,
intentionally lossy for issuer / derivative / regulatory detail (same scope
boundary as `Database_Integration/`).

```bash
python3 Data_Binding_JSON/python/fundsxml_json.py to-json \
FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml fund.json
python3 Data_Binding_JSON/python/fundsxml_json.py roundtrip \
FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml regenerated.xml
xmllint --noout --schema .schema-cache/4.2.9/FundsXML.xsd regenerated.xml
```

Verified: NAV, position count and percentage-sum preserved through
XML → JSON → XML.

## Binding: native vs. generated

[`java/NativeBinding.java`](java/NativeBinding.java) — **native Java, no JAXB**:
a thin hand-written binding over DOM + XPath into Java `record`s. Verified on
4.2.9 and 4.0.0 (FundsXML 4.x is backward compatible, so one binding spans
versions). This is the recommended default: the FundsXML schema is very large,
so a full generated model is heavy and brittle to maintain.

### Generated-binding references (when you do want codegen)

| Stack | Tool | Command (against the fetched schema) |
|-------|------|--------------------------------------|
| Java | JAXB `xjc` | `xjc -d src -p org.fundsxml.model .schema-cache/4.2.9/FundsXML.xsd` |
| Python | `xsdata` | `xsdata --package fundsxml.model .schema-cache/4.2.9/FundsXML.xsd` |
| .NET | `xsd.exe` / `XmlSerializer` | `xsd.exe /classes /namespace:FundsXml.Model .schema-cache\4.2.9\FundsXML.xsd` |

All three consume the **official released schema** fetched by
`tools/fetch-schema.sh` (which also pulls the imported `xmldsig-core-schema.xsd`
for 4.2.9). Trade-off: generated models are type-safe but regenerate on every
schema bump and produce thousands of classes; the native binding stays small
and version-tolerant. Pick per use case.

CI runs the JSON round-trip (XSD-validated, figures asserted) and the native
Java binding on every push.
98 changes: 98 additions & 0 deletions Data_Binding_JSON/java/NativeBinding.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Native Java data binding for FundsXML — DOM + XPath into records, NO JAXB.
//
// javac -d /tmp/nb Data_Binding_JSON/java/NativeBinding.java
// java -cp /tmp/nb NativeBinding <fundsxml.xml>
//
// Why no JAXB: the FundsXML schema is huge; generating/maintaining a full JAXB
// model is heavy and brittle across versions. For most integration work a thin
// hand-written binding over DOM/XPath is simpler, version-tolerant (FundsXML
// 4.x is backward compatible) and dependency-free. The codegen alternatives
// (JAXB xjc, xsdata, xsd.exe) are documented in the README.
//
// FundsXML 4.x has no XML namespace — XPath uses bare element names.
// Security: namespace-unaware is fine here; DTDs/external entities disabled.

import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class NativeBinding {

// Immutable binding types (Java records).
record Fund(String lei, String name, String currency, double totalNav,
int positionCount) {}
record Position(String uniqueId, String isin, double value,
double percentage, String kind) {}

public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("usage: NativeBinding <fundsxml.xml>");
System.exit(2);
}

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setExpandEntityReferences(false);
Document doc;
try (FileInputStream in = new FileInputStream(args[0])) {
doc = dbf.newDocumentBuilder().parse(in);
}
XPath xp = XPathFactory.newInstance().newXPath();

String ccy = xp.evaluate("/FundsXML4/Funds/Fund/Currency", doc);
Fund fund = new Fund(
xp.evaluate("/FundsXML4/Funds/Fund/Identifiers/LEI", doc),
xp.evaluate("/FundsXML4/Funds/Fund/Names/OfficialName", doc),
ccy,
Double.parseDouble(xp.evaluate(
"/FundsXML4/Funds/Fund/FundDynamicData/TotalAssetValues/"
+ "TotalAssetValue/TotalNetAssetValue/Amount[@ccy='" + ccy
+ "']", doc)),
((Number) xp.evaluate(
"count(//Position)", doc, XPathConstants.NUMBER)).intValue());

NodeList pn = (NodeList) xp.evaluate("//Positions/Position", doc,
XPathConstants.NODESET);
List<Position> positions = new ArrayList<>();
double sumPct = 0;
for (int i = 0; i < pn.getLength(); i++) {
Node p = pn.item(i);
String kind = xp.evaluate(
"local-name(*[local-name()='Equity' or local-name()='Bond' "
+ "or local-name()='ShareClass' or local-name()='Warrant' "
+ "or local-name()='Certificate' or local-name()='Option' "
+ "or local-name()='Future' or local-name()='FXForward' "
+ "or local-name()='Swap' or local-name()='Repo' "
+ "or local-name()='RealEstate' or local-name()='CallMoney'][1])",
p);
double pct = num(xp.evaluate("TotalPercentage", p));
sumPct += pct;
positions.add(new Position(
xp.evaluate("UniqueID", p),
xp.evaluate("Identifiers/ISIN", p),
num(xp.evaluate("TotalValue/Amount[@ccy='" + ccy + "']", p)),
pct, kind));
}

System.out.println("Fund : " + fund.name());
System.out.println("LEI / ccy : " + fund.lei() + " / " + fund.currency());
System.out.printf("Total NAV : %.2f%n", fund.totalNav());
System.out.println("Positions : " + fund.positionCount());
System.out.printf("Sum %% : %.4f%n", sumPct);
System.out.println("First 3 :");
positions.stream().limit(3).forEach(p -> System.out.printf(
" %s %-12s %14.2f %6.2f%% [%s]%n",
p.uniqueId(), p.isin(), p.value(), p.percentage(), p.kind()));
}

private static double num(String s) {
return (s == null || s.isBlank()) ? 0.0 : Double.parseDouble(s.trim());
}
}
Loading
Loading