Dynamisches Wurzelelement JAXB?

8

Ich versuche, mich in ein System eines Drittanbieters zu integrieren. Je nach Objekttyp ändert sich das Stammelement des zurückgegebenen XML-Dokuments. Ich verwende die JAXB-Bibliothek für Marshalling / Unmarshalling.

Root1:

<?xml version="1.0" encoding="UTF-8"?>
<root1 id='1'>
   <MOBILE>9831138683</MOBILE>
   <A>1</A>
   <B>2</B>
</root1>

Root2:

<?xml version="1.0" encoding="UTF-8"?>
<root2 id='3'>
   <MOBILE>9831138683</MOBILE>
   <specific-attr1>1</specific-attr1>
   <specific-attr2>2</specific-attr2>
</root2>

Ich verwende all die verschiedenen XML-Dateien, die sie einem generischen Objekt zuordnen :

 @XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "ROW")
public class Row {

    @XmlAttribute
    private int id;
    @XmlElement(name = "MOBILE")
    private int mobileNo;

    @XmlMixed
    @XmlAnyElement
    @XmlJavaTypeAdapter(MyMapAdapter.class)
    private Map<String, String> otherElements;
}

Und der Adapter zum Verwandeln der unbekannten Werte in eine Karte :

import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.parsers.DocumentBuilderFactory;
import java.util.HashMap;
import java.util.Map;

public class MyMapAdapter extends XmlAdapter<Element, Map<String, String>> {

    private Map<String, String> hashMap = new HashMap<>();

    @Override
    public Element marshal(Map<String, String> map) throws Exception {
        // expensive, but keeps the example simpler
        Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();

        Element root = document.createElement("dynamic-elements");

        for(Map.Entry<String, String> entry : map.entrySet()) {
            Element element = document.createElement(entry.getKey());
            element.setTextContent(entry.getValue());
            root.appendChild(element);

        }

        return root;
    }


    @Override
    public Map<String, String> unmarshal(Element element) {
        String tagName = element.getTagName();
        String elementValue = element.getChildNodes().item(0).getNodeValue();
        hashMap.put(tagName, elementValue);

        return hashMap;
    }
}

Dadurch werden ID und Handynummer in die Felder und der Rest, das Unbekannte, in eine Karte eingefügt .

Dies funktioniert, wenn das Stammelement wie im obigen Beispiel auf ROW festgelegt ist.

Wie kann dies so funktionieren, dass das Stammelement in jedem XML unterschiedlich ist? Ein Weg, um beim Unmarshalling vielleicht nur agnostisch gegenüber Root-Elementen zu sein?

Siddharth Trikha
quelle
1
Ich denke, ist generisch über jeden Gebrauch hinaus. Hier gibt es keinen Vertrag. Sie können alles zurückgeben, was Sie wollen. Sie lassen Ihre Benutzer raten. Ich würde entweder eine bessere API erstellen, die explizite Methoden für jeden zurückgegebenen Typ enthält, oder diese Anforderung löschen.
Duffymo
Weißt du zumindest, was alle möglichen Wurzelelemente sind?
Harshal Khachane
@HarshalKhachane Ja, ich kenne das Set!
Siddharth Trikha
Wenn die Menge nicht zu lang ist, können Sie einfach die Vererbung verwenden und alle XMLElements in der übergeordneten Klasse verschieben und untergeordnete Klassen für jede Wurzel erstellen.
Harshal Khachane

Antworten:

3

Vergessen Sie dafür JAXB und analysieren Sie es selbst mit StAX.

Im folgenden Code habe ich das Feld mobileNovon intin geändert String, da der Wert 9831138683für ein zu groß ist int.

private static Row parse(String xml) throws XMLStreamException {
    XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory();
    XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(new StringReader(xml));
    reader.nextTag(); // read root element
    Row row = new Row(Integer.parseInt(reader.getAttributeValue(null, "id")));
    while (reader.nextTag() == XMLStreamConstants.START_ELEMENT) {
        String tagName = reader.getLocalName();
        if (tagName.equals("MOBILE")) {
            row.setMobileNo(reader.getElementText());
        } else {
            row.addOtherElement(tagName, reader.getElementText());
        }
    }
    return row;
}
public class Row {
    private int id;
    private String mobileNo;
    private Map<String, String> otherElements = new LinkedHashMap<>();

    public Row(int id) {
        this.id = id;
    }
    public void setMobileNo(String mobileNo) {
        this.mobileNo = mobileNo;
    }
    public void addOtherElement(String name, String value) {
        this.otherElements.put(name, value);
    }

    // getters here

    @Override
    public String toString() {
        return "Row[id=" + this.id + ", mobileNo=" + this.mobileNo +
                 ", otherElements=" + this.otherElements + "]";
    }
}

Prüfung

public static void main(String[] args) throws Exception {
    test("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
         "<root1 id='1'>\n" +
         "   <MOBILE>9831138683</MOBILE>\n" +
         "   <A>1</A>\n" +
         "   <B>2</B>\n" +
         "</root1>");
    test("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
         "<root2 id='3'>\n" +
         "   <MOBILE>9831138683</MOBILE>\n" +
         "   <specific-attr1>1</specific-attr1>\n" +
         "   <specific-attr2>2</specific-attr2>\n" +
         "</root2>");
}
private static void test(String xml) throws XMLStreamException {
    System.out.println(parse(xml));
}

Ausgabe

Row[id=1, mobileNo=9831138683, otherElements={A=1, B=2}]
Row[id=3, mobileNo=9831138683, otherElements={specific-attr1=1, specific-attr2=2}]
Andreas
quelle
Ich bin mir nicht sicher, ob xmles besser wäre, auf einer niedrigeren Abstraktionsebene (über StAX) für diese Art von vs Unmarhshalling über JAXB (für bekannte Datenobjekte, in denen die XSD definiert ist) zu arbeiten. Selbst wenn ich StAX verwende, muss ich das RowObjekt mit dem passenden Stammelement wieder in die richtige XML- Datei serialisieren .
Siddharth Trikha
Außerdem muss ich mich sowohl um XML- als auch um JSON-Eingaben kümmern.
Siddharth Trikha
1
@SiddharthTrikha Wenn Sie den Namen des Stammelements bei der Serialisierung in XML beibehalten müssen, muss sich die RowKlasse den Namen merken. --- Wenn Sie sich um die JSON-Eingabe kümmern müssen, verwenden Sie einen JSON-Parser.
Andreas
1
@Andreas ausgezeichneter Punkt beim Erstellen mobileNoeines Strings . Selbst wenn intes groß genug war, um eine Telefonnummer zu speichern, repräsentiert der Wert keinen Betrag. IMO, dies garantiert, dass es als Zeichenfolge behandelt wird, auch wenn der Wert numerisch ist.
Hfontanez
1

Ich glaube nicht, dass es einen Weg gibt, das zu tun, was Sie verlangen. In XML muss der Stammknoten (das Dokument) ein definiertes Element (oder eine definierte Klasse) haben. Mit anderen Worten xs:any, funktioniert nur für Unterelemente. Selbst wenn es einen Weg gab, dies zu erreichen, ist dies meiner Meinung nach eine schlechte Entscheidung. Anstatt ein variables ("dynamisches") Stammelement zu erstellen, sollten Sie demselben Element ein Namensattribut hinzufügen, um die XML-Dateien zu unterscheiden. Zum Beispiel:

<?xml version="1.0" encoding="UTF-8"?>
<ROW id='1' name="me">
   <MOBILE>9831138683</MOBILE>
   <specific-attr1>1</specific-attr1>
   <specific-attr2>2</specific-attr2>
</ROW>


<?xml version="1.0" encoding="UTF-8"?>
<ROW id='2' name="you">
   <MOBILE>123456790</MOBILE>
   <specific-attr1>3</specific-attr1>
   <specific-attr2>4</specific-attr2>
</ROW>

Dazu müssen Sie Ihrem vorhandenen Element lediglich ein Namensattribut hinzufügen:

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "ROW")
public class Row {

    @XmlAttribute
    private int id;

    @XmlAttribute(name = "name", required=true)
    private String name;

    @XmlElement(name = "MOBILE")
    private int mobileNo;

    @XmlMixed
    @XmlAnyElement
    @XmlJavaTypeAdapter(MyMapAdapter.class)
    private Map<String, String> otherElements;
}
hfontanez
quelle
Ich verwende XML von einem Client, kann also die XML-Struktur nicht ändern. Ich habe versucht , das Entfernen XmlRootElementvon Rowund unmarshalling zu JAXBElementdurch: JAXBElement<Row> element = unmarshaller.unmarshal(new StreamSource(xml), Row.class); Row root = element.getValue();. Dies ermöglicht das Auffüllen aller Felder und das Konsumieren von XMLs mit unterschiedlichen Stammelementen.
Siddharth Trikha
@SiddharthTrikha In diesem Fall benötigen Sie mehrere Basisklassen, um Ihr Stammelement darzustellen. Dies ist ein Chaos und lässt sich höchstwahrscheinlich nicht gut skalieren. Ihr Wurzelknoten kann nicht sein xs:any. Ich habe dies auch mit dem XML-Schema versucht. Wenn Sie Ihr Stammelement so einstellen, dass es im Grunde genommen alles ist, bricht Ihr Schema. Mit anderen Worten, xs:anykann nur im Kontext eines untergeordneten Knotens verwendet werden. Zuletzt müssen Sie zu Ihrem Kunden gehen und ihm zeigen, warum dies eine schlechte Implementierung ist.
Hfontanez
@SiddharthTrikha Ich verstehe, dass das Entfernen XmlRootElementvon Rowbeim Aufmarschieren funktioniert. Meine Frage an Sie ist, wie würden Sie es aufstellen, wenn Sie müssen? Sie können dies mit anderen Mechanismen tun, jedoch nicht mit JAXB. Der Nachteil dieses Ansatzes besteht darin, dass Sie dann Ihre eigenen Validatoren schreiben müssen. Machbar, aber Sie verlieren die nativen Fähigkeiten von JAXB, um dies zu tun.
Hfontanez