XML verstehen und verarbeiten¶
Viele Fremdsysteme sprechen XML: SOAP-Dienste, Bank- und Behördenformate, Exportdateien aus Altsystemen. Diese Seite erklärt XML von Grund auf und zeigt, wie du es in KCForms liest, durchsuchst, änderst und selbst baust.
Vorwissen ist nicht nötig. Wenn dir Begriffe wie Variable, Liste oder Schleife noch nichts sagen, lies vorher JavaScript-Grundlagen; die Verwandtschaft zu Objekten steht unter JSON und Objekte verstehen.
Zum Mitmachen
Alle Beispiele laufen in der Skript-Konsole: System-Administration → Werkzeuge & Diagnose → Skript-Konsole. Hineinkopieren, Ausführen klicken, unten die Ausgabe lesen.
Wie ein XML-Dokument aufgebaut ist¶
XML beschreibt Daten mit Etiketten, die man Tags nennt. Jedes Etikett wird geöffnet und wieder geschlossen; dazwischen steht der Inhalt:
Die Bestandteile:
- Element — ein Etikettenpaar samt Inhalt:
<ort>Kiel</ort>. Der Name des Elements istort. - Attribut — eine Zusatzangabe im Start-Tag:
nr="K-1001". - Text — was zwischen Start- und End-Tag steht:
Kiel. - Verschachtelung — Elemente stecken in Elementen.
adresseenthältort. - Wurzel — das äußerste Element, hier
kunde. Davon gibt es genau eines.
Ein Element ohne Inhalt darf man kurz schreiben: <ort/> statt <ort></ort>.
XML oder JSON — derselbe Gedanke, andere Schreibweise¶
Kennst du schon JSON, kommt dir das bekannt vor. Dieselben Daten in beiden Schreibweisen:
| XML | JSON | |
|---|---|---|
| Bezeichnung | <ort>Kiel</ort> |
"ort": "Kiel" |
| Karte in der Karte | <adresse><ort>…</ort></adresse> |
"adresse": { "ort": "…" } |
| Mehrere gleiche Einträge | <position/><position/> |
"positionen": [ … ] |
| Zusatzangaben | Attribute (nr="K-1001") |
gibt es nicht — alles ist ein Feld |
| Sonderzeichen | müssen maskiert werden (&) |
nur Anführungszeichen |
Zwei Unterschiede sind im Alltag wichtig:
- XML kennt Attribute und Text — JSON kennt nur Werte.
- XML sagt dir nicht, ob etwas eine Liste ist. Ein einzelnes
<position>sieht genauso aus wie das erste von zwanzig. Deshalb fragt man in KCForms mitfindAll(...)nach allen und schaut auflength.
Vom Text zum Knoten¶
Ein XML-Dokument kommt immer als Text an — aus einer Datei, aus einer
SOAP-Antwort, aus einem Formularfeld. xml.parse(text) macht daraus einen
Knoten, in dem du dich bewegen kannst:
var doc = xml.parse("<kunde nr='K-1001'><name>Meyer</name></kunde>");
log.info("Wurzel: " + doc.name); // Wurzel: kunde
log.info("Nummer: " + doc.attr("nr")); // Nummer: K-1001
Jeder Knoten hat fünf Eigenschaften und ein paar Handgriffe:
| Eigenschaft | Was sie liefert |
|---|---|
node.name |
der Name des Elements (nur lesen) |
node.text |
der Textinhalt — lesen und setzen |
node.attrs |
alle Attribute |
node.children |
die direkten Kind-Elemente als Liste |
node.parent |
das Element eine Ebene höher |
| Handgriff | Was er tut |
|---|---|
node.find(tag) |
das erste direkte Kind mit diesem Namen, sonst null |
node.findAll(tag) |
alle direkten Kinder mit diesem Namen, als Liste |
node.findDeep(tag) |
das erste Element im ganzen Teilbaum |
node.findAllDeep(tag) |
alle Elemente im ganzen Teilbaum |
node.attr(name) |
Attribut lesen (null, wenn es fehlt) |
node.attr(name, wert) |
Attribut setzen; null als Wert entfernt es |
node.addChild(tag[, text]) |
neues Kind anlegen, liefert das neue Element |
node.removeChild(kind) |
ein Kind entfernen |
xml.stringify(node[, true]) |
zurück in Text; true = eingerückt |
Werte herauslesen¶
find steigt eine Ebene hinab, .text holt den Inhalt:
var doc = xml.parse(
"<kunde><name>Meyer</name><adresse><ort>Kiel</ort></adresse></kunde>");
var name = doc.find("name").text; // Meyer
var ort = doc.find("adresse").find("ort").text; // Kiel
log.info(name + " aus " + ort);
Bei tiefer Verschachtelung wird diese Kette schnell lang. findDeep sucht
deshalb im ganzen Dokument — egal, wie weit unten das Element steckt:
var doc = xml.parse(
"<antwort><daten><kunde><adresse><ort>Kiel</ort></adresse></kunde></daten></antwort>");
log.info(doc.findDeep("ort").text); // Kiel — ohne den ganzen Weg zu tippen
| sucht in | liefert | |
|---|---|---|
find |
nur direkte Kinder | ein Element oder null |
findAll |
nur direkte Kinder | eine Liste (evtl. leer) |
findDeep |
dem ganzen Teilbaum | ein Element oder null |
findAllDeep |
dem ganzen Teilbaum | eine Liste (evtl. leer) |
find kann null liefern
Fehlt das Element, bekommst du null — und null.text bricht das Skript
ab („Cannot read property 'text' of null"). Sichere es ab, wenn du dem
Fremdsystem nicht blind vertraust:
var doc = xml.parse("<kunde><name>Meyer</name></kunde>");
var ort = doc.findDeep("ort");
log.info(ort ? ort.text : "kein Ort angegeben");
Durch mehrere Einträge laufen¶
findAll liefert eine ganz normale Liste — mit length, forEach, filter
und map, genau wie im JSON-Kapitel beschrieben:
var doc = xml.parse(
"<bestellung>"
+ " <position><artikel>Stift</artikel><menge>3</menge><preis>1,20</preis></position>"
+ " <position><artikel>Block</artikel><menge>2</menge><preis>2,50</preis></position>"
+ "</bestellung>");
var posten = doc.findAll("position");
var summe = 0;
posten.forEach(function (p) {
summe += kc.num(p.find("menge").text) * kc.num(p.find("preis").text);
});
log.info(posten.length + " Positionen, Summe " + summe.toFixed(2));
Beachte kc.num(...): Aus XML kommt immer Text, nie eine Zahl — genau wie
aus einem Formularfeld.
Ein XML-Dokument in ein Formular übernehmen¶
Das ist der häufigste Fall im Alltag: Antwort lesen, Felder füllen, Zeilen in eine Detail-Tabelle schreiben.
var doc = xml.parse(
"<kunde nr='K-1001'><name>Meyer</name>"
+ "<position><artikel>Stift</artikel><menge>3</menge></position>"
+ "<position><artikel>Block</artikel><menge>2</menge></position></kunde>");
ctx.form.kundennr = doc.attr("nr");
ctx.form.name = doc.find("name").text;
// Detail-Tabelle: eine Liste von Objekten, ein Objekt je Zeile
ctx.form.positionen = doc.findAll("position").map(function (p) {
return {
artikel: p.find("artikel").text,
menge: kc.num(p.find("menge").text)
};
});
log.info(ctx.form.positionen.map(function (z) { return z.artikel; }).join(", "));
Werte ändern¶
Ein eingelesener Knoten lässt sich verändern und danach wieder ausgeben:
var doc = xml.parse("<position nr='1'><artikel>Stift</artikel><menge>3</menge></position>");
doc.find("menge").text = "9"; // Text ersetzen
doc.attr("nr", "2"); // Attribut setzen
doc.attr("veraltet", null); // Attribut entfernen (gab es hier gar nicht)
doc.addChild("bemerkung", "Eilt"); // Kind anhängen
log.info(xml.stringify(doc));
XML selbst bauen¶
xml.create(tag) legt ein leeres Dokument an, addChild hängt Elemente
daran. Jedes addChild liefert das neue Element zurück — so baust du dich
Ebene für Ebene nach unten:
var auftrag = xml.create("auftrag");
auftrag.attr("nr", "4711");
var kunde = auftrag.addChild("kunde");
kunde.addChild("name", "Meyer");
kunde.addChild("ort", "Kiel");
var pos = auftrag.addChild("position");
pos.addChild("artikel", "Stift");
pos.addChild("menge", "3");
log.info(xml.stringify(auftrag, true));
Das Ergebnis:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<auftrag nr="4711">
<kunde>
<name>Meyer</name>
<ort>Kiel</ort>
</kunde>
<position>
<artikel>Stift</artikel>
<menge>3</menge>
</position>
</auftrag>
Sonderzeichen erledigt KCForms
&, < und > werden beim Ausgeben automatisch maskiert, beim Einlesen
automatisch zurückverwandelt. Du musst also nichts von Hand ersetzen — und
solltest XML nie mit "<tag>" + wert + "</tag>" zusammenkleben: Ein
& im Wert macht das Dokument sonst ungültig.
Wohin mit dem fertigen XML? Die üblichen Wege:
| Ziel | So |
|---|---|
| Datei | fs.write("ausgang/auftrag.xml", xml.stringify(doc)) — fs |
| SOAP-Dienst | ws.soap.sendReceive(endpunkt, aktion, doc) — ws |
| HTTP-Schnittstelle | http.post(url, xml.stringify(doc), { contentType: "application/xml" }) — http |
| Mail-Anhang | mp.addBytes("auftrag.xml", …) — mail |
Namensräume und Präfixe¶
In SOAP-Antworten tragen Elemente oft einen Präfix: <soap:Body>,
<ns2:Material>. Dieser Präfix gehört in KCForms zum Namen — du suchst
also genau so, wie es im Dokument steht:
var antwort = xml.parse(
"<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'>"
+ "<soap:Body><ns2:MaterialResponse><ns2:nummer>4711</ns2:nummer>"
+ "</ns2:MaterialResponse></soap:Body></soap:Envelope>");
log.info(antwort.findDeep("ns2:nummer").text); // 4711
Praktischer Rat: Präfixe sind nicht garantiert stabil — manche Dienste
schicken heute ns2:, morgen ns5:. Wenn dich nur der Inhalt interessiert,
suche über den Teilbaum und vergleiche selbst:
var antwort = xml.parse(
"<soap:Envelope><soap:Body><ns2:MaterialResponse>"
+ "<ns2:nummer>4711</ns2:nummer></ns2:MaterialResponse></soap:Body></soap:Envelope>");
var treffer = antwort.findAllDeep("*").filter(function (n) {
return n.name.replace(/^.*:/, "") === "nummer"; // Präfix wegschneiden
});
log.info("gefunden: " + (treffer.length ? treffer[0].text : "–"));
Stolpersteine¶
| Stolperstein | So ist es richtig |
|---|---|
find("Ort") findet nichts |
Groß- und Kleinschreibung zählt: <ort> ≠ <Ort> |
<ns2:nummer> wird nicht gefunden |
Präfix gehört zum Namen — mitsuchen |
null.text bricht ab |
find kann null liefern; vorher prüfen |
doc.text ist leer |
text liefert nur den eigenen Text, nicht den der Kinder |
| Gerechnet wird falsch | XML liefert Text; mit kc.num(...) in Zahlen wandeln |
JSON.stringify(node.attrs) zeigt {} |
attrs ist eine Java-Map — attr("nr") benutzen oder mit for (var k in node.attrs) durchgehen (warum) |
Dokument mit <!DOCTYPE …> wird abgelehnt |
DOCTYPE und externe Entitäten sind aus Sicherheitsgründen abgeschaltet (Schutz vor XXE-Angriffen); solche Dateien vorher aufbereiten |
XML per + zusammengebaut |
xml.create(...) benutzen — sonst zerlegt ein & das Dokument |
Der Unterschied bei text in einem Beispiel:
var doc = xml.parse("<kunde><name>Meyer</name></kunde>");
log.info("eigener Text: [" + doc.text + "]"); // [] — kunde hat nur Kinder
log.info("Kind-Text: [" + doc.find("name").text + "]"); // [Meyer]
Von E4X kommend¶
Wer früher mit E4X gearbeitet hat (XML-Literale direkt im JavaScript,
x..artikel, x.@nr), findet das hier nicht mehr — und auch sonst nirgends:
E4X war nie Teil des JavaScript-Standards, Firefox hat es 2013 entfernt, und
die in KCForms verwendete GraalVM-Engine kennt es nicht. Ein XML-Literal
scheitert schon beim Einlesen des Skripts:
Die gute Nachricht: Für jedes E4X-Idiom gibt es eine direkte Entsprechung.
| E4X (früher) | KCForms heute |
|---|---|
new XML(text) |
xml.parse(text) |
var x = <auftrag/> |
xml.create("auftrag") |
x.position |
doc.find("position") bzw. doc.findAll("position") |
x..artikel |
doc.findDeep("artikel") bzw. doc.findAllDeep("artikel") |
x.@nr |
doc.attr("nr") |
x.@nr = "42" |
doc.attr("nr", "42") |
x.position.length() |
doc.findAll("position").length |
x.menge = "9" |
doc.find("menge").text = "9" |
x.appendChild(<artikel>Stift</artikel>) |
doc.addChild("artikel", "Stift") |
delete x.a |
doc.removeChild(kind) |
for each (var p in x.position) |
doc.findAll("position").forEach(function (p) { … }) |
x.toXMLString() |
xml.stringify(doc, true) |
x.parent() |
node.parent |
Dasselbe Beispiel in beiden Welten — erst E4X, dann KCForms:
// FRÜHER (E4X) — läuft heute nicht mehr
var doc = new XML(text);
log.info(doc.@nr);
for each (var p in doc.position) {
log.info(p.artikel + " x " + p.menge);
}
// HEUTE
var doc = xml.parse("<bestellung nr='42'>"
+ "<position><artikel>Stift</artikel><menge>3</menge></position>"
+ "<position><artikel>Block</artikel><menge>2</menge></position></bestellung>");
log.info(doc.attr("nr"));
doc.findAll("position").forEach(function (p) {
log.info(p.find("artikel").text + " x " + p.find("menge").text);
});
Drei Gewohnheiten musst du dabei ablegen:
- Kein
default xml namespace. Namensräume werden nicht aufgelöst; der Präfix gehört zum Namen (siehe oben). - Kein automatisches Umwandeln.
p.mengewar in E4X je nach Zusammenhang Text oder Zahl. Hier ist es immer Text —kc.num(...)macht eine Zahl daraus. - Kein DOCTYPE. Dokumente mit Dokumenttyp-Deklaration oder externen Entitäten weist der Parser ab; das ist der Schutz gegen XXE-Angriffe.
Spickzettel¶
| Ich will … | So geht's |
|---|---|
| Text zu XML | var doc = xml.parse(text); |
| neues Dokument | var doc = xml.create("wurzel"); |
| Element eine Ebene tiefer | doc.find("kunde") |
| Element irgendwo im Baum | doc.findDeep("ort") |
| alle Einträge | doc.findAll("position") |
| Textinhalt lesen | node.text |
| Textinhalt setzen | node.text = "neu" |
| Attribut lesen / setzen | node.attr("nr") / node.attr("nr", "42") |
| Kind anlegen | node.addChild("artikel", "Stift") |
| Kind entfernen | node.removeChild(kind) |
| eine Ebene höher | node.parent |
| alle Einträge durchgehen | doc.findAll("x").forEach(function (n) { … }) |
| Zahl aus XML | kc.num(node.text) |
| zurück zu Text | xml.stringify(doc) bzw. xml.stringify(doc, true) |
Weiter¶
- XML und Webservices in Beispielen — über 20 fertige Skripte zum Ausprobieren
ws— WebService-Client — SOAP und REST aufrufenhttp-Client —r.xml()wandelt eine Antwort direkt in einen Knoten- JSON und Objekte verstehen — das Gegenstück für JSON
- JavaScript-Grundlagen — Listen, Schleifen, Funktionen