jaxb XML Mapper - Root Element mit Attribut

8u3631984

Bekanntes Mitglied
Guten Morgen.

Ich möchte gerne folgenden XML String in Object mappen:
Code:
<seasons xmlns="http://sams-server.de/api/xml/ns/seasons" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://sams-server.de/api/xml/ns/seasons seasons.xsd">
<season>
<id>766158250</id>
<name>2023/24</name>
<begin>2023-07-01</begin>
<end>2024-06-30</end>
</season>
...
</seasons>

Dazu habe ich folgenden Klassen erstellt:

Java:
@XmlRootElement(name = "seasons")
@Getter
@ToString
public class XMLSeasons implements XMLObject {

    @XmlElement(name = "season")
    private List<XMLSeason> seasons;

}

Code:
@Getter
@ToString
public class XMLSeason {

    @XmlElement
    private String id;
}

Zum eigentlichen Mappen verwende ich folgenden Code:
Java:
@Component
@Log4j2
public class XMLSeasonMapper implements XMLMapper<XMLSeasons> {

    @Override
    @SneakyThrows
    public XMLSeasons mapXMLToObject(@NonNull String xmlData) {
        JAXBContext jaxbContext = JAXBContext.newInstance(XMLSeasons.class);
        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();

        StringReader reader = new StringReader(xmlData);
        XMLSeasons xmlSeasons = (XMLSeasons) unmarshaller.unmarshal(reader);

        xmlSeasons.getSeasons().stream().forEach(mappedSeason -> log.info("{}", kv("xml season", mappedSeason)));
        return xmlSeasons;
    }
}

WEnn ich nun den Code laufen lassen bekomme ich folgenden Fehler :
Code:
jakarta.xml.bind.UnmarshalException: unerwartetes Element (URI:"http://sams-server.de/api/xml/ns/seasons", lokal:"seasons"). Erwartete Elemente sind [{}seasons]

Für mich auch nachvollziehbar, da ja im Seasons Root element noch das Attribut "xmlns="http://sams-server.de/api/xml/ns/seasons" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://sams-server.de/api/xml/ns/seasons seasons.xsd" hängt. Nun meine Frage wie kann man das konfigurieren ?

Vielen Dank für die Hilfe
 
Wenn ich das so richtig verstehe, dann ist das Problem durch den Namespace?
Den kannst Du ja mit angeben beim XmlRootElement:
@XmlRootElement(namespace = "http://sams-server.de/api/xml/ns/seasons")

Und was mir noch durch den Kopf geht: Man kann auch Typen mit angeben, also bei XMLSeason:
@XmlType(name = "season")

Ich muss aber gestehen, dass ich mit XML schon sehr lange nichts mehr gemacht habe. Daher sind das nur kleine Vorschläge zum selbst ausprobieren. Und ich sage mal sorry, falls Dich das nicht weitergebracht haben sollte. (Habe jetzt leider gerade keine Zeit für einen schnellen Test auf meiner Seite.)
 
Ich habe beide Annotationen angepasst - also :
Java:
@XmlRootElement(namespace = "http://sams-server.de/api/xml/ns/seasons")
und
Code:
@XmlType(name = "season")

Allerdings klappt nun das Mapping nicht mehr :
Code:
XMLSeasons xmlSeasons = (XMLSeasons) unmarshaller.unmarshal(reader);
        xmlSeasons.getSeasons().stream().forEach(mappedSeason -> log.info("{}", kv("xml season", mappedSeason)));

getSeasons liefert imm NULL zurück
 
Sorry, das war mein Fehler. Ich hatte jetzt nur die Annotation namespace zeigen wollen. Da der KLassenname vom Root-Tag abweicht, wirst Du den Namen weiter angeben müssen. (Sprich: Das was Du schon hattest).

Die Annotation würde dann z.B. so aussehen:
@XmlRootElement(name = "seasons", namespace = "http://sams-server.de/api/xml/ns/seasons")

