JAXB - Unmarshalling con generics e nodi XML con lo stesso nome

di il
2 risposte

JAXB - Unmarshalling con generics e nodi XML con lo stesso nome

Ciao a tutti,
sto interrogando diversi endpoint di un'API, ognuno dei quali restituisce un XML diverso.
Gli XML sono molto simili tra di loro ad eccezione di 2 elementi. Di seguito un esempio semplificato (nella realtà sono molto più complessi, ma gli esempi si focalizzano sul problema che sto riscontrando):

User.xml

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <header>
        <userAPI info="User info"/>
    </header>
    <userAPI>
        <username>charles</username>
    </userAPI>
</root>

Order.xml

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <header>
        <orderAPI info="Order info"/>
    </header>
    <orderAPI>
        <orderId>1</orderId>
    </orderAPI>
</root>

Visto la similitudine tra i vari XML, il mio obiettivo è quello di evitare del codice ridondante per l'unmarshalling di questi file.
Sono riuscito (in parte) ad ottenere quanto desidero con l'utilizzo di classi generiche, ma sto riscontrando dei problemi legati al fatto che ogni file ha due elementi con lo stesso nome (un figlio del nodo header, un figlio del nodo root):

  • userAPI per il file User.xml
  • orderAPI per il file Order.xml

Di seguito sono riportate le classi implementate (utilizzando Lombok) per eseguire l'unmarshalling del file User.xml

pom.xml

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
    <scope>provided</scope>
</dependency>

UserApiHeader.java

@Data
@XmlRootElement(name = "userAPI")
@XmlAccessorType(XmlAccessType.FIELD)
public class UserApiHeader {
    @XmlAttribute
    private String info;
}

UserApi.java

@Data
@XmlRootElement(name = "userAPI")
@XmlAccessorType(XmlAccessType.FIELD)
public class UserApi {
    @XmlElement
    private String username;
}

Header.java

@Data
@XmlRootElement(name = "header")
@XmlAccessorType(XmlAccessType.FIELD)
public class Header<T> {
    @XmlAnyElement(lax = true)
    private T headerDetail;
}

Root.java

@Data
@XmlRootElement(name = "root")
@XmlAccessorType(XmlAccessType.FIELD)
public class Root<T, E> {
    @XmlElement
    private Header<T> header;
    @XmlAnyElement(lax = true)
    private E body;
}

Main.java

public class Main {
    public static void main(String[] args) {
        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"
                + "<root>\r\n"
                + "    <header>\r\n"
                + "        <userAPI info=\"User info\"/>\r\n"
                + "    </header>\r\n"
                + "    <userAPI>\r\n"
                + "        <username>charles</username>\r\n"
                + "    </userAPI>\r\n"
                + "</root>";
        try {
            JAXBContext jaxbContext = JAXBContext.newInstance(Root.class, UserApi.class, UserApiHeader.class);
            Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
            @SuppressWarnings("unchecked")
            Root<UserApiHeader, UserApi> root = (Root<UserApiHeader, UserApi>) unmarshaller.unmarshal(new StringReader(xml));
            System.out.println("root.header.userAPI: " + root.getHeader().getHeaderDetail());
            System.out.println("root.userAPI:        " + root.getBody());
        } catch (JAXBException e) {
            e.printStackTrace();
        }
    }
}

Questo è risultato stampato in console

root.header.userAPI: UserApiHeader(info=User info)
root.userAPI:        UserApiHeader(info=null)

Dal risultato della console, si evince che l'unmarshaller associa la classe UserApiHeader ad entrambi i nodi userAPI; riuscendo a deserializzare il nodo root.header.userAPI ma non il nodo root.userAPI.

Provando ad invertire le classi UserApi e UserApiHeader nell'istanziare il JAXBContext, il problema si inverte:

public class Main {
    public static void main(String[] args) {
        // ...
            JAXBContext jaxbContext = JAXBContext.newInstance(Root.class, UserApiHeader.class, UserApi.class);
        // ...
    }
}
root.header.userAPI: UserApi(username=null)
root.userAPI:        UserApi(username=charles)

Come dicevo, il problema si inverte:  l'unmarshaller associa la classe UserApi ad entrambi i nodi userAPI, riuscendo a deserializzare il nodo root.userAPI ma non il nodo root.header.userAPI.

Il problema descritto non viene riscontrato modificando il nome di uno dei due nodi userAPI (cosa che nella realtà non posso fare in quanto ricevo i file così come sono). Esempio:

User.xml (solo parti modificate)

...
    <header>
        <userAPI_ info="User info"/>
    </header>
...

UserApiHeader.java (solo parti modificate)

// ...
@XmlRootElement(name = "userAPI_")
// ...

rieseguendo il main, ottengo questo (indipendentemente dall'ordine delle classi nell'instanziare il JAXBContext):

root.header.userAPI: UserApiHeader(info=User info)
root.userAPI:        UserApi(username=charles)

Qualcuno riesce ad aiutarmi?
Grazie mille in anticipo.

2 Risposte

  • Re: JAXB - Unmarshalling con generics e nodi XML con lo stesso nome

    Il problema lo risolvi innanzitutto se accetti di avere JAXBContext separati (uno per user e l'altro per order).

    Queste classi modellano lo user.xml

    @Data
    @XmlRootElement(name = "root")
    @XmlAccessorType(XmlAccessType.FIELD)
    public class User {
        @XmlElement(name = "header")
        private UserHeader userHeader;
        @XmlElement(name = "userAPI")
        private UserApi userApi;
    }
    
    @Data
    @XmlAccessorType(XmlAccessType.FIELD)
    public class UserHeader {
        @XmlElement(name = "userAPI")
        private Info userApi;
    }
    
    @Data
    @XmlAccessorType(XmlAccessType.FIELD)
    public class UserApi {
        @XmlElement
        private String username;
    }
    
    @Data
    @XmlAccessorType(XmlAccessType.FIELD)
    public class Info {
        @XmlAttribute
        private String info;
    }

    E nota che Info è riusabile anche per gli order (se non ci sono altre complicazioni non deducibili dal post)

    JAXBContext userContext = JAXBContext.newInstance(User.class);

    Basta la classe “root”, non serve elencarle tutte nel newInstance!

  • Re: JAXB - Unmarshalling con generics e nodi XML con lo stesso nome

    15/06/2024 - andbin ha scritto:


    Il problema lo risolvi innanzitutto se accetti di avere JAXBContext separati (uno per user e l'altro per order).

    I due xml che ho postato sono un esempio, in realtà interrogo diversi metodi dell'API, ognuno dei quali restituisce un XML diverso ma molto simili tra loro.  Il mio intento era quello di evitarmi n classi identiche.

    Con la soluzione che hai proposto (che è quella che utilizzo attualmente) servono 4 classi per ogni xml, 2 di queste (root e header) sono praticamente uguali.

    La mia speranza è che mi manca qualche pezzo e che ci sia una soluzione con i generics…

Devi accedere o registrarti per scrivere nel forum
2 risposte