Probleme mit Auszeichnungen in einem Textbereich

Ich bekomme von einem Kollegen eine XML-Datei, die mich vor ein unerwartetes Problem stellt. Die XML-Dateien, die ich bisher verwendet hatte, hatten als Inhalt von Tags entweder nur Text, oder andere Tags. Aber keine Mischform. Also etwa so:

<dinge>
    <information>huhu</information>
    <zeugs>
        <zeug>A</zeug>
        <zeug>B</zeug>
    </zeugs>
</dinge>

Nun aber sieht ein Exemplar dieser Dateien so aus:

<dinge>
    <information>huhu<oha>ueberraschung!</oha></information>
</dinge>

Das wird wohl kaum den XML-Regeln widersprechen, immerhin gibt es sowas in HTML ja auch:

<p>Dies ist ein Satz mit einem <b>fettgeschriebenem</b> Wort.</p>

Ich weiß nur nicht so genau, wie ich dies nun parsen soll. Ich hab mal ein KKSB (oder wie sich das noch gleich abkürzte) geschrieben:


import java.io.Reader;
import java.io.StringReader;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

public class XmlProblemDemonstration {

    public class XmlInfo {
        private String information;

        public String getInformation() {
            return information;
        }

        void setInformation(String information) {
            this.information = information;
        }

        @Override
        public String toString() {
            return "XmlInfo [information=" + information + "]";
        }
    }

    public XmlInfo work(String input) {
        XmlInfo xmlInfo = null;
        try {
            xmlInfo = tryToRead(input);
        } catch (XMLStreamException exception) {
            exception.printStackTrace();
        }
        return xmlInfo;
    }

    private XmlInfo tryToRead(String input) throws XMLStreamException {
        Reader reader = new StringReader(input);
        XMLInputFactory factory = XMLInputFactory.newInstance();
        XMLStreamReader parser = factory.createXMLStreamReader(reader);

        XmlInfo xmlInfo = new XmlInfo();

        while (parser.hasNext()) {
            parser.next();
            switch (parser.getEventType()) {
                case XMLStreamConstants.START_ELEMENT:
                    switch (parser.getLocalName()) {
                        case "information": {
                            String information = parser.getElementText();
                            //String information = parser.getText();
                            xmlInfo.setInformation(information);
                            break;
                        }
                    }
                    break;
            }
        }
        return xmlInfo;
    }

    public static void main(String[] args) {
        XmlProblemDemonstration demo = new XmlProblemDemonstration();
        for (String input : new String[] {
                "<dinge><information>foo</information></dinge>",
                "<dinge><information>bar</information></dinge>",
                "<dinge><information>baz<oha>ueberraschung!</oha></information></dinge>"
                }) {
            System.out.println("Eingabe: " + input);
            XmlInfo info = demo.work(input);
            if (null == info) {
                System.out.println("Fehler in der Verarbeitung!");
            }
            else {
                System.out.println("Ergebnis: " + info);
            }
            System.out.println();
        }
    }

}

Die Ausgabe ist:

Eingabe: <dinge><information>foo</information></dinge>
Ergebnis: XmlInfo [information=foo]

Eingabe: <dinge><information>bar</information></dinge>
Ergebnis: XmlInfo [information=bar]

Eingabe: <dinge><information>baz<oha>ueberraschung!</oha></information></dinge>
javax.xml.stream.XMLStreamException: ParseError at [row,col]:[1,29]
Message: elementGetText() function expects text only elment but START_ELEMENT was encountered.
	at com.sun.org.apache.xerces.internal.impl.XMLStreamReaderImpl.getElementText(Unknown Source)
	at xml.XmlProblemDemonstration.tryToRead(XmlProblemDemonstration.java:53)
	at xml.XmlProblemDemonstration.work(XmlProblemDemonstration.java:33)
	at xml.XmlProblemDemonstration.main(XmlProblemDemonstration.java:73)
Fehler in der Verarbeitung!


Gefunden habe ich schon: http://stackoverflow.com/questions/13218352/java-xmlstreamreader-how-to-get-element-text-when-text-contains-start-element

Aber wenn ich die auskommentierte Zeile mit getText statt der mit getElementText verwende, bekomme ich den folgenden Fehler:

