inhoudstafel en auteursrecht
* STER *


10. Extensible Markup Language (XML)

10.1 Wat is XML, en wat heeft het met Java te maken ?

XML is een taal voor gegevensuitwisseling. Het is dus geen programmeertaal zoals Java. Je kan de adressen van je klanten in een XML-bestand opslaan, of ook de tekst van een boek.

XML is niet de enige manier om gegevens op te slaan. Systemen voor het relationeel beheer van gegevens (relational database management systems, kortweg relationele databases of RDBMS) hebben hun eigen manier om grote verzamelingen gegevens te coderen. Denk bijvoorbeeld aan de .mdb-bestanden waarvan Microsoft Access gebruik maakt.

De grote troef van XML ten opzichte van andere standaarden is zijn platform-onafhankelijkheid. XML is een gewoon tekstformaat dat geen gebruik maakt van binaire codes, en dat onafhankelijk is van de gebruikte machine (zoals Java). XML is dus een heel geschikt formaat om een gegevensbestand over te brengen van de ene machine naar de andere, of van het ene computerprogramma naar het andere.

A priori is XML onafhankelijk van Java. Java kan uitstekend communiceren met relationele databases, zoals we zagen in hoofdstuk 6. En XML is onafhankelijk van de gebruikte programmeertaal, kan dus net zo goed gebruikt worden om een Visual Basic-programma te laten praten met een C++ programma op een Unix-systeem.

Toch is er een sterke culturele verwevenheid tussen Java en XML. Dat heeft te maken met: openheid, de eerder genoemde platform-onafhankelijkheid, en een nadrukkelijke aanwezigheid op (en verbondenheid met) het World Wide Web. Praktisch heeft dit twee gevolgen:

Het tweede punt is de hoofdreden waarom we in deze tekst een hoofdstuk aan XML wijden. De volgende drie paragrafen vormen een korte inleiding op XML. Ze bevatten net genoeg informatie om mee te kunnen bij de verschillende raakpunten tussen Java en XML die we in dit boek zullen ontmoeten. Ze mogen niet worden beschouwd als een volwaardige cursus XML!

In paragrafen 10.5 - 10.7 schetsen we de belangrijkste interfaces waarmee een Java-programma XML-gegevens kan lezen en behandelen.

10.2 De vorm van een XML-bestand

Naar de vorm lijkt XML sterk op HTML, de taal van webpagina's. Dat is geen toeval, omdat ze allebei geëvolueerd zijn uit een gemeenschappelijke voorouder met de naam SGML. Sinds het jaar 2000 is het zelfs mogelijk webpagina's in een versie van XML te coderen; die moderne vorm van HTML heet XHTML.

Hier is een eerste eenvoudig voorbeeld van een XML-bestand.

<?xml version="1.1"?>
<!-- voorbeeld.xml
 Voorbeeld van een welgevormd xml-document
 Auteur: Lieven Smits
 Datum: 6 sep 2004
 Versie: 1.0
-->
<klant>
<naam>Jansen</naam>
<adres type="straatnr">Veldweg 6</adres>
<adres type="postnr">1791</adres>
<adres type="gemeente">Ons Dorp</adres>
<goedeklant/>
</klant>

De eerste regel bepaalt welke versie van de XML-standaard we hanteren, in dit geval 1.1 (indien zulke regel ontbreekt, hanteren we automatisch de versie 1.0). Daarna volgen enkele regels commentaar. De rest van de XML-tekst bestaat uit twee soorten onderdelen: enerzijds gewone tekst ("1790", "Jansen"), anderzijds elementen die tussen schuine haken (< >) staan. Die laatsten noemen we met een Engels woord tags. Naargelang van het optreden van de voorwaartse schuine streep (/) onderscheiden we drie soorten tags:

De opening tag <adres> heeft een attribuut met de naam type. Met het gelijkteken wordt aan dit attribuut driemaal een verschillende waarde toegekend.

Iedere opening tag moet vroeg of laat worden gevolgd door een closing tag met dezelfde naam. Wanneer een opening tag wordt gevolgd door een tweede opening tag voorafgaand aan zijn closing tag, dan moet de closing tag van de tweede tag voorafgaan aan de closing tag van de eerste tag. Uiteraard moeten closing tags altijd overeenkomen met een eerdere opening tag. Een element is ofwel een empty tag, ofwel een stuk tekst dat begint met een opening tag en eindigt met de overeenkomstige closing tag. Elementen van de tweede soort kunnen tussen hun opening en hun closing tag nog andere elementen bevatten. Drie voorbeelden van elementen in bovenstaand XML-document zijn:

(1)
<klant>
<naam>Jansen</naam>
<adres type="straatnr">Veldweg 6</adres>
<adres type="postnr">1791</adres>
<adres type="gemeente">Ons Dorp</adres>
<goedeklant/>
</klant>

(2)
<naam>Jansen</naam>

(3)
<goedeklant/>