Das hätte ich natürlich zumindest erwähnen müssen.
 
Nein das war nicht dein Fehler ich hatte es genau so verstande, ich hatte das auch so umgesetzt nur den aktuellen Codestand nicht veröffentlicht. So sieht es nun aus :
Java:
@XmlRootElement(name = "seasons", namespace = "http://sams-server.de/api/xml/ns/seasons")
@Getter
@ToString
public class XMLSeasons implements XMLObject {

    @XmlElement(name = "season")
    private List<XMLSeason> seasons = new ArrayList<>();

}

Java:
@XmlType(name = "season")
@Getter
@ToString
public class XMLSeason {

    @XmlElement
    private String id;
}
 
Ich hab auch so ein Jaxb-Projekt. Ich hab mal schnell nachgeschaut: das @XmlRootElement hat ebenfalls eine @XmlType-Annotation. Das ist mir nur so aufgefallen...
Ich lasse mir so etwas immer generieren und passe es dann noch etwas an. Zu Fuß ist mir das zu viel Aufwand. Deshalb kann ich das auch nicht weiter begründen.
 
Was ich immer gerne mache: Ich serialisiere etwas um dann zu vergleichen, wie nah ich schon dran bin. Mit den aktuellen Annotations: Was wird da an XML erzeugt? Dann sieht man den Unterschied und kann da dann eingreifen.

Und was man auch noch machen kann bei JAXB: Einen Listener hinzunehmen. Also eine Klasse wie:
Java:
import jakarta.xml.bind.ValidationEvent;
import jakarta.xml.bind.ValidationEventHandler;

class MyValidationEventHandler implements ValidationEventHandler {
    @Override
    public boolean handleEvent(ValidationEvent event) {
        System.out.println("\nEVENT");
        System.out.println("SEVERITY: " + event.getSeverity());
        System.out.println("MESSAGE: " + event.getMessage());
        System.out.println("LINKED EXCEPTION: " + event.getLinkedException());
        System.out.println("LOCATOR");
        System.out.println("    LINE NUMBER: " + event.getLocator().getLineNumber());
        System.out.println("    COLUMN NUMBER: " + event.getLocator().getColumnNumber());
        System.out.println("    OFFSET: " + event.getLocator().getOffset());
        System.out.println("    OBJECT: " + event.getLocator().getObject());
        System.out.println("    NODE: " + event.getLocator().getNode());
        System.out.println("    URL: " + event.getLocator().getURL());

        return true;
    }
}
die ich dann z.B. per
unmarshaller.setEventHandler(new MyValidationEventHandler());
einbinden kann. Die Meldung ist auch mit dem Namespace immer noch da - nur das Level hat sich wohl geändert so dass es nicht mehr angezeigt wurde.

...mit xjc aus dem JDK
Hier ist dann ggf. wichtig: Mit Java 11 ist das wohl aus dem JDK geflogen. Also ein früheres JDK verwenden oder xjc separat installieren.
Ubuntu / debian hat wohl ein Paket jaxb
Ansonsten einfach als jar laden von https://mvnrepository.com/artifact/org.glassfish.jaxb/jaxb-xjc/4.0.5
(Runnable jar, aber braucht auch noch Abhängigkeiten!)

Das wäre so noch mein Input. Habe da eben auch mal auf die Schnelle etwas gespielt, aber ich hatte da auch meine Probleme, es zu lösen.
 
...mit xjc aus dem JDK
Also wenn ich das XML mir generien lassen will : xjc -d src seasons.xml bekomme ich folgenden Fehler :
Java:
[ERROR] s4s-elt-character: Non-whitespace characters are not allowed in schema elements other than 'xs:appinfo' and 'xs:documentation'. Saw '766158250'.

DAs ist der Inhalt der Datei :
Code:
<?xml version="1.0" encoding="UTF-8"?>
<seasons xmlns="http://sams-server.de/api/xml/ns/seasons" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://sams-server.de/api/xml/ns/seasons seasons.xsd">
<season>
<id>766158250</id>
<name>2023/24</name>
<begin>2023-07-01</begin>
<end>2024-06-30</end>
</season>           
</seasons>
 