Eingabe: <dinge><information>foo</information></dinge>
Exception in thread "main" java.lang.IllegalStateException: Current state START_ELEMENT is not among the statesCHARACTERS, COMMENT, CDATA, SPACE, ENTITY_REFERENCE, DTD valid for getText() 
	at com.sun.org.apache.xerces.internal.impl.XMLStreamReaderImpl.getText(Unknown Source)
	at xml.XmlProblemDemonstration.tryToRead(XmlProblemDemonstration.java:54)
	at xml.XmlProblemDemonstration.work(XmlProblemDemonstration.java:33)
	at xml.XmlProblemDemonstration.main(XmlProblemDemonstration.java:73)

Vermutlich nur ein Knoten in den Gedanken. Kann mich jemand in die richtige Richtung stupsen?

Ich könnte natürlich damit anfangen, diese Xml-Datei mit regulären Ausdrücken zu bearbeiten, aber eigentlich ist das ja der deutlich weniger elegante Weg, und wenn sich eine Parser-Lösung bietet, wäre das vorzuziehen.

Inzwischen habe ich mit einem Freund zusammen folgende Lösung gefunden. Wie schön das ganze ist, steht auf einem anderen Blatt, aber so “funktioniert” es zumindest, alle mich interessierenden Informationen aus der XML-Datei zu extrahieren.


import java.io.Reader;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;

import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

public class XmlProblemDemonstration2 {

    public class XmlInfo {
        private String information;
        private Map<String, String> informationParts;

        public String getInformation() {
            return information;
        }

        public Map<String, String> getInformationParts() {
            return informationParts;
        }

        void setInformationParts(Map<String, String> informationParts) {
            this.informationParts = informationParts;
        }

        void setInformation(String information) {
            this.information = information;
        }

        @Override
        public String toString() {
            return "XmlInfo [information=" + information
                    + ", informationParts=" + informationParts + "]";
        }
    }

    public XmlInfo work(String input) {
        XmlInfo xmlInfo = null;
        try {
            xmlInfo = tryToRead(input);
        }
        catch (XMLStreamException exception) {
            exception.printStackTrace();
        }
        catch (FactoryConfigurationError error) {
            error.printStackTrace();
        }
        return xmlInfo;
    }

    private XmlInfo tryToRead(String input) throws FactoryConfigurationError,
            XMLStreamException {
        XMLStreamReader parser = openXmlParser(input);
        XmlInfo xmlInfo = new XmlInfo();

        /* Das zuletzt geöffnete Elemente. */
        String element = null;

        /* Gibt an, ob information offen ist. */
        boolean informationIsOpen = false;

        /*
         * Verzeichnis der anderen Unter-Informationen, die zu "information"
         * gehören.
         */
        Map<String, String> informationParts = null;

        while (parser.hasNext()) {
            parser.next();
            switch (parser.getEventType()) {
                case XMLStreamConstants.START_ELEMENT:
                    /* Wir merken uns, welches Element zuletzt geöffnet wurde: */
                    element = parser.getLocalName();
                    //System.out.println("Öffnendes Element: " + element);

                    /*
                     * Falls sich information öffnet, merken wir uns, dass
                     * dieses Element geöffnet ist und initialisieren das
                     * Verzeichnis der anderen zur Information gehörenden Werte:
                     */
                    if ("information".equals(element)) {
                        informationIsOpen = true;
                        informationParts = new HashMap<>();
                    }
                    break;
                case XMLStreamConstants.END_ELEMENT:
                    switch (parser.getLocalName()) {
                        case "information": {
                            informationIsOpen = false;
                            xmlInfo.setInformationParts(informationParts);
                        }
                    }
                    break;
                case XMLStreamConstants.CDATA:
                    //System.out.println("CDATA: " + parser.getText());
                    break;
                case XMLStreamConstants.SPACE:
                    //System.out.println("SPACE: " + parser.getText());
                    break;
                case XMLStreamConstants.CHARACTERS:
                    String characters = parser.getText();
                    //System.out.println("CHARACTERS: " + characters);

                    /*
                     * Wenn "information" geöffnet ist, aber nicht zuletzt
                     * "information" geöffnet wurde, sondern ein anderes
                     * Element, sichern wir dieses:
                     */
                    if (informationIsOpen && !"information".equals(element)) {
                        informationParts.put(element, characters);
                        /*
                         * Falls das letzte öffnende Element information war,
                         * die Information sichern:
                         */
                        if ("information".equals(element)) {
                            xmlInfo.setInformation(characters);
                        }
                    }
                    break;
            }
        }
        return xmlInfo;
    }

