Avatar billede mergelspir Seniormester
14. december 2019 - 13:43 Der er 17 kommentarer og
2 løsninger

StAX pattern, best practice?

Jeg skal læse en stor XML-fil, og har derfor besluttet mig for at benytte StAX. Jeg har valgt at benytte cursor-metoden (og altså ikke iterator-metoden).

Alle de tuturials, jeg har set, viser kun, hvordan man bruger API'et.
De benytter derfor naturligvis en ukompliceret XML-fil.
fx en klasse med mange elever med navn, adresse, osv.
Her er det typisk en while-lykke, med følgende pattern:

public void parseXMLDocument(String XMLfilename) {
  while(xmlStreamReader.hasNext()){
    int event=xmlStreamReader.next();
    Switch (event) {
    case...
    case...
    case...
    }
  }
}


Men, lad os nu forestille os et land med mange skoler med mange klasser med mange elever med navn, adresse, (og endnu mere kompliceret)
Her kan jeg forestille mig to patterns:

1) Et pattern med switch-sætninger inde i switch-sætninger inde i switch-sætninger, osv
MEN, med kun een while-lykke med ,xmlStreamReader.hasNext() som i koden ovenfor.
Fordelen ved pattern 1 er, at det er en flad metode med een while-lykke, og kun een class.
Ulemperne er, at der er nestede switch-sætninger, samt at metoden vil bive ufattelig lang (læs 1000+ linier).

2) Et pattern et antal metoder, hvor hver metode læser hvert sit level i XML-strukturen, dvs. en for documentet som sådan, en for landet, en for skolen, en for klassen, en for eleven.
MEN, hver metode skal så have sin egen while-lykke med sin egen xmlStreamReader.hasNext().
Fordelen ved pattern 2 er, at hver level i XML-filen har sin egen metode, så metoderne bliver små.
Ulemperne er, at der skal een metode for hver XML-element, samt at hver metode skal have sin egen while-sætning med sin eget hasNext()-kald.
Det giver *mange* metoder, (og mange classes)

Her er så endelig mit spørgsmål:
Hvad er best practice med komplicerede XML-filer?
Pattern 1 eller 2, eller har jeg overset et hel tredie pattern?
Umiddelbart hælder jeg til 2'eren, da den er mest struktureret, men jeg orker næsten ikke at skrive 50+ classes.
Avatar billede arne_v Ekspert
14. december 2019 - 20:45 #1
Et interessant spørgsmål.

Jeg ved ikke hvad der betragtes som best practice.

Og jeg kan slet ikke forestille mig hvordan din løsning #1 vil se ud i praksis.

Men jeg kom jo til at tænke på hvordan jeg normalt gør det og hvad der er af alternativer.
Avatar billede arne_v Ekspert
14. december 2019 - 20:46 #2
Kode eksempel:


package december;

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

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