Ok, ich hatte jetzt auch einmal etwas Zeit, damit herum zu spielen.

Kommen wir erst einmal zu dem Problem:
Das Problem selbst ist, dass Du hier eine Verschachtelung hast: Du hast einmal XMLSeasons - das liefert dann auch das Root Element des XML. Ich nenne es einmal outer.

In dem hast Du jetzt eine List. Dies wird durch zwei Tags gekennzeichnet. Einmal die äußere, dessen Namen du mit XmlElementWrapper festlegen kannst und zum anderen das Tag mit dem jedes Listenelement umschlossen wird. Das könnte man mit XmlElement festlegen.

Die Struktur, die da also mehr oder weniger entsteht ist etwas wie:
XML:
<ns2:seasons xmlns:ns2="http://sams-server.de/api/xml/ns/seasons">
    <seasons>
        <season>
            <id>0</id>
            <name>Test</name>
        </season>
    </seasons>
</ns2:seasons>

Du willst also nicht ein Objekt serialisieren sondern nur eine List<XMLSchema>.

Ich habe jetzt aber keine Möglichkeit gefunden, das mit Annotations anzugeben. Daher habe ich die Klasse erst einmal geändert, dass XMLSchemas von ArryList<XMLSchema> erbt. Das hatte aber leider erst einmal nicht den gewünschten Erfolg und da hat mir dann eine KI ins Ohr geflüstert:

Java:
@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "seasons") //, namespace = "http://sams-server.de/api/xml/ns/seasons")
public class XMLSeasons extends ArrayList<XMLSeason> {
    @XmlElement(name = "season") // Definiert das Element für jedes Element in der Liste
    public ArrayList<XMLSeason> getSeasons() {
        return this;
    }
}

Das hat dann ohne die Namespaces auch funktioniert (Bei meinem Test hatte ich die Angaben beim root Element einfach erst einmal heraus genommen um da keine unnötigen Probleme zu bekommen ... es ging mir erst einmal nur um die Verschachtelung).

Funktioniert bedeutet: Marshaling lieferte das gewünschte XML und unmarshaling ging auch...

Mit dem Namespace nur in XMLSchemas war das Problem, dass er nur das äußere Element in den Namespace gepackt hat und nicht das innere. Das habe ich dann mal auf package Ebene gesetzt. Damit sieht das marshaling ok aus:
XML:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<seasons xmlns="http://sams-server.de/api/xml/ns/seasons">
    <season>
        <id>0</id>
        <name>Test</name>
    </season>
</seasons>

Und siehe da: Unmarshaling lief jetzt auch durch.

Also einfach einmal, wie mein Code jetzt aussieht, mit dem es funktionierte:

package-info.java​

Java:
@XmlSchema(namespace = "http://sams-server.de/api/xml/ns/seasons", elementFormDefault = XmlNsForm.QUALIFIED)
package de.kneitzel;

import jakarta.xml.bind.annotation.XmlNsForm;
import jakarta.xml.bind.annotation.XmlSchema;

XMLSeasons​

Java:
package de.kneitzel;

import jakarta.xml.bind.annotation.*;
import java.util.ArrayList;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "seasons")
public class XMLSeasons extends ArrayList<XMLSeason> {
    @XmlElement(name = "season") // Definiert das Element für jedes Element in der Liste
    public ArrayList<XMLSeason> getSeasons() {
        return this;
    }
}
Hier ist halt wichtig: Das dient nur als eine Art Wrapper für eben die List. Daher dieses unschöne extends ArrayList. Laut KIs hätte es auch anders gehen sollen, aber das marshaling als Gegentest hat bei mir immer klar gezeigt, dass es Quatsch ist!

Auch wichtig: Kein namespace hier - ist ja jetzt in der package-info.java. (Kann man aber auch hinzu packen. Es würde nicht stören)