De vijf elementen naam, adres (drie keer) en goedeklant zijn kinderen van het element klant. Het element klant is de ouder van de kinderen. In ingewik­kelde XML-documenten kan een kind op zijn beurt ouder zijn van andere elementen, enzovoort tot willekeurige diepte. In het algemeen spre­ken we van de voorouders respec­tievelijk de afstammelingen van een element. Zo worden de gegevens van het document op natuurlijke wijze gerang­schikt in een boomstructuur. Informatici tekenen bomen gewoonlijk op hun kop, met de wortels bovenaan en de takken onderaan - genealogen doen dat trouwens ook soms met stambomen.

klant/naam - adres - adres - adres - goedeklant

Een welgevormd XML-document bevat minstens één tag. Als het meer dan één tag bevat, dan moet de eerste een opening tag zijn die wordt afgesloten door de laatste tag. Met andere woorden: op het hoogste niveau bevat het document precies één element. Dit hoogste element noemen we het wortelelement, in overeenstemming met de metafoor van de omgekeerde boom.

Als een XML-document aan enkele elementaire vormvereisten zoals hierboven voldoet, heet het welgevormd. De precieze specificatie van een welgevormd document is nogal technisch. De standaard wordt onderhouden en gepubliceerd door een werkgroep van vrijwilligers in het kader van het World Wide Web Consortium (W3C).

De namen van de tags zijn, binnen enkele heel algemene vormvereisten, vrij. Dat hebben we onderstreept door in ons voorbeeld Nederlandse namen te kiezen. Ook de volgorde waarin opening en empty tags voorkomen is vrij (op voorwaarde dat de closing tags de hiërarchie van opening tags respecteren). In praktische toepassingen willen we meestal beperkingen opleggen aan de collectie beschikbare tags en aan hun onderlinge relatie. De formele vastlegging van die beperkingen noemen we de metadata.

Commentaar bevindt zich tussen de tekens <!-- en -->. De inhoud van de commentaar mag geen twee opeenvolgende streepjes (--) bevatten. Commentaar mag tussen de opening en closing tag van een element staan, maar niet binnen de haken van één afzonderlijke tag. Een programma dat XML-gegevens verwerkt, moet commentaar negeren. Twee XML-documenten die alleen in de commentaren verschillen, worden geacht dezelfde informatie over te dragen.

Opmerking

Namen van XML-tags zijn hoofdlettergevoelig, in tegenstelling tot klassieke HTML-tags. De volgende code kan dus geen deel uitmaken van een welgevormd XML-document:

<!-- FOUT: opening tag heeft niet identiek dezelfde naam als closing tag -->
<Javabean>MyBean.class</JavaBean>

10.3 Metadata

De XML-wereld verleent een bijzondere betekenis aan het woord geldig. Een XML-document is geldig als en slechts als het gelijktijdig aan de volgende voorwaarden voldoet:

Er zijn technisch twee manieren om de metadata van een document vast te leggen: document type definition (DTD) en schema. DTD-bestanden hebben een eigen formaat, verschillend van XML. Schema's zijn op hun beurt correcte XML-bestanden die aan welbepaalde metadata voldoen. In deze paragraaf gaan we in op het formaat en het gebruik van DTD. Paragraaf 10.4 hieronder beschrijft schema's.

Hier is een voorbeeld van een (eenvoudige) DTD.

<!-- klant: Document Type Definition
 Auteur: Lieven Smits
 Datum: 6 sep 2004
 Versie: 1.0
Een XML-file die aan deze definitie voldoet, stelt precies één klant voor.
 -->