public class StaxBestPractice {
    // switch event type + switch node name
    public static void switchTypeSwitchName(Reader rdr) throws XMLStreamException {
        XMLInputFactory xif = XMLInputFactory.newInstance();
        XMLStreamReader xsr = xif.createXMLStreamReader(rdr);
        StringBuilder sb = new StringBuilder();
        while(xsr.hasNext()) {
            xsr.next();
            switch(xsr.getEventType()) {
                case XMLStreamReader.CHARACTERS:
                    sb.append(xsr.getText());
                    break;
                case XMLStreamReader.START_ELEMENT:
                    switch(xsr.getLocalName()) {
                        case "skole":
                            System.out.println("skole = " + xsr.getAttributeValue(null, "navn"));
                            break;
                        case "klasse":
                            System.out.println("  klasse = " + xsr.getAttributeValue(null, "navn"));
                            break;
                        case "elev":
                            System.out.println("    elev");
                            break;
                    }
                    sb.setLength(0);
                    break;
                case XMLStreamReader.END_ELEMENT:
                    switch(xsr.getLocalName()) {
                        case "navn":
                            System.out.println("      navn = " + sb.toString());
                            break;
                        case "adresse":
                            System.out.println("      adresse = " + sb.toString());
                            break;
                    }
                    break;
            }
        }
        xsr.close();
    }
    // switch event type + switch node name (with sub methods)
    private static void processCharacters(XMLStreamReader xsr, StringBuilder sb) {
        sb.append(xsr.getText());
    }
    private static void processStartElement(XMLStreamReader xsr, StringBuilder sb) {
        switch(xsr.getLocalName()) {
            case "skole":
                System.out.println("skole = " + xsr.getAttributeValue(null, "navn"));
                break;
            case "klasse":
                System.out.println("  klasse = " + xsr.getAttributeValue(null, "navn"));
                break;
            case "elev":
                System.out.println("    elev");
                break;
        }
        sb.setLength(0);
    }
    private static void processEndElement(XMLStreamReader xsr, StringBuilder sb) {
        switch(xsr.getLocalName()) {
            case "navn":
                System.out.println("      navn = " + sb.toString());
                break;
            case "adresse":
                System.out.println("      adresse = " + sb.toString());
                break;
        }
    }
    public static void switchTypeSwitchNameSub(Reader rdr) throws XMLStreamException {
        XMLInputFactory xif = XMLInputFactory.newInstance();
        XMLStreamReader xsr = xif.createXMLStreamReader(rdr);
        StringBuilder sb = new StringBuilder();
        while(xsr.hasNext()) {
            xsr.next();
            switch(xsr.getEventType()) {
                case XMLStreamReader.CHARACTERS:
                    processCharacters(xsr, sb);
                    break;
                case XMLStreamReader.START_ELEMENT:
                    processStartElement(xsr, sb);
                    break;
                case XMLStreamReader.END_ELEMENT:
                    processEndElement(xsr, sb);
                    break;
            }
        }
        xsr.close();
    }
    // process context based
    private static boolean moveStartElement(XMLStreamReader xsr, String targetelm, String stopelm) throws XMLStreamException {
        while(xsr.hasNext()) {
            if(xsr.getEventType() == XMLStreamReader.START_ELEMENT && xsr.getLocalName().equals(targetelm)) {
                return true;
            }
            if(xsr.getEventType() == XMLStreamReader.END_ELEMENT && xsr.getLocalName().equals(stopelm)) {
                return false;
            }
            xsr.next();
        }
        return false;
    }
    private static String grabText(XMLStreamReader xsr) throws XMLStreamException {
        StringBuilder sb = new StringBuilder();
        while(xsr.getEventType() == XMLStreamReader.CHARACTERS) {
            sb.append(xsr.getText());
            xsr.next();
        }
        return sb.toString();
    }
    private static void processElev(XMLStreamReader xsr) throws XMLStreamException {
        System.out.println("    elev");
        if(moveStartElement(xsr, "navn", "elev")) {
            xsr.next(); // move to text
            System.out.println("      navn = " + grabText(xsr));
        }
        if(moveStartElement(xsr, "adresse", "elev")) {
            xsr.next(); // move to text
            System.out.println("      adresse = " + grabText(xsr));
        }
    }
    private static void processKlasse(XMLStreamReader xsr) throws XMLStreamException {
        System.out.println("  klasse = " + xsr.getAttributeValue(null, "navn"));
        while(moveStartElement(xsr, "elev", "klasse")) {
            processElev(xsr);
        }
    }
    private static void processSkole(XMLStreamReader xsr) throws XMLStreamException {
        System.out.println("skole = " + xsr.getAttributeValue(null, "navn"));
        while(moveStartElement(xsr, "klasse", "skole")) {
            processKlasse(xsr);
        }
    }
    private static void processAlleSkoler(XMLStreamReader xsr) throws XMLStreamException {
        while(moveStartElement(xsr, "skole", "alleskoler")) {
            processSkole(xsr);
        }
    }
    public static void contextBased(Reader rdr) throws XMLStreamException {
        XMLInputFactory xif = XMLInputFactory.newInstance();
        XMLStreamReader xsr = xif.createXMLStreamReader(rdr);
        if(moveStartElement(xsr, "alleskoler", null)) {
            processAlleSkoler(xsr);
        }
        xsr.close();
    }
    public static void main(String[] args) throws XMLStreamException {
        String xmlstr = "<alleskoler>\r\n" +
                        "    <skole navn='Skole X'>\r\n" +
                        "        <klasse navn='1'>\r\n" +
                        "            <elev id='1'>\r\n" +
                        "                <navn>A A</navn>\r\n" +
                        "                <adresse>A Vej 1</adresse>\r\n" +
                        "            </elev>\r\n" +
                        "            <elev id='2'>\r\n" +
                        "                <navn>B B</navn>\r\n" +
                        "                <adresse>B Vej 2</adresse>\r\n" +
                        "            </elev>\r\n" +
                        "        </klasse>\r\n" +
                        "        <klasse navn='2'>\r\n" +
                        "            <elev id='3'>\r\n" +
                        "                <navn>C C</navn>\r\n" +
                        "                <adresse>C Vej 3</adresse>\r\n" +
                        "            </elev>\r\n" +
                        "            <elev id='4'>\r\n" +
                        "                <navn>D D</navn>\r\n" +
                        "                <adresse>D Vej 4</adresse>\r\n" +
                        "            </elev>\r\n" +
                        "        </klasse>\r\n" +
                        "    </skole>\r\n" +
                        "    <skole navn='Skole Y'>\r\n" +
                        "        <klasse navn='8'>\r\n" +
                        "            <elev id='5'>\r\n" +
                        "                <navn>E E</navn>\r\n" +
                        "                <adresse>E Vej 5</adresse>\r\n" +
                        "            </elev>\r\n" +
                        "            <elev id='6'>\r\n" +
                        "                <navn>F F</navn>\r\n" +
                        "                <adresse>F Vej 6</adresse>\r\n" +
                        "            </elev>\r\n" +
                        "        </klasse>\r\n" +
                        "        <klasse navn='9'>\r\n" +
                        "            <elev id='7'>\r\n" +
                        "                <navn>G G</navn>\r\n" +
                        "                <adresse>G Vej 7</adresse>\r\n" +
                        "            </elev>\r\n" +
                        "            <elev id='8'>\r\n" +
                        "                <navn>H H</navn>\r\n" +
                        "                <adresse>H Vej 8</adresse>\r\n" +
                        "            </elev>\r\n" +
                        "        </klasse>\r\n" +
                        "    </skole>\r\n" +
                        "</alleskoler>\r\n";
        System.out.print(xmlstr);
        switchTypeSwitchName(new StringReader(xmlstr));
        switchTypeSwitchNameSub(new StringReader(xmlstr));
        contextBased(new StringReader(xmlstr));
    }
}
Avatar billede arne_v Ekspert
14. december 2019 - 20:46 #3
Output:


<alleskoler>
    <skole navn='Skole X'>
        <klasse navn='1'>
            <elev id='1'>
                <navn>A A</navn>
                <adresse>A Vej 1</adresse>
            </elev>
            <elev id='2'>
                <navn>B B</navn>
                <adresse>B Vej 2</adresse>
            </elev>
        </klasse>
        <klasse navn='2'>
            <elev id='3'>
                <navn>C C</navn>
                <adresse>C Vej 3</adresse>
            </elev>
            <elev id='4'>
                <navn>D D</navn>
                <adresse>D Vej 4</adresse>
            </elev>
        </klasse>
    </skole>
    <skole navn='Skole Y'>
        <klasse navn='8'>
            <elev id='5'>
                <navn>E E</navn>
                <adresse>E Vej 5</adresse>
            </elev>
            <elev id='6'>
                <navn>F F</navn>
                <adresse>F Vej 6</adresse>
            </elev>
        </klasse>
        <klasse navn='9'>
            <elev id='7'>
                <navn>G G</navn>
                <adresse>G Vej 7</adresse>
            </elev>
            <elev id='8'>
                <navn>H H</navn>
                <adresse>H Vej 8</adresse>
            </elev>
        </klasse>
    </skole>
</alleskoler>
skole = Skole X
  klasse = 1
    elev
      navn = A A
      adresse = A Vej 1
    elev
      navn = B B
      adresse = B Vej 2
  klasse = 2
    elev
      navn = C C
      adresse = C Vej 3
    elev
      navn = D D
      adresse = D Vej 4
skole = Skole Y
  klasse = 8
    elev
      navn = E E
      adresse = E Vej 5
    elev
      navn = F F
      adresse = F Vej 6
  klasse = 9
    elev
      navn = G G
      adresse = G Vej 7
    elev
      navn = H H
      adresse = H Vej 8
skole = Skole X
  klasse = 1
    elev
      navn = A A
      adresse = A Vej 1
    elev
      navn = B B
      adresse = B Vej 2
  klasse = 2
    elev
      navn = C C
      adresse = C Vej 3
    elev
      navn = D D
      adresse = D Vej 4
skole = Skole Y
  klasse = 8
    elev
      navn = E E
      adresse = E Vej 5
    elev
      navn = F F
      adresse = F Vej 6
  klasse = 9
    elev
      navn = G G
      adresse = G Vej 7
    elev
      navn = H H
      adresse = H Vej 8
skole = Skole X
  klasse = 1
    elev
      navn = A A
      adresse = A Vej 1
    elev
      navn = B B
      adresse = B Vej 2
  klasse = 2
    elev
      navn = C C
      adresse = C Vej 3
    elev
      navn = D D
      adresse = D Vej 4
skole = Skole Y
  klasse = 8
    elev
      navn = E E
      adresse = E Vej 5
    elev
      navn = F F
      adresse = F Vej 6
  klasse = 9
    elev
      navn = G G
      adresse = G Vej 7
    elev
      navn = H H
      adresse = H Vej 8