XMLSeason​

Dürfte egal sein - das ist jetzt einfach etwas, das ich hier genommen hatte:
Java:
package de.kneitzel;

import jakarta.xml.bind.annotation.*;



@XmlType(name = "season")
public class XMLSeason {
    private long id;
    private String name;
    private String begin;
    private String end;

    // Standard-Konstruktor
    public XMLSeason() {
    }

    // Getter und Setter für id
    @XmlElement(name = "id")
    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    // Getter und Setter für name
    @XmlElement(name = "name")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    // Getter und Setter für begin
    @XmlElement(name = "begin")
    public String getBegin() {
        return begin;
    }

    public void setBegin(String begin) {
        this.begin = begin;
    }

    // Getter und Setter für end
    @XmlElement(name = "end")
    public String getEnd() {
        return end;
    }

    public void setEnd(String end) {
        this.end = end;
    }

    // Optional: Überschreiben Sie die toString-Methode für besseres Debugging
    @Override
    public String toString() {
        return "XMLSeason{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", begin='" + begin + '\'' +
                ", end='" + end + '\'' +
                '}';
    }
}

Tests​

Evtl. noch meine Tests - keine wirklichen Unit Tests da ich keine Asserts oder so habe.
Java:
package de.kneitzel;

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;
import org.junit.jupiter.api.Test;

import java.io.StringReader;
import java.io.StringWriter;

public class UnmarshalTest {

    @Test
    void testUnmarshaling() {
        try {
            String xmlContent = "<seasons xmlns=\"http://sams-server.de/api/xml/ns/seasons\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://sams-server.de/api/xml/ns/seasons seasons.xsd\">\n" +
                    "    <season>\n" +
                    "        <id>1</id>\n" +
                    "        <name>2023/24</name>\n" +
                    "        <begin>2023-07-01</begin>\n" +
                    "        <end>2024-06-30</end>\n" +
                    "    </season>\n" +
                    "    <season>\n" +
                    "        <id>2</id>\n" +
                    "        <name>2024/25</name>\n" +
                    "        <begin>2024-07-01</begin>\n" +
                    "        <end>2025-06-30</end>\n" +
                    "    </season>\n" +
                    "</seasons>";

            JAXBContext context = JAXBContext.newInstance(XMLSeasons.class);
            Unmarshaller unmarshaller = context.createUnmarshaller();
            unmarshaller.setEventHandler(new MyValidationEventHandler());
            StringReader reader = new StringReader(xmlContent);
            XMLSeasons xmlSeasons = (XMLSeasons) unmarshaller.unmarshal(reader);

            // Verwenden Sie die deserialisierten Objekte nach Bedarf
            System.out.println("Deserialisiertes Objekt: " + xmlSeasons);
        } catch (JAXBException e) {
            e.printStackTrace();
        }
    }

    @Test
    void testMarshaling() {
        try {
            XMLSeasons seasons = new XMLSeasons();
            XMLSeason season = new XMLSeason();
            season.setName("Test");
            seasons.add(season);
            JAXBContext context = JAXBContext.newInstance(XMLSeasons.class);
            Marshaller marshaller = context.createMarshaller();

            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); // Schön formatiert

            StringWriter writer = new StringWriter();
            marshaller.marshal(seasons, writer);
            System.out.println(writer.toString());
        } catch (JAXBException e) {
            e.printStackTrace();
        }
    }

}


xjc Versuche​

Ich habe einfach per IntelliJ aus der XML eine xsd bauen lassen, diese manuell angepasst und hatte da:
XML:
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="seasons" type="seasonsType"/>
  <xs:complexType name="seasonType">
    <xs:sequence>
      <xs:element name="id" type="xs:string"/>
      <xs:element name="name" type="xs:string"/>
      <xs:element name="begin" type="xs:string"/>
      <xs:element name="end" type="xs:string"/>
    </xs:sequence>
  </xs:complexType>
  <xs:complexType name="seasonsType">
    <xs:sequence>
      <xs:element type="seasonType" name="season" maxOccurs="unbounded" minOccurs="0"/>
    </xs:sequence>
  </xs:complexType>
</xs:schema>

Dann daraus per xjc Code erzeugen lassen ==> Da kam das heraus, was wir schon hatten, was auch die KIs mir geben wollten, was halt immer ein zusätzliches seasons haben wollte ...

Es mag evtl. auch andere Lösungen geben und meine Lösung mag unschön sein ... Ich kann es nicht bewerten. Es war einfach ein Murks. Ich vermisse z.B. TypeToken von gson. War das damals mit XML immer so schlimm? Oder hatten wir einfach immer "besseres" XML, wo so extra Ebenen halt immer da waren?
 
Also wenn ich das XML mir generien lassen will : xjc -d src seasons.xml bekomme ich folgenden Fehler :
Dein Problem ist, dass Du ein XML File angibst er aber bei dem Aufruf ein XSD erwartet. Das, was Du als input geben kannst ist:
Code:
\\ -xmlschema         :  Behandelt Eingabe als W3C-XML-Schema (Standard)
\\ -relaxng           :  Behandelt Eingabe als RELAX NG (experimentell, nicht unterstützt)
\\ -relaxng-compact   :  Behandelt Eingabe als RELAX NG-Kompaktsyntax (experimentell, nicht unterstützt)
\\ -dtd               :  Behandelt Eingabe als XML DTD (experimentell, nicht unterstützt)
\\ -wsdl              :  Behandelt Eingabe als WSDL und kompiliert enthaltene Schemas (experimentell, nicht unterstützt)
(Aus meinem xjc Aufruf die Hilfe - xml geht also nicht)
 