    private XMLStreamReader openXmlParser(String input)
            throws FactoryConfigurationError, XMLStreamException {
        Reader reader = new StringReader(input);
        XMLInputFactory factory = XMLInputFactory.newInstance();
        XMLStreamReader parser = factory.createXMLStreamReader(reader);
        return parser;
    }

    public static void main(String[] args) {
        XmlProblemDemonstration2 demo = new XmlProblemDemonstration2();
        for (String input : new String[] {
                "<dinge><information>foo</information></dinge>",
                "<dinge><information>bar</information></dinge>",
                "<dinge><information>baz<oha>ueberraschung!</oha></information></dinge>"
                }) {
            System.out.println("Eingabe: " + input);
            XmlInfo info = demo.work(input);
            if (null == info) {
                System.out.println("Fehler in der Verarbeitung!");
            }
            else {
                System.out.println("Ergebnis: " + info);
            }
            System.out.println();
        }
    }

}

Ausgabe:

Eingabe: <dinge><information>foo</information></dinge>
Ergebnis: XmlInfo [information=null, informationParts={}]

Eingabe: <dinge><information>bar</information></dinge>
Ergebnis: XmlInfo [information=null, informationParts={}]

Eingabe: <dinge><information>baz<oha>ueberraschung!</oha></information></dinge>
Ergebnis: XmlInfo [information=null, informationParts={oha=ueberraschung!}]


Vermutlich ist für mein eigentliches Problem keine Map die Lösung, sondern eine Liste solcher String-Paare, damit nicht zwei “oha”-Elemente einander überschrieben.

*** Edit ***

Falls es interessiert, die Lösung sieht dann so aus:


import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

public class XmlProblemDemonstration3 {

    public class ElementData {
        private final String element;
        private final String data;
        public ElementData(String element, String data) {
            this.element = element;
            this.data = data;
        }
        public String getElement() {
            return element;
        }
        public String getData() {
            return data;
        }
        @Override
        public String toString() {
            return "ElementData [element=" + element + ", data=" + data + "]";
        }
    }

    public class XmlInfo {
        private String information;
        private List<ElementData> informationParts;

        public String getInformation() {
            return information;
        }

        public List<ElementData> getInformationParts() {
            return informationParts;
        }

        void setInformationParts(List<ElementData> informationParts) {
            this.informationParts = informationParts;
        }

        void setInformation(String information) {
            this.information = information;
        }

        @Override
        public String toString() {
            return "XmlInfo [information=" + information
                    + ", informationParts=" + informationParts + "]";
        }
    }

    public XmlInfo work(String input) {
        XmlInfo xmlInfo = null;
        try {
            xmlInfo = tryToRead(input);
        }
        catch (XMLStreamException exception) {
            exception.printStackTrace();
        }
        catch (FactoryConfigurationError error) {
            error.printStackTrace();
        }
        return xmlInfo;
    }