Avatar billede arne_v Ekspert
14. december 2019 - 20:50 #4
Jeg plejer normalt at bruge den første metode.

Men med tilpas komplekse strukturer må den kontekst baserede metode give mere overskuelig kode.

Men det er bare mig.

Jeg tror at de fleste kun bruger SAX/StAX til simpel meget stor XML mens man bruger XML binding (JAXB i Java) eller DOM med XPath til kompleks mindre XML.

Og at grunden til at der er lidt omkirng kompleks meget stor XML er at det sjældent er situationen.
Avatar billede mergelspir Seniormester
15. december 2019 - 18:00 #5
Det XML-format, som jeg skal læse, har mange forskellige tags (50+)
Volumenmæssigt varierer det en del. Den største XML-fil er pt. 95 MB.
Men jeg skal kunne læse større filer end den.

Det første pattern, jeg beskrev, havde jeg tænkt som en tilstandsmaskine. Derfor de mange switches.

Jeg synes, at begge dine forslag er bedre end mine :-(. Jeg går helt klart ind for velstruktureret kode, men jeg er også doven! Jeg vil jo gerne have både i pose og i sæk. Det ser ud til, at dine forslag giver mig det.
Avatar billede arne_v Ekspert
15. december 2019 - 18:37 #6
Min tommelfinger regel siger at et DOM træ fylder 3-4 gange så meget som XML text.

Så 95 MB XML fil vil med overvejende sandsynlighed være mindre end 400 MB.

400 MB kan sagtens være i memory idag.

Er det en 9.5 GB XML fil, så ....
Avatar billede arne_v Ekspert
15. december 2019 - 18:42 #7
Jeg prøvede også at lave et eksempel med state, men jeg stoppede da det ikke syntes at give meget værdi.

Det grundliggende problem synes at være at hvis man kender state, så ved man at der kun er få mulige input, men man skal alligevel hente og teste på næste input, og valideringen kan laves via DTD eller schema validering.
Avatar billede arne_v Ekspert
15. december 2019 - 18:44 #8
Og så vil jeg lige linke til hvad jeg har skrevet om XML:

http://www.vajhoej.dk/arne/articles/xmlproc.html
Avatar billede mergelspir Seniormester
15. december 2019 - 22:15 #9
"400 MB kan sagtens være i memory idag."
Ja, sådan burde det være, men jeg hoster min JVM hos Levonline i Stockholm, og der tilbyder de kun 64 MB i den lille version og 256 MB i deluxe-udgaven.
De tilbyder iøvrigt også kun Java 1.7.
Jeg skulle på et tidspunkt bruge Java 1.8-funktionalitet. Min work-around var at hente kildeteksten og oversætte den i v1.7, og så kørte det.
Ja, nogle gange må man sno sig.
Avatar billede arne_v Ekspert
16. december 2019 - 01:05 #10
Det var godt nok ikke meget.

Måske er det snart tid at flytte til PaaS cloud som f.eks. Google App Engine for Java eller IaaS cloud som f.eks. Amazon AWS.

:-)

Jeg checkede lige - Google app engine understøtter både Java 8 og 11, memory fra 128 MB til 2048 MB,
Avatar billede mergelspir Seniormester
29. december 2019 - 00:32 #11
Jeg har lige et tillægsspørgsmål:
Når jeg læser xml-filen ind fra en fil, går det ok, så længe jeg ikke har fx æøå.
Prøv fx denne version:
    public static void contextBased(String filename) throws XMLStreamException, FileNotFoundException {
        XMLInputFactory xif = XMLInputFactory.newInstance();
        InputStream input = new FileInputStream(new File(filename));
        XMLStreamReader xsr = xif.createXMLStreamReader(input);
        if(moveStartElement(xsr, "alleskoler", null)) {
            processAlleSkoler(xsr);
        }
        xsr.close();
    }
    public static void main(String[] args) throws XMLStreamException, FileNotFoundException {
        if (args[0]==null) {
            System.out.println("XML input fil mangler");
        } else {
            contextBased(args[0]);
        }
    }
, hvor skolen hedder "Blåbærgrød", så får jeg fejlen:
"Invalid byte 3 of 3-byte UTF-8 sequence."
Hvorfor det?!? Jeg læser jo filen ind som en binær fil.
Avatar billede arne_v Ekspert
29. december 2019 - 00:56 #12
Hvis jeg skulle gætte: Java antager at filen er i UTF-8 encoding enten fordi XML filen siger det eller fordi det er default, men filen er faktisk i en anden encoding formentligt ISO-8859-1.
Avatar billede arne_v Ekspert
29. december 2019 - 01:14 #13
Jeg tror på mit gæt.


