Zum Inhalt

XML und Webservices

Viele Fremdsysteme tauschen Daten als XML aus, ältere als SOAP-Dienst, neuere als REST-Dienst mit JSON. In KCForms hilft dir dabei:

  • xml — XML lesen, durchsuchen und selbst bauen,
  • ws.rest — REST-Dienste aufrufen,
  • ws.soap — SOAP-Dienste aufrufen.

XML von Grund auf — Aufbau, Suchen, Ändern, Bauen, Namensräume und die Umsteigertabelle für E4X — erklärt das Kapitel XML verstehen und verarbeiten. Ausführliches zu den Diensten: ws — WebService-Client. Für einfache HTTP-Aufrufe (Download, Upload, Cookies) gibt es zusätzlich den http-Client.

Zum Üben: die eingebauten Echo-Dienste

Die Beispiele rufen zwei Dienste auf, die KCForms selbst mitbringt. Sie schicken einfach zurück, was sie bekommen:

  • http://localhost:8099/api/demo/ws-echo — REST, antwortet mit JSON
  • http://localhost:8099/api/demo/ws-soap — SOAP, spiegelt das gesendete Element

localhost ist ein internes Ziel. Damit die Aufrufe erlaubt sind, muss localhost in der Allowlist stehen (siehe Sicherheit) — auf Testsystemen üblich.

Einsteiger

1. XML-Text einlesen

xml.parse macht aus einem Text einen Knoten, in dem du dich bewegen kannst. name ist der Name des Elements.

var doc = xml.parse("<kunde><name>Meyer</name><ort>Hagen</ort></kunde>");
log.info("Wurzel: " + doc.name);

2. Den Text eines Elements lesen

find("ort") sucht ein direktes Kind-Element mit diesem Namen.

var doc = xml.parse("<kunde><name>Meyer</name><ort>Hagen</ort></kunde>");
log.info("Ort: " + doc.find("ort").text);

3. Ein Attribut lesen

Attribute sind die name="wert"-Angaben im Start-Tag.

var doc = xml.parse("<kunde nr='K-1001' aktiv='ja'><name>Meyer</name></kunde>");
log.info("Kundennummer: " + doc.attr("nr"));
log.info("Aktiv: " + doc.attr("aktiv"));

Gibt es das Attribut nicht, liefert attr den Wert null.

4. Alle gleichnamigen Elemente durchlaufen

var doc = xml.parse(
  "<bestellung>" +
  "  <artikel menge='10'>Stift</artikel>" +
  "  <artikel menge='2'>Block</artikel>" +
  "</bestellung>");

doc.findAll("artikel").forEach(function (a) {
  log.info(a.text + ": " + a.attr("menge"));
});

5. Tief verschachtelte Elemente finden

find schaut nur eine Ebene tief. findDeep durchsucht alles darunter.

var doc = xml.parse(
  "<kunde><adresse><strasse>Hauptstr. 1</strasse><plz>58095</plz></adresse></kunde>");
log.info("PLZ: " + doc.findDeep("plz").text);

6. Selbst XML bauen

xml.create legt ein Element an, addChild hängt ein Kind-Element an.

var kunde = xml.create("kunde");
kunde.addChild("name", "Meyer");
kunde.addChild("ort", "Hagen");
log.info(xml.stringify(kunde));

7. Attribute setzen und schön eingerückt ausgeben

xml.stringify(knoten, true) rückt das XML ein — gut lesbar für Menschen.

var kunde = xml.create("kunde");
kunde.attr("nr", "K-1001");
kunde.addChild("name", "Meyer");
log.info(xml.stringify(kunde, true));

8. Sonderzeichen erledigen sich von selbst

Zeichen wie & und < haben in XML eine besondere Bedeutung. Mit xml.create werden sie automatisch richtig umgewandelt. Baust du XML dagegen als Text zusammen, musst du das selbst tun.

var firma = xml.create("firma", "Müller & Söhne <GmbH>");
log.info(xml.stringify(firma));

// Beim Zurücklesen kommt der Originaltext wieder heraus:
log.info(xml.parse(xml.stringify(firma)).text);

Fortgeschritten

9. Formularwerte als XML

var antrag = xml.create("antrag");
antrag.attr("datum", new Date().format("yyyy-MM-dd"));
antrag.addChild("vorname", ctx.form.vorname);
antrag.addChild("name", ctx.form.name);
antrag.addChild("email", ctx.form.email);
antrag.addChild("betrag", String(ctx.form.betrag));
log.info(xml.stringify(antrag, true));

10. Eine Tabelle als XML-Liste

Jede Zeile einer Detail-Tabelle wird ein eigenes Element.