<!ELEMENT klant (naam|adres|goedeklant)+>
<!ELEMENT naam (#PCDATA)>
<!ELEMENT adres (#PCDATA)>
<!ATTLIST adres
 type (straatnr|postnr|gemeente|land|telefoon) #REQUIRED
>
<!ELEMENT goedeklant EMPTY>

De regels die het woord ELEMENT bevatten, bepalen elk een soort tag die in het XML-document mag optreden. In ons geval zijn dat: klant, naam, adres en goedeklant. A priori mogen deze tags in de drie verschillende gedaanten optreden: opening, closing en empty.

De code #PCDATA betekent dat tussen de opening en closing tag gewone tekst mag optreden. Dat is het geval bij de tags naam en adres, maar niet bij klant of goedeklant. Ons XML-document mag dus niet het volgende bevatten:

<klant>
Supermarkt Alwa
</klant>

Een welgevormd document dat een DTD heeft en eraan voldoet, is geldig. Bovenstaand tegenvoorbeeld is welgevormd maar ongeldig.

In de definitie van het element klant komt de volgende uitdrukking voor:

  (naam|adres|goedeklant)+

Dit geeft aan dat het element klant moet bestaan uit een opening tag <klant>, gevolgd door één of meer elementen van de types naam, adres of goedeklant (in willekeurige volgorde), gevolgd door een closing tag </klant>. Bovenstaande uitdrukking is een voorbeeld van een reguliere expressie.

Opmerking

Krachtens de reguliere expressie die de inhoud van het element klant definieert, is er geen beperking op de volgorde of het maximum aantal optredens van de deelelementen. Dus ook het volgende, ietwat absurde document is geldig:

<?xml version="1.1"?>
<!DOCTYPE klant SYSTEM "klant.dtd">
<!-- voorbeeld.xml
 Voorbeeld van een geldig xml-document
 Auteur: Lieven Smits
 Datum: 17 sep 2004
 Versie: 1.0
-->
<klant>
<goedeklant/>
<adres type="straatnr">Veldweg 6</adres>
<adres type="straatnr">Veldweg 7</adres>
<goedeklant/>
<goedeklant/>
</klant>

De volgende onderdelen kunnen optreden in reguliere expressies die de inhoud van XML-elementen vastleggen:

Expressie-onderdeel Betekenis
naam van een element Dit element treedt op als "tak" in de boomstructuur
#PCDATA Willekeurige tekst tussen de opening tag en de closing tag
| Ofwel hetgeen links van de verticale streep staat, ofwel hetgeen rechts ervan staat (keuze uit alternatieven)
+ Wat voor het plusteken staat, komt één of meer keren voor
? Wat voor het vraagteken staat, komt al dan niet voor (optioneel)
* Wat voor de asterisk staat, komt nul of meer keren voor (optioneel/meervoudig)
( ) Haakjes bepalen de volgorde der bewerkingen

Voorbeelden

  1. De volgende reguliere expressie codeert: een voorkaft, gevolgd door een willekeurig aantal bladzijden (mag 0 zijn), gevolgd door een achterkaft.
      voorkaft (bladzijde)* achterkaft
    
  2. De volgende regel, in een DTD, geeft aan dat het element voertuig geen tekst mag bevatten. Een voertuig moet minstens één wiel hebben. Er mag ten hoogste één stuur voorkomen, en dat moet dan aan de wielen voorafgaan.
    <!ELEMENT voertuig (stuur)?(wiel)+>
    
    Dat wil zeggen dat een geldig XML-document de volgende code mag bevatten...
    <voertuig><wiel/></voertuig>
    <voertuig><stuur></stuur><wiel></wiel><wiel></wiel></voertuig>
    
    ...maar niet de volgende:
    <voertuig/>
    <!-- bevat geen wiel -->
    
    <voertuig><wiel/><wiel/><stuur/><wiel/><wiel/></voertuig>
    <!-- wiel gaat vooraf aan stuur -->
    

De definitie van het element goedeklant specificeert dat dit element uitsluitend de vorm van een empty tag kan aannemen:

<!ELEMENT goedeklant EMPTY>

Het element goedeklant mag geen inhoud hebben. Dit wil zeggen dat <goedeklant/> is toegelaten, maar <goedeklant>Voorbeeldtekst</goedeklant> niet.

Sommige elementen hebben attributen. Dat is extra informatie die bij de opening tag (of de empty tag) wordt vermeld. De DTD somt de mogelijke attributen van een element op in een attribute list. Voor ieder attribuut wordt aangegeven:

In ons oorspronkelijke voorbeeld kan alleen het element adres een attribuut hebben. Dat attribuut heet type, het is verplicht, en het moet één van de volgende vijf waarden aannemen: straatnr, postnr, gemeente, land of telefoon.

Sommige XML-documenten gebruiken elementen en attributen uit verschillende DTDs tegelijkertijd. Dan moet er een mechanisme bestaan om gelijknamige elementen (of attributen) in verschillende DTDs van elkaar te onderscheiden. Dat gebeurt door een namespace prefix. De naam van een element of attribuut kan worden voorafgegaan door een afkorting die verwijst naar de desbetreffende DTD, en een dubbelepunt.

Het verband tussen een afkorting en de DTD zelf moet worden gelegd in een speciaal attribuut, xmlns (ns staat voor namespace). In het volgende voorbeeld maken we gebruik van twee verschillende definities van het element regel. In de opening tag van het element factuur verwijzen we naar twee verschillende DTDs.

<factuur
 xmlns:mijndtd="http://www.ster.be/facturen.dtd"
 xmlns:jouwdtd="http://www.javabron.com/berekening.dtd"
>
  <mijndtd:nummer>12345</mijndtd:nummer>
  <mijndtd:datum>16/9/2005</mijndtd:datum>
  <mijndtd:regel omschrijving="schrijfblok"
    eenheidsprijs="2.35" aantal="3" btw="21"/>
  <mijndtd:regel omschrijving="pen" eenheidsprijs="3.50" aantal="1" btw="21"/>
  <jouwdtd:regel type="exclusief" bedrag="10.55"/>
  <jouwdtd:regel type="inclusief" bedrag="12.77/>
</factuur>

10.4 Schema

De vorm van DTDs is een erfenis uit het tijdperk van SGML. Er is kritiek geuit op het feit dat de metadata van een XML-document niet in XML kunnen worden uitgedrukt. XML Schema is een taal voor metadata die aan dat euvel verhelpt. XML Schema is rijker dan DTD, maar we geven hier alleen kort de onderdelen aan die nodig zijn om een DTD in XML te "vertalen". Laten we beginnen met de Schema-versie van het documenttype "klant".

<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="klant" type="klantType"/>
<xsd:complexType name="klantType">
 <xsd:choice minOccurs="1" maxOccurs="unbounded">
 <xsd:element name="naam" type="xsd:string"/>
 <xsd:element name="adres">
  <xsd:attribute name="type" type="xsd:string" use="required"/>
 </xsd:element>
 <xsd:element name="goedeklant"/>
 </xsd:choice>
</xsd:complexType>
</xsd:schema>

De elementen van een schema horen bij een specifieke namespace, waarvan de definitie zich op de publieke webserver van het World Wide Web Consortium (www.w3.org) bevindt. Het gebruik van het prefix xsd is conventie.

Het wortelelement van een schema is altijd schema (met bijhorend namespace-voorvoegsel, in ons geval xsd). De inhoud van dit element is een definitie van het wortelelement van het XML-document.

De definitie van een nieuw elementtype in XML Schema gebeurt met het element element. De volgende regel definieert een nieuw element met de naam klant:

<xsd:element name="klant" type="klantType"/>

Met het attribuut type geven we de toelaatbare inhoud van het element klant aan. Het type van een element kan eenvoudig of complex zijn. De inhoud van het type klant is complex. De precieze definitie van de inhoud wordt uitgesteld met de verwijzing naar een type-naam, klantType.

Op zijn beurt wordt het type klantType gedefinieerd door het schema-element complexType. In ons voorbeeld bestaat een klant uit elementen van de types naam, adres en nieuweklant, in willekeurige volgorde en aantal. De keuze tussen de drie elementtypes wordt weergegeven door het element choice. Met de attribuutwaarde minOccurs="1" geven we aan dat klant minstens één kind moet hebben. Het getal 1 is de standaardwaarde voor het attribuut minOccurs, we hadden dit dus kunnen weglaten. Met de waarde 0 hadden we kunnen aangeven dat ook een klant-element zonder kinderen geldig kan zijn. Met de attribuutwaarde maxOccurs="unbounded" laten we toe dat een klant meer dan één deel-element kan bevatten. In plaats van unbounded hadden we hier een natuurlijk getal kunnen zetten, bijvoorbeeld 5. De standaardwaarde is 1.

Naast choice kunnen we in een typedeclaratie ook sequence gebruiken, om een verplichte volgorde op te leggen aan de kinderen van een element. Dat is toevallig niet zo bij onze definitie van klant, maar we hadden als volgt kunnen eisen dat in geldige documenten de volgorde naam-adresgegevens-goedeklant gerespecteerd zou worden, met de naam in elk geval verplicht (minOccurs niet vermeld en dus per standaard 1):

<xsd:complexType name="klantType">
 <xsd:sequence>
 <xsd:element name="naam" type="xsd:string"/>
 <xsd:element name="adres" minOccurs="0" maxOccurs="unbounded">
  <xsd:attribute name="type" type="xsd:string" use="required"/>
 </xsd:element>
 <xsd:element name="goedeklant" minOccurs="0"/>
 </xsd:sequence>
</xsd:complexType>

Het element naam heeft een eenvoudig type, namelijk string (karakterstreng).

Het type van een element kan ook impliciet worden vastgelegd, zonder uitdrukkelijke verwijzing naar een typenaam. Zo wordt het type van het element adres vastgelegd door de inhoud van de element-definitie:

 <xsd:element name="adres">
  <xsd:attribute name="type" type="xsd:string" use="required"/>
 </xsd:element>

Het element adres heeft impliciet een complex type, zonder inhoud (empty) maar met één verplicht attribuut.

Ook van het element goedeklant wordt het type impliciet gedeclareerd. Het betreft een "complex" type (nou ja) zonder inhoud of attributen.

 <xsd:element name="goedeklant"/>

De uitvinder van een schema heeft vaak de keuze tussen impliciete en expliciete type-declaraties. Daarbij kunnen de volgende overwegingen een rol spelen.

  impliciet expliciet
wat is het ? typedefinitie als onderdeel van de element-declaratie afzonderlijk benoemd type waarnaar element-declaraties kunnen verwijzen
hergebruik als een tweede element hetzelfde type heeft, moet de impliciete definitie herhaald worden als een tweede element hetzelfde type heeft, volstaat het een tweede keer naar de typenaam te verwijzen
leesbaarheid elementtype en element dicht bij elkaar het wordt mogelijk, types op een afzonderlijke plaats te groeperen
lengte korter langer

We hebben ons in deze paragraaf beperkt tot een passieve analyse van slechts enkele aspecten van XML Schema, nauwelijks genoeg om een schema-equivalent te kunnen lezen voor de DTD van paragraaf 10.3. In het algemeen kunnen we stellen dat XML Schema een krachtiger metataal is dan Document Type Definition, dat wil zeggen dat we fijnere (strengere) normen kunnen opleggen aan welke XML-documenten we precies als geldig willen beschouwen. We bevelen een grondige studie van XML Schema van harte aan, bijvoorbeeld via de officieuze inleiding bij de officiële W3C-specificatie.

10.5 SAX: XML lezen en produceren in Java

Een computerprogramma dat in staat is een computertaal te herkennen (al dan niet met de bedoeling instructies uit te voeren of te vertalen), heet een parser. Diverse soorten parsers zijn in staat na te gaan of een XML-bestand welgevormd is, of het geldig is ten opzichte van een DTD, enzovoort. Er zijn verschillende soorten XML-parsers verkrijgbaar onder allerlei licenties, waaronder ook enkele goede open source-producten. In deze tekst beperken we ons tot hulpmiddelen die deel uitmaken van de standaardbibliotheken van Sun.

JAXP staat voor Java API for XML Processing. JAXP biedt een interface waarmee parsers kunnen worden geselecteerd. Hij bevat ook defaultimplementaties van sommige parsertypes. JAXP is een onderdeel van de Java Development Kit sinds versie 1.4 (pakketten java.xml.parsers, java.xml.transform en aanverwanten). Parsers heb je momenteel in twee smaken: SAX en DOM.

SAX staat voor Simple API for XML. Het is een standaard waarmee Java-programma's XML-parsers kunnen aanspreken. SAX bevat zelf geen parser-implementatie. SAX is een onderdeel van de Java Development Kit sinds versie 1.4 (pakketten org.xml.sax en aanverwanten). We beperken ons tot de bespreking van SAX2, een licht geëvolueerde vorm van de oorspronkelijke SAX die beter omgaat met namespaces.

SAX is vooral geschikt om relatief eenvoudige XML-documenten serieel te doorlopen, en bepaalde handelingen te stellen (methoden aan te roepen) overeenkomstig de tags. De lengte van het document speelt daarbij a priori niet zo'n rol. Voor erg complexe documenten, of voor niet-seriële behandeling (bv. het opbouwen van de overeenkomstige boom) bevelen we het alternatief DOM aan: zie paragraaf 10.6. DOM is minder geschikt voor erg lange documenten, omdat de volledige inhoud van het document zich gelijktijdig in het computergeheugen bevindt.

De standaardmanier om SAX te laten opereren op een eenvoudig XML-bestand vereist het schrijven van de volgende stukken code:

  1. een klasse die de interface org.xml.sax.DocumentHandler implementeert; dit kan een eenvoudige uitbreiding zijn van de default-implementatie org.xml.sax.helpers.DefaultHandler;
  2. creatie van de parser (rechtstreeks de constructor aanroepen, of onrechtstreeks via een zogenaamd factory-object;
  3. creatie van een gegevens-invoerstroom waar de parser de XML kan lezen
  4. activering van de parser

De interface ContentHandler bevat een aantal methoden die overeenkomen met gebeurtenissen tijdens het serieel doorlopen van de XML-gegevensstroom. De belangrijkste zijn:

/** Begin van het parsen. */
void startDocument()

/** De parser ontmoet een opening tag of een empty tag. */
void startElement(String namespaceURI, String localName, String qName, Attributes atts)

/** De parser ontmoet een closing tag of een empty tag. Bij een empty tag worden
 *  de methoden startElement en endElement achter elkaar aangeroepen.
 */
void endElement(String namespaceURI, String localName, String qName)

/** De parser heeft (een deel van) de tekst gelezen tussen de opening en closing tag
 *  van een element. Kan verscheidene malen worden opgeroepen voor hetzelfde element,
 *  met opeenvolgende stukken tekst.
 */
public void characters(char[] ch, int start, int length)

/** Einde van het parsen. */
void endDocument()

De programmeur kan in haar implementatie van deze methoden aangeven welke handelingen het programma moet verrichten overeenkomstig de verschillende tags die de parser ontmoet. De klasse DefaultHandler geeft standaardimplementaties voor alle methoden van ContentHandler, meestal door niets te doen. Je kan overbodige code vermijden door je klasse af te leiden van DefaultHandler, en slechts twee of drie methoden te herdefiniëren.

Voorbeeld

We schrijven een programma dat de gegevens van een klant leest, en een adresetiket afdrukt. De klantgegevens bevinden zich in een XML-bestand waarvan de naam als parameter op de opdrachtregel wordt meegegeven. Ons programma maakt geen gebruik van de DTD, en is dus in principe toepasbaar op elk welgevormd XML-bestand. Ons voorbeeldprogramma bestaat slechts uit de handler-klasse; de parser creëren en activeren doen we in de main-methode.

import java.io.FileReader;
import java.io.IOException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/** Lees een klantadres uit een XML-bestand en druk een adresetiket af. */
public class DrukEtiket extends DefaultHandler {
  /* Symbolische constanten voor de opvulling van huidigElement */
  private final static int ELEMENT_NAAM = 101;
  private final static int ELEMENT_STRAATNR = 102;
  private final static int ELEMENT_POSTNR = 103;
  private final static int ELEMENT_GEMEENTE = 104;
  private final static int ELEMENT_TELEFOON = 105;
  private final static int ELEMENT_LAND = 106;
  private final static int ELEMENT_ANDER = 200;

  /** Soort element dat overeenkomt met de laatst gelezen opening of empty tag. */
  private int huidigElement = ELEMENT_ANDER;

  /** Tekstinhoud van het laatst gelezen naam-element. */
  private String naam ="";

  /** Tekstinhoud van het laatst gelezen adres-element waarvan het type "straatnr" is. */
  private String straatnr = "";

  /** Tekstinhoud van het laatst gelezen adres-element waarvan het type "postnr" is. */
  private String postnr = "";

  /** Tekstinhoud van het laatst gelezen adres-element waarvan het type "gemeente" is. */
  private String gemeente = "";

  /** Tekstinhoud van het laatst gelezen adres-element waarvan het type "land" is. */
  private String land = "";

  /** Deze methode wordt aangeroepen bij de start van een nieuw element
   *  (opening tag of empty tag). Onthou het soort element en, indien
   *  het een adreselement is, het "type"-attribuut.
   */
  public void startElement(
    String uri,
    String localName,
    String qName,
    Attributes attributes) throws SAXException {
    if (qName.equalsIgnoreCase("naam"))
      huidigElement = ELEMENT_NAAM;
    else if (qName.equalsIgnoreCase("adres")) {
      String adresType = attributes.getValue("type");
      if (adresType.equalsIgnoreCase("straatnr"))
        huidigElement = ELEMENT_STRAATNR;
      else if (adresType.equalsIgnoreCase("postnr"))
        huidigElement = ELEMENT_POSTNR;
      else if (adresType.equalsIgnoreCase("gemeente"))
        huidigElement = ELEMENT_GEMEENTE;
      else if (adresType.equalsIgnoreCase("telefoon"))
        huidigElement = ELEMENT_TELEFOON;
      else if (adresType.equalsIgnoreCase("land"))
        huidigElement = ELEMENT_LAND;
      else
        huidigElement = ELEMENT_ANDER;
    }
    else
      huidigElement = ELEMENT_ANDER;
  }

  /** Deze methode wordt aangeroepen bij het lezen van tekstinhoud.
   *  Zet de tekst om naar een String en plaats hem in de adresgegevens van de klant.
   */
  public void characters(char[] ch, int start, int length) {
    if (huidigElement == ELEMENT_NAAM)
      naam += new String(ch, start, length);
    else if (huidigElement == ELEMENT_STRAATNR)
      straatnr += new String(ch, start, length);
    else if (huidigElement == ELEMENT_POSTNR)
      postnr += new String(ch, start, length);
    else if (huidigElement == ELEMENT_GEMEENTE)
      gemeente += new String(ch, start, length);
    else if (huidigElement == ELEMENT_LAND)
      land += new String(ch, start, length);
  }

  /** Druk een adresetiket met behulp van de adresgegevens van de klant. */
  public void print() {
    if (!naam.trim().equals(""))
      System.out.println(naam.trim());
    if (!straatnr.trim().equals(""))
      System.out.println(straatnr.trim());
    if (!postnr.trim().equals(""))
      System.out.print(postnr.trim());
    if (!gemeente.trim().equals(""))
      System.out.print(" " + gemeente.trim());
    if (!gemeente.trim().equals("") || !postnr.trim().equals(""))
      System.out.println();
    if (!land.trim().equals(""))
      System.out.println(land.trim());
  }

  /** Lees een klantadres uit een XML-bestand en druk een adresetiket af.
   *  @param args[0]
   *    XML-bestandsnaam
   */
  public static void main(String[] args) throws IOException, SAXException, ParserConfigurationException {
    // Creeer een parser-factory
    SAXParserFactory spf = SAXParserFactory.newInstance();

    // We zijn geinteresseerd in alle welgevormde documenten,
    // we controleren niet de geldigheid tegenover een of andere DTD
    spf.setValidating(false);

    // Laat de parser-factory een parser-object creeren
    SAXParser sp = spf.newSAXParser();

    // Creeer een invoerstroom
    InputSource is = new InputSource(new FileReader(args[0]));

    // Creeer een content handler, in dit geval van het subtype DrukEtiket
    DrukEtiket handler = new DrukEtiket();

    // Lees en verwerk het XML-bestand
    sp.parse(is, handler);

    // Druk het adresetiket af
    handler.print();
  }
}

Als we bovenstaand programma de input van paragraaf 10.2 geven (voor de gelegenheid opgeslagen in een tekstbestand met de naam voorbeeld.xml), dan krijgen we het volgende resultaat.

C:\MijnJava>java DrukEtiket voorbeeld.xml
Jansen
Veldweg 6
1791 Ons Dorp

Bij de argumenten van startElement en endElement hoort enige uitleg. De eerste drie argumenten, uri, localName en qName geven alledrie informatie over de naam van het element. Hun concrete invulling hangt af van het gebruik van namespaces.

In ons voorbeeld is de parser niet namespace aware, we moeten dus de elementnamen uit het argument qName halen. Indien nodig, kunnen we het namespace aware-karakter van de parser beïnvloeden via de methode setNamespaceAware van de parser factory alvorens de parser te creëren.

Het argument attributes van de methode startElement modelleert een collectie (naam, waarde)-paren die kan ondervraagd worden met de methoden van de interface Attributes. Zo gebruiken wij de methode getValue(String) om de inhoud van een argument met gegeven naam te achterhalen:

      String adresType = attributes.getValue("type");

10.6 Document Object Model

DOM is een standaard die ook buiten de Java-wereld nog betekenis heeft. Hij is door het World Wide Web Consortium (W3C) ontwikkeld om een universele API te bieden voor het raadplegen en manipuleren van XML-documenten. Als dusdanig neemt hij de vorm aan van een CORBA-interface in de Interface Definition Language. Het W3C heeft echter bij de standaard meteen een vertaling naar een stel Java-interfaces gevoegd, een zogenaamde language mapping.

De DOM-standaard evolueert naar een steeds rijkere verzameling methoden. De meest recente versie op het moment dat we dit schrijven (september 2004) is DOM level 3. De huidige Java Development Kit 1.4 ondersteunt in het pakket org.w3c.dom de beperktere DOM level 2.

Het belangrijkste verschil tussen SAX en DOM vanuit het standpunt van de programmeur is dat je bij SAX passief, gebeurtenisgestuurd programmeert (de parser verwittigt je als hij een tag ontmoet), terwijl je bij DOM actief, procedureel programmeert. Het parsen van een document is slechts het opbouwen van een gegevensstructuur, een object van het type Document. De opzoekingen en manipulaties vinden achteraf plaats door methoden van de klasse Document aan te roepen.

De DOM API heeft twee verschillende soorten objecten waarmee de onderdelen van het XML-bestand kunnen worden gemodelleerd. Enerzijds de klassen Node en NodeList, die de boomstructuur modelleren zonder onderscheid van de soorten knopen (elementen, attributen en stukken tekst zijn allemaal voorbeelden van nodes). Anderzijds de klassen Element, Attribute, Text (en nog een paar andere) die het specifieke gedrag van elk soort boom-onderdeel modelleren. Zo heeft de klasse Element een methode getAttribute, omdat XML-elementen voorzien kunnen worden van attributen.

Voorbeeld

Het volgende programma DrukEtiketDOM doet hetzelfde als het vorige voorbeeld DrukEtiket, maar dan gebruikmakend van een document-objectmodel. De methode main construeert een parser (in de DOM-wereld heet dat een DocumentBuilder) en bouwt daarmee het documentobject. De methode print gaat op zoek naar de elementen van het type <naam> en <adres>, en formatteert daarmee een etiket.

import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/** Lees een klantadres uit een XML-bestand en druk een adresetiket af.
 *  Dit programma maakt gebruik van W3C Document Object Model om
 *  relevante informatie op te zoeken in de documentboom.
 */
public class DrukEtiketDOM {
  /** Druk een adresetiket met behulp van de adresgegevens van de klant. */
  public static void print(Document document, PrintStream bestemming) {
    String naam = "", straatnr = "", postnr="", gemeente="", land="";

    // Construeer de naam als concatenatie van de inhouden van alle naam-tags
    NodeList naamTags = document.getElementsByTagName("naam");
    for (int i = 0; i < naamTags.getLength(); i++) {
      Element naamTag = (Element) naamTags.item(i);
      Text naamDeel = (Text) naamTag.getFirstChild();
      naam += naamDeel.getNodeValue().trim();
    }

    // Construeer elk adresonderdeel als concatenatie van de inhoud van
    // alle adres-tags met het juiste type-attribuut
    NodeList adresTags = document.getElementsByTagName("adres");
    for (int i = 0; i < adresTags.getLength(); i++) {
      Element adresTag = (Element) adresTags.item(i);
      Text adresDeel = (Text) adresTag.getFirstChild();
      String adresType = adresTag.getAttribute("type");
      if (adresType == null)
        ;
      else if (adresType.equalsIgnoreCase("straatnr"))
        straatnr += adresDeel.getNodeValue().trim();
      else if (adresType.equalsIgnoreCase("postnr"))
        postnr += adresDeel.getNodeValue().trim();
      else if (adresType.equalsIgnoreCase("gemeente"))
        gemeente += adresDeel.getNodeValue().trim();
      else if (adresType.equalsIgnoreCase("land"))
        land += adresDeel.getNodeValue().trim();
    }

    if (!naam.trim().equals(""))
      bestemming.println(naam.trim());
    if (!straatnr.trim().equals(""))
      bestemming.println(straatnr.trim());
    if (!postnr.trim().equals(""))
      bestemming.print(postnr.trim());
    if (!gemeente.trim().equals(""))
      bestemming.print(" " + gemeente.trim());
    if (!gemeente.trim().equals("") || !postnr.trim().equals(""))
      bestemming.println();
    if (!land.trim().equals(""))
      bestemming.println(land.trim());
  }

  /** Lees een klantadres uit een XML-bestand en druk een adresetiket af.
   *  @param args[0]
   *    XML-bestandsnaam
   */
  public static void main(String[] args)
    throws IOException, ParserConfigurationException, SAXException {
    // Creeer een parser-factory
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

    // Laat de parser-factory een parser-object creeren
    DocumentBuilder db = dbf.newDocumentBuilder();

    // Creeer een invoerstroom
    InputSource is = new InputSource(new FileReader(args[0]));

    // Lees en verwerk het XML-bestand
    Document document = db.parse(is);

    // Druk het adresetiket af
    print(document, System.out);
  }
}

10.7 Vertaling (XSL)

Een vaak voorkomend probleem bij de behandeling van XML-documenten is: een gegeven XML-formaat (vastgelegd in een DTD of een schema) omzetten in een ander formaat. Dat doelformaat kan ook XML zijn (eventueel met een verschillende DTD), of een andere vorm (bijvoorbeeld HTML). Er bestaat een speciale vorm om dergelijke vertalingen vast te leggen: XML Stylesheet Transformation of XSLT. Naar de vorm is XSLT een XML-schema. XSLT-documenten, lijken op XML, maar zijn niet noodzakelijk welgevormd; ze kunnen tags en andere componenten van de doeltaal bevatten. XSLT-documenten worden stylesheets genoemd. Ze moeten niet verward worden met de zgn. cascading style sheets (CSS) van het World Wide Web; dat is een ouder formaat, waarmee sommige aspecten van de visuele weergave van een webpagina kunnen worden gestuurd.

We hernemen het voorbeeld van de adresetiketten. We kunnen XSLT gebruiken om een klant-document om te zetten in een "leesbaar" formaat, bijvoorbeeld HTML 4.01. De volgende stylesheet doet dit:

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:template match="klant">
  <html>
   <head>
   <title>Klantgegevens</title>
   </head>
   <body>
   <h1>Klantgegevens</h1>
    <xsl:apply-templates/>
   </body>
  </html>
 </xsl:template>
 <xsl:template match="naam">
  Naam: <xsl:value-of select="."/><br/>
 </xsl:template>
 <xsl:template match="adres">
  <xsl:if test="@type='straatnr'">
   <xsl:value-of select="."/><br/>
  </xsl:if>
  <xsl:if test="@type='postnr'">
   <xsl:value-of select="."/>
  </xsl:if>
  <xsl:if test="@type='gemeente'">
   <xsl:value-of select="."/><br/>
  </xsl:if>
  <xsl:if test="@type='land'">
   <xsl:value-of select="."/><br/>
  </xsl:if>
 </xsl:template>
 <xsl:template match="goedeklant">
  Goede klant!
 </xsl:template>
</xsl:stylesheet>

De eerste regel zegt dat onze stylesheet zelf een XML-document is. In de tweede regel zien we een element van het type stylesheet met een verwijzing naar de XSL-namespace. Vanaf dan bestaat de XSL-transformatie uit een reeks van drie template-elementen. Een template is in deze context een vertaalregel. Templates kunnen universeel op alle elementen uit het XML-document van toepassing zijn, maar in ons voorbeeld zijn ze alledrie selectie toepasbaar op elementen waarvan de naam overeenkomt met het attribuut match. Zo vertelt de volgende regel ons hoe een naam moet worden weergegeven in HTML:

 <xsl:template match="naam">
  Naam: <xsl:value-of select="."/><br/>
 </xsl:template>

De lege tag <xsl:value-of select="."/> herneemt de inhoud van het naam-element, d.w.z. de tekst tussen <naam> en </naam>. Zo wordt bijvoorbeeld de XML-code <naam>Jansen</naam> vertaald in de HTML: Naam: Jansen<br/>

Met het element <xsl:if/> wordt voorwaardelijke code opgenomen, afhankelijk van de logische waarde van het attribuut test. Daarmee kunnen we het onderscheid maken tussen een postnummer (wordt niet gevolgd door een regelafbreker) en een gemeente (wordt wel gevolgd door een regelafbreker).

De volledige vertaling van het XML-document van paragraaf 10.2 is de volgende HTML:

<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Klantgegevens</title>
</head>
<body>
<h1>Klantgegevens</h1>

  Naam: Jansen<br>
Veldweg 6<br>
1791
Ons Dorp<br>

  Goede klant!

</body>
</html>

De Java-pakketten javax.xml.transform en verwanten bieden interfaces voor de automatische vertaling van een XML-document aan de hand van een XSL-stylesheet. Het volgende programma verzorgt een minimale automatische vertaling. De output hierboven is er trouwens mee gegenereerd.

import java.io.FileInputStream;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import org.xml.sax.InputSource;

/** Automatische vertaling van een XML-document aan de
 *  hand van een XSL-stylesheet.
 */
public class XMLVertaler {
  /** Lees een XML-document en een XSL-stylesheet en
   *  vertaal het document aan de hand van de stylesheet.
   *  Toon het resultaat op het standaard uitvoermedium
   *  System.out.
   *  @param args[0]
   *    Padnaam van een XML-bestand.
   *  @param args[1]
   *    Padnaam van een XSL-bestand. Waarschuwing: kleine typefouten
   *    in de namespace-definitie kunnen veroorzaken dat het programma
   *    eindigt met een moeilijk te begrijpen Exception, bijvoorbeeld
   *    "version number required".
   */
  public static void main(String[] args) throws Exception {
    TransformerFactory tf = TransformerFactory.newInstance();
    StreamSource vertaler = new StreamSource(new FileInputStream(args[1]));
    Transformer t = tf.newTransformer(vertaler);
    InputSource is = new InputSource(new FileInputStream(args[0]));
    SAXSource bron = new SAXSource(is);
    StreamResult doel = new StreamResult(System.out);
    t.transform(bron, doel);
  }
}

De centrale klasse is Transformer met the methode transform. In ons voorbeeldprogramma heeft de tweede parameter van deze methode het type StreamResult, zodat we het resultaat naar het scherm kunnen sturen. In praktische toepassingen zal dit soms een SAXResult of een DOMResult zijn, zodat verdere parsing van het resultaat mogelijk is.

In voorbereiding: alternatieve stylesheet die willekeurige volgorde accepteert


inhoudstafel en auteursrecht
* STER *

Valid HTML 4.0! Valid CSS!