import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.xml.sax.SAXException;

public class XmlAndEncoding {
    public static void test(String label, String actual) throws UnsupportedEncodingException, ParserConfigurationException, SAXException {
        System.out.printf("%s labeled as %s: ",  actual, label);
        String xmls = "<?xml version='1.0' encoding='" + label + "' standalone='yes'?>\r\n<data>Blåbærgrød</data>";
        byte[] xmlb = xmls.getBytes(actual);
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        try {
            @SuppressWarnings("unused")
            Document doc = db.parse(new ByteArrayInputStream(xmlb));
            System.out.println("OK");
        } catch (IOException e) {
            System.out.println(e.getMessage());
        }
    }
    public static void main(String[] args) throws Exception {
        test("UTF-8", "UTF-8");
        test("ISO-8859-1", "ISO-8859-1");
        test("ISO-8859-1", "UTF-8");
        test("UTF-8", "ISO-8859-1");
    }
}


giver:

UTF-8 labeled as UTF-8: OK
ISO-8859-1 labeled as ISO-8859-1: OK
UTF-8 labeled as ISO-8859-1: OK
ISO-8859-1 labeled as UTF-8: Invalid byte 2 of 3-byte UTF-8 sequence.
Avatar billede arne_v Ekspert
29. december 2019 - 01:15 #14
UTF-8 labeled as ISO-8859-1

giver naturligvis et forkert resultat, men det smider ikke en exception.
Avatar billede mergelspir Seniormester
29. december 2019 - 14:48 #15
Tak skal du have. Din kommentar ledte mig på sporet.

En af af de XML-filer, jeg forsøgte at læse, manglede linien
<?xml version="1.0" encoding="ISO-8859-1"?>

Jeg havde set mig blind på, at jeg læste filen ind som binary, og at der derfor ikke burde kastes en exception.

En lille kuriøsitet
ø: Invalid byte 2 of 3-byte UTF-8 sequence.
ö: Invalid byte 2 of 4-byte UTF-8 sequence.
------------------------------------------------------
Forklaring:
ø: 11111000 (ASCII)
ö: 11110110 (ASCII)
1110xxxx = 3 byte UTF-8
11110xxx = 4 byte UTF-8
Avatar billede mergelspir Seniormester
29. december 2019 - 14:53 #16
Når jeg læser min "forklaring", så ligner det vrøvl. Hvorfor bliver ø til 3 byte UTF?!?
Avatar billede arne_v Ekspert
29. december 2019 - 15:46 #17
Jeg tror at det er æ ikke ø som giver den fejl.

æ = 11100110
Avatar billede mergelspir Seniormester
30. december 2019 - 11:55 #18
Ja eller å = 11100101
Tak! Jeg bryder mig ikke om, når der er noget jeg ikke forstår, så nu kan jeg sove roligt :-)

Jeg har opdaget en besynderlighed ved StAX:

<item name="navn">Hans og Grete</item>
her er CHARACTERS: "Hans og Grete"
MEN:
<item name="navn">Hans &amp Grete</item>
her er CHARACTERS: "Hans", "&",  "Grete"

Kan man undgå, at CHARACTERS bliver delt?
Der er tegn, der er ulovlige, men så vidt jeg kan se af dokumentationen, er "Hans &amp Grete" fuldt lovlig, så jeg kan ikke se nogen grund til, at det bliver delt i tre.
Avatar billede arne_v Ekspert
30. december 2019 - 14:40 #19
Næppe.

Det er en del af API at implementationen kan splitte tekst i flere characater events.

Begrundelsen er memory. Der kunne være en 500 MB tekst. Og hele pointen i streaming XML er at undgå store objekter i memory.

Det er naturligvis ikke et problem i dit tilfælde. Men here tror jeg bare at de forsøger at undgå et midlertidigt String objekt.
Avatar billede Ny bruger Nybegynder

Din løsning...

Tilladte BB-code-tags: [b]fed[/b] [i]kursiv[/i] [u]understreget[/u] Web- og emailadresser omdannes automatisk til links. Der sættes "nofollow" på alle links.

Loading billede Opret Preview
Kategori
Kurser inden for grundlæggende programmering

Log ind eller opret profil

Hov!

For at kunne deltage på Computerworld Eksperten skal du være logget ind.

Det er heldigvis nemt at oprette en bruger: Det tager to minutter og du kan vælge at bruge enten e-mail, Facebook eller Google som login.

Du kan også logge ind via nedenstående tjenester