Er hat doch in #1 ein Schema angegeben. Dann muss man doch keins generieren
Es ist ein Schema angegeben, aber da (http://sams-server.de/api/xml/ns/seasons seasons.xsd) konnte ich nichts herunter laden. Und ansonsten habe ich kein Schema gesehen.

Aber evtl. habe ich es ja übersehen. Wo ist das Schema? Evtl. habe ich ja bei meinen Ansätzen etwas falsch gemacht so dass xjc etwas unbrauchbares erzeugt hat.
 
Danke wäre an dieser Stelle einfach zu wenig. Wahnsinn. Es funktioniert !!! Wirklich ein großes großes Dankeschön.

Ich habe an meinen Klassen noch etwas herumgespielt. Bei mir klappt es jetzt auch ohne Ableitung von ArrayList :
Java:
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlRootElement(name = "seasons")
    private static class SamsSeasonList {

        @Getter
        @Setter
        @XmlElement(name = "season")
        private List<SamsSeason> samsSeasonElements;
    }

Aber ich bin noch auf ein anderes Problem gestoßen :
Im Match XML siehe unten gibt es 2x das Team Attribut. Wie kann ich das in meiner Klasse unterbringen :
Hier das XML :

XML:
 <matches xmlns="http://sams-server.de/api/xml/ns/matches" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://sams-server.de/api/xml/ns/matches matches.xsd">
                    <match>
                        <id>id-match-1</id>
                        <uuid>uuid-match-1</uuid>
                        <number>123</number>
                        <date>01.01.2000</date>
                        <time>19:30</time>
                        <delayPossible>false</delayPossible>
                        <decidingMatch>false</decidingMatch>
                        <indefinitelyRescheduled>false</indefinitelyRescheduled>
                        <gameReassessed>false</gameReassessed>
                        <host>
                            <id>id-host</id>
                            <uuid>uuid-host</uuid>
                            <name>TEAM 1</name>
                            <club>CLUB 1</club>
                        </host>
                        <team>
                            <number>1</number>
                            <id>id-team-1</id>
                            <uuid>uuid-team-1</uuid>
                            <seasonTeamId>id-season-1</seasonTeamId>
                            <name>TEAM 1</name>
                            <shortName>TEAM 1</shortName>
                            <clubCode>CLUB 1</clubCode>
                            <club>
                                <name>CLUB 1</name>
                                <shortName>CLUB1</shortName>
                            </club>
                        </team>
                        <team>
                            <number>2</number>
                            <id>id-team-2</id>
                            <uuid>uuid-team-2</uuid>
                            <seasonTeamId>id-season-2</seasonTeamId>
                            <name>TEAM 2</name>
                            <shortName>TEAM 2</shortName>
                            <clubCode>CLUB 2</clubCode>
                            <club>
                                <name>CLUB 2</name>
                                <shortName>CLUB2</shortName>
                            </club>
                        </team>
                        ...

Hier mein Versuch :
Java:
@XmlType(name = "match")
@NoArgsConstructor
@ToString
public class SamsMatch implements XMLMatch {

    @Getter
    @XmlElement(name = "id")
    private String id;

    @Getter
    @XmlElement(name = "date")
    private String date;

    @Getter
    @XmlElement(name = "time")
    private String time;

    @Getter
    @XmlElement(name = "team")
    private SamsTeam firstTeam;

    @Getter
    @XmlElement(name = "team")
    private SamsTeam secondTeam;

Code:
@XmlType(name = "team")
@NoArgsConstructor
@ToString
public class SamsTeam implements XMLObject {

    @Getter
    @XmlElement(name = "id")
    private String id;

    @Getter
    @XmlElement(name = "number")
    private String number = "1";

}

Bei mir wird immer das nur das erste team Objekt erzeugt. Gibt es da einen Trick
 
Ich habe auch mal eine KI befragt und meine Lösung sieht nun so aus :
Code:
@XmlType(name = "match")
@NoArgsConstructor
@ToString
public class SamsMatch implements XMLMatch {

    @Getter
    @XmlElement(name = "id")
    private String id;

    @Getter
    @XmlElement(name = "date")
    private String date;

    @Getter
    @XmlElement(name = "time")
    private String time;

    @Getter
    @XmlElement(name = "team")
    private List<SamsTeam> teams;
}

Damit klappt es
 
Ja genau. Bei mehreren team Elementen hast Du dann im Code eine List.

Aber ich bin jetzt gerade am überlegen, wieso es bei Dir mit
Java:
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlRootElement(name = "seasons")
    private static class SamsSeasonList {

        @Getter
        @Setter
        @XmlElement(name = "season")
        private List<SamsSeason> samsSeasonElements;
    }
funktioniert. Das war doch eigentlich, was wir schon hatten und probiert hatten, oder?

Irgendwie bin ich gerade etwas verwirrt - denn da muss ich bei meinen Tests auch irgendwas falsch gemacht haben. Aber das erklärt, warum xjc und auch die KIs sowas in der Art haben wollten. Nur unsere Probleme erklärt es gerade leider nicht wirklich 🙂

Aber super, dass es funktioniert. Da können wir uns beruhigt anderen, schöneren Themen widmen.
 
Ja genau. Bei mehreren team Elementen hast Du dann im Code eine List.

Aber ich bin jetzt gerade am überlegen, wieso es bei Dir mit
Java:
    @XmlAccessorType(XmlAccessType.FIELD)
    @XmlRootElement(name = "seasons")
    private static class SamsSeasonList {

        @Getter
        @Setter
        @XmlElement(name = "season")
        private List<SamsSeason> samsSeasonElements;
    }
funktioniert. Das war doch eigentlich, was wir schon hatten und probiert hatten, oder?

Irgendwie bin ich gerade etwas verwirrt - denn da muss ich bei meinen Tests auch irgendwas falsch gemacht haben. Aber das erklärt, warum xjc und auch die KIs sowas in der Art haben wollten. Nur unsere Probleme erklärt es gerade leider nicht wirklich 🙂

Aber super, dass es funktioniert. Da können wir uns beruhigt anderen, schöneren Themen widmen.
Der großes UNterschied war die package.json
 

Zurück
Oben