var bestellung = xml.create("bestellung");
var liste = bestellung.addChild("positionen");

var posten = ctx.form.positionen || [];
// Indizierte Schleife: das Attribut nr braucht den Index, und eine Liste AUS
// DEM FORMULAR gibt ihn per forEach nicht her.
for (var i = 0; i < posten.length; i++) {
  var pos = liste.addChild("position");
  pos.attr("nr", String(i + 1));
  pos.addChild("artikel", posten[i].artikel);
  pos.addChild("menge", String(posten[i].menge));
}

log.info(liste.children.length + " Positionen");
log.info(xml.stringify(bestellung, true));

11. XML in Formularfelder übernehmen

Ein Fremdsystem liefert Kundendaten als XML — du verteilst sie auf die Felder.

var doc = xml.parse(
  "<kunde nr='K-1001'><vorname>Anna</vorname><name>Meyer</name>" +
  "<kontakt><mail>anna@beispiel.example</mail></kontakt><ort>Hagen</ort></kunde>");

ctx.form.kundennr = doc.attr("nr");
ctx.form.vorname  = doc.find("vorname").text;
ctx.form.name     = doc.find("name").text;
ctx.form.email    = doc.findDeep("mail").text;
ctx.form.land     = "DE";
log.info(ctx.form.vorname + " " + ctx.form.name + ", " + doc.find("ort").text);

12. Fehlende Elemente sicher behandeln

Gibt es ein Element nicht, liefert find den Wert null. Ohne Prüfung bricht das Skript mit „Cannot read property 'text' of null" ab.

var doc = xml.parse("<kunde><name>Meyer</name></kunde>");

function text(knoten, tag, ersatz) {
  var k = knoten.find(tag);
  return k ? k.text : ersatz;
}

log.info("Name: " + text(doc, "name", "?"));
log.info("Telefon: " + text(doc, "telefon", "(keine Angabe)"));

13. Elemente ändern und entfernen

text kannst du auch setzen. removeChild entfernt ein Kind-Element.

var doc = xml.parse("<artikel><name>Stift</name><preis>9.90</preis><intern>A-7</intern></artikel>");

doc.find("preis").text = "12.50";          // Preis ändern
doc.removeChild(doc.find("intern"));       // interne Angabe entfernen
doc.attr("geaendert", "ja");

log.info(xml.stringify(doc));

14. Kaputtes XML abfangen

Ist der Text kein gültiges XML, wirft xml.parse einen Fehler.

try {
  xml.parse("<kunde><name>Meyer</kunde>");
} catch (e) {
  log.info("Kein gültiges XML: " + e.message);
}

15. XML-Datei speichern und wieder einlesen

var kunde = xml.create("kunde");
kunde.addChild("name", "Meyer");
fs["/beispiele/xml/kunde.xml"].writeText(xml.stringify(kunde, true));

var wieder = xml.parse(fs["/beispiele/xml/kunde.xml"].readText());
log.info("Gelesen: " + wieder.find("name").text);

fs["/beispiele/xml"].remove(true);

16. Einen REST-Dienst abfragen

ws.rest.get liefert eine Antwort mit status, ok und text. json() macht aus dem Text ein Objekt.

var r = ws.rest.get("http://localhost:8099/api/demo/ws-echo");
if (r.ok) {
  log.info("Status " + r.status + ", Dienst: " + r.json().service);
}

17. Daten an einen REST-Dienst schicken

Ein JavaScript-Objekt geht automatisch als JSON hinaus.

var r = ws.rest.post("http://localhost:8099/api/demo/ws-echo", {
  kunde:  ctx.form.name,
  betrag: ctx.form.betrag
});
var echo = r.json();
log.info("angekommen: " + echo.body.kunde + " / " + echo.body.betrag);

18. HTTP-Fehler erkennen

Antwortet der Dienst mit einem Fehlercode (z. B. 404 „nicht gefunden"), gibt es keine Ausnahme — du prüfst r.ok.

var r = ws.rest.get("http://localhost:8099/api/demo/http/status/404");
if (!r.ok) {
  log.info("Fehler " + r.status + " – bitte später erneut versuchen");
}

Profi

19. Suchparameter, eigene Header und Token

Mit ws.rest.request gibst du alles in einem Objekt an. Umlaute und Leerzeichen in query werden automatisch richtig kodiert.

var r = ws.rest.request({
  method:  "GET",
  url:     "http://localhost:8099/api/demo/ws-echo",
  query:   { suche: "Müller & Co", seite: 1 },
  headers: { "X-Mandant": "4711" },
  auth:    "bearer",
  token:   "mein-zugangs-token",
  timeout: 5000
});
var echo = r.json();
log.info("Suche nach: " + echo.query.suche);
log.info("Anmeldung: " + echo.headers.authorization);   // vom Echo maskiert

20. Einen SOAP-Dienst aufrufen

Du lieferst nur den Inhalt, den SOAP-Umschlag („Envelope") baut KCForms selbst. Die Antwort ist wieder ein XML-Knoten.

var antwort = ws.soap.sendReceive(
  "http://localhost:8099/api/demo/ws-soap",
  "urn:Echo",                                        // die SOAPAction
  "<Ping xmlns='urn:demo'><wert>test</wert></Ping>"
);
log.info("Antwort-Element: " + antwort.name);
log.info("Wert zurück: " + antwort.findDeep("wert").text);

21. Die SOAP-Anfrage mit xml.create bauen

Sicherer als Text-Stückelei: Sonderzeichen in Eingaben können das XML nicht kaputtmachen.

var anfrage = xml.create("KundeSuchen");
anfrage.attr("xmlns", "urn:demo");
anfrage.addChild("name", ctx.form.name);

var antwort = ws.soap.sendReceive("http://localhost:8099/api/demo/ws-soap", "urn:KundeSuchen", anfrage);
log.info("zurück: " + antwort.findDeep("name").text);

22. Elemente mit Präfix

In SOAP-Antworten tragen Elemente oft ein Präfix wie tns:. Beim Suchen schreibst du es genauso mit.

var antwort = ws.soap.sendReceive("http://localhost:8099/api/demo/ws-soap", "urn:Echo", "<Hallo/>");
log.info("Antwort heißt: " + antwort.name);                   // tns:echoResponse
log.info("Dienst: " + antwort.find("tns:service").text);

23. SOAP 1.2 und fehlgeschlagene Aufrufe

SOAP gibt es in zwei Versionen; die Voreinstellung ist 1.1. Anders als bei REST wirft ein SOAP-Fehler (etwa ein Fault oder eine Antwort, die gar kein SOAP ist) eine Ausnahme — deshalb gehört try … catch dazu.

var v12 = ws.soap.sendReceive("http://localhost:8099/api/demo/ws-soap", "urn:Echo",
                              "<Ping><wert>1.2</wert></Ping>", { soapVersion: "1.2" });
log.info("SOAP 1.2: " + v12.findDeep("wert").text);

try {
  // Der REST-Echo-Dienst versteht kein SOAP → Fehler
  ws.soap.sendReceive("http://localhost:8099/api/demo/ws-echo", "urn:Echo", "<Ping/>");
} catch (e) {
  log.info("Aufruf fehlgeschlagen: " + e.message);
}

24. Über einen benannten Konnektor

Adresse und Zugangsdaten pflegt der Admin zentral als Konnektor. Im Skript steht nur noch der Name — ändert sich die Adresse, bleibt das Skript gleich.

var dienst = ws.connect("demo-echo");            // Basis-URL: …/api/demo
var r = dienst.get("/ws-echo?quelle=konnektor");
log.info("über Konnektor: " + r.json().method + " " + r.json().query.quelle);

25. Abschicken, ohne auf die Antwort zu warten

Für Benachrichtigungen, deren Antwort dich nicht interessiert. Das Formular muss so nicht auf den Dienst warten.

ws.rest.fireAndForget({
  method: "POST",
  url:    "http://localhost:8099/api/demo/ws-echo",
  body:   { ereignis: "antrag-eingegangen", zeit: new Date().toISOString() }
});
log.info("Benachrichtigung abgeschickt");

26. Alles zusammen: Kunde prüfen und Ergebnis anzeigen

Ein vollständiger Ablauf, wie er in einem Button-Skript stehen könnte: Eingabe prüfen, Dienst fragen, Antwort auswerten, Fehler freundlich melden.

var nr = String(ctx.form.kundennr || "").trim();

if (!/^K-\d{4}$/.test(nr)) {
  ctx.addError("kundennr", "Bitte eine Kundennummer wie K-1234 eingeben.");
} else {
  try {
    var anfrage = xml.create("KundePruefen");
    anfrage.addChild("nummer", nr);
    var antwort = ws.soap.sendReceive("http://localhost:8099/api/demo/ws-soap", "urn:KundePruefen", anfrage);

    var zurueck = antwort.findDeep("nummer");
    ctx.form.ergebnis = zurueck
      ? "Kunde " + zurueck.text + " geprüft"
      : "Keine Antwort vom Dienst";
  } catch (e) {
    ctx.form.ergebnis = "Dienst nicht erreichbar";
    log.warn("Kundenprüfung fehlgeschlagen: " + e.message);
  }
}
log.info("Ergebnis: " + ctx.form.ergebnis);