    private XmlInfo tryToRead(String input) throws FactoryConfigurationError,
            XMLStreamException {
        XMLStreamReader parser = openXmlParser(input);

        /*
         * In diesem Objekt werden die aus der XML-Datei eingelesenen
         * Informationen zusammengetragen:
         */
        XmlInfo xmlInfo = new XmlInfo();

        /* Das zuletzt geöffnete Elemente: */
        String element = null;

        /* Gibt an, ob information offen ist: */
        boolean informationIsOpen = false;

        /*
         * Liste der anderen Unter-Informationen, die zu "information" gehören:
         */
        List<ElementData> informationParts = null;

        while (parser.hasNext()) {
            parser.next();
            switch (parser.getEventType()) {
                case XMLStreamConstants.START_ELEMENT:
                    /* Wir merken uns, welches Element zuletzt geöffnet wurde: */
                    element = parser.getLocalName();
                    //System.out.println("Öffnendes Element: " + element);

                    /*
                     * Falls sich information öffnet, merken wir uns, dass
                     * dieses Element geöffnet ist und initialisieren das
                     * Verzeichnis der anderen zur Information gehörenden Werte:
                     */
                    if ("information".equals(element)) {
                        informationIsOpen = true;
                        informationParts = new ArrayList<>();
                    }
                    break;
                case XMLStreamConstants.END_ELEMENT:
                    switch (parser.getLocalName()) {
                        case "information": {
                            informationIsOpen = false;
                            xmlInfo.setInformationParts(informationParts);
                        }
                    }
                    break;
                case XMLStreamConstants.CDATA:
                    System.out.println("CDATA: " + parser.getText());
                    break;
                case XMLStreamConstants.SPACE:
                    System.out.println("SPACE: " + parser.getText());
                    break;
                case XMLStreamConstants.CHARACTERS:
                    String characters = parser.getText();
                    //System.out.println("CHARACTERS: " + characters);

                    /*
                     * Wenn "information" geöffnet ist, aber nicht zuletzt
                     * "information" geöffnet wurde, sondern ein anderes
                     * Element, sichern wir dieses:
                     */
                    if (informationIsOpen) {
                        /*
                         * Falls das letzte öffnende Element information war,
                         * die Information sichern:
                         */
                        if ("information".equals(element)) {
                            xmlInfo.setInformation(characters);
                        }
                        /* anderenfalls die zusätzlichen Daten abspeichern: */
                        else {
                            informationParts.add(new ElementData(element, characters));
                        }
                    }
                    break;
            }
        }
        return xmlInfo;
    }

    private XMLStreamReader openXmlParser(String input)
            throws FactoryConfigurationError, XMLStreamException {
        Reader reader = new StringReader(input);
        XMLInputFactory factory = XMLInputFactory.newInstance();
        XMLStreamReader parser = factory.createXMLStreamReader(reader);
        return parser;
    }

    public static void main(String[] args) {
        XmlProblemDemonstration3 demo = new XmlProblemDemonstration3();
        for (String input : new String[] {
                "<dinge><information>foo</information></dinge>",
                "<dinge><information>bar</information></dinge>",
                "<dinge><information>baz<oha>ueberraschung!</oha></information></dinge>",
                "<dinge><information>baz<oha>ueberraschung!</oha><oha>ueberraschung2!</oha></information></dinge>"
                }) {
            System.out.println("Eingabe: " + input);
            XmlInfo info = demo.work(input);
            if (null == info) {
                System.out.println("Fehler in der Verarbeitung!");
            }
            else {
                System.out.println("Ergebnis: " + info);
            }
            System.out.println();
        }
    }

}

Mit der Ausgabe:

Eingabe: <dinge><information>foo</information></dinge>
Ergebnis: XmlInfo [information=foo, informationParts=[]]

Eingabe: <dinge><information>bar</information></dinge>
Ergebnis: XmlInfo [information=bar, informationParts=[]]

Eingabe: <dinge><information>baz<oha>ueberraschung!</oha></information></dinge>
Ergebnis: XmlInfo [information=baz, informationParts=[ElementData [element=oha, data=ueberraschung!]]]

Eingabe: <dinge><information>baz<oha>ueberraschung!</oha><oha>ueberraschung2!</oha></information></dinge>
Ergebnis: XmlInfo [information=baz, informationParts=[ElementData [element=oha, data=ueberraschung!], ElementData [element=oha, data=ueberraschung2!]]]


Ohne mir das jetzt genauer angeschaut zu haben: Wenn du Markup hast, das zu den Daten und nicht zur eigentlichen Struktur des XMLs gehört, ist die beste Lösung normalerweise, die Daten mit Tags in eine CDATA-Sektion zu packen.

[quote=Crian]Ich bekomme von einem Kollegen eine XML-Datei, die mich vor ein unerwartetes Problem stellt.[/quote]Habt ihr eine XSD vereinbart, die die Struktur der Datei beschreibt?

Das solltet ihr unbedingt tun, denn dies bietet 2 Vorteile.[ol]
[li]Du kannst die Datei Deines Kollegen dagegen validieren und bei solchen “Überraschungen” intervenieren.[/li][li]Du kannst die das Parsen der Datei von JaxB, Castor oder ähnlichen Frameworks abnehmen lassen.[/li][/ol]bye
TT