Elemento raíz dinámico JAXB?

8

Estoy tratando de integrarme con un sistema de terceros y, según el tipo de objeto, cambia el elemento raíz del documento XML devuelto. Estoy usando la biblioteca JAXB para 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>

Estoy consumiendo todos los diferentes XML que los asignan a un objeto genérico :

 @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;
}

Y el adaptador para convertir los valores desconocidos en un mapa :

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;
    }
}

Esto pondrá la identificación y el número de móvil en los campos , y el resto, lo desconocido en un mapa .

Esto funciona si el elemento raíz está fijado a ROW como en el ejemplo anterior.

¿Cómo hacer que esto funcione de manera que el elemento raíz sea diferente en cada XML? ¿Una forma de ser agnóstico al elemento raíz mientras se desarma?

Siddharth Trikha
fuente
1
Creo que es genérico más allá de todo uso. No hay contrato aquí. Puedes devolver lo que quieras. Hace que sus usuarios adivinen. O haría una mejor API que tuviera métodos explícitos para cada tipo devuelto o descartaría este requisito.
duffymo
Al menos, ¿sabes cuáles son todos los elementos raíz posibles?
Harshal Khachane
@HarshalKhachane Sí, sé el conjunto!
Siddharth Trikha
Si el conjunto no es demasiado largo, simplemente puede usar la herencia y mover todos los elementos XMLE en la clase principal y crear clases secundarias para cada raíz.
Harshal Khachane

Respuestas:

3

Olvídate de JAXB para esto y analízalo tú mismo con StAX.

En el siguiente código, he cambiado el campo mobileNode inta String, ya que el valor 9831138683es demasiado grande para un 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 + "]";
    }
}

Prueba

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));
}

Salida

Row[id=1, mobileNo=9831138683, otherElements={A=1, B=2}]
Row[id=3, mobileNo=9831138683, otherElements={specific-attr1=1, specific-attr2=2}]
Andreas
fuente
No estoy seguro de si trabajar en un nivel inferior de abstracción (a través de StAX) para este tipo de xmlvs unmarhshalling a través de JAXB (para objetos de datos conocidos donde se define el XSD), lo que sería mejor. Incluso si uso StAX, necesito volver a serializar el Rowobjeto en xml adecuado con el elemento raíz correspondiente.
Siddharth Trikha
Además, tengo que ocuparme de las entradas XML y JSON.
Siddharth Trikha
1
@SiddharthTrikha Si necesita conservar el nombre del elemento raíz al volver a serializar en XML, debe hacer que la Rowclase recuerde el nombre. --- Si necesita cuidar la entrada JSON, use un analizador JSON.
Andreas
1
@Andreas excelente punto para hacer mobileNouna cadena. Incluso si intfuera lo suficientemente grande como para almacenar un número de teléfono, el valor no representa una cantidad. En mi opinión, esto garantiza que se trate como una cadena incluso si el valor es numérico.
hfontanez
1

No creo que haya una manera de hacer lo que pides. En XML, el nodo raíz (el documento) debe tener un elemento (o clase) definido. En otras palabras, xs:anysolo funciona para subelementos. Incluso si hubiera una manera de lograr esto, en mi humilde opinión, esta es una mala decisión. En lugar de crear un elemento raíz variable ("dinámico"), debe agregar un atributo de nombre al mismo elemento para distinguir los archivos XML. Por ejemplo:

<?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>

Para esto, todo lo que necesita hacer es agregar un atributo de nombre a su elemento existente:

@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
fuente
Estoy consumiendo XML de un cliente, por lo que no puedo cambiar la estructura XML. Probé la eliminación XmlRootElementde Rowy unmarshalling a JAXBElementtravés de: JAXBElement<Row> element = unmarshaller.unmarshal(new StreamSource(xml), Row.class); Row root = element.getValue();. Esto proporciona el llenado de todos los campos y el consumo de XML con diferentes elementos raíz.
Siddharth Trikha
@SiddharthTrikha en ese caso, necesita múltiples clases base para representar su elemento raíz, lo que será un desastre y lo más probable es que no se amplíe bien. Su nodo raíz no puede ser xs:any. Intenté esto con un esquema XML también. Si configura su elemento raíz para que sea básicamente cualquier cosa, su esquema se rompe. En otras palabras, xs:anysolo se puede usar en el contexto de un nodo hijo. Por último, debe ir a su cliente y mostrarle por qué esta es una mala implementación.
hfontanez
@SiddharthTrikha Entiendo que eliminar XmlRootElementde Rowfuncionará cuando desarme. Mi pregunta para usted es, ¿cómo lo ordenaría si fuera necesario? Puede hacerlo con otros mecanismos, pero no con JAXB. La desventaja de este enfoque es que necesitará escribir sus propios validadores. Posible, pero pierde las capacidades nativas de JAXB para hacerlo.
hfontanez