Elemento raiz dinâmico JAXB?

8

Estou tentando me integrar a um sistema de terceiros e, dependendo do tipo de objeto, o elemento raiz do documento XML retornado é alterado. Estou usando a biblioteca JAXB para Marshalling / unmarshalling.

Raiz1:

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

Estou consumindo todos os XMLs diferentes, mapeando-os para um 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;
}

E o adaptador para transformar os valores conhecidos em um 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;
    }
}

Isso colocará o ID e o número do celular nos campos , e o restante, o desconhecido em um mapa .

Isso funciona se o elemento raiz estiver fixo em ROW, como no exemplo acima.

Como fazer isso funcionar de modo que o elemento raiz seja diferente em cada XML? Uma maneira de talvez sermos agnósticos para enraizar o elemento enquanto não organizamos?

Siddharth Trikha
fonte
1
Eu acho que é genérico além de qualquer uso. Não há contrato aqui. Você pode devolver o que quiser. Você faz seus usuários adivinharem. Eu faria uma API melhor que tivesse métodos explícitos para cada tipo retornado ou eliminaria esse requisito.
Duffymo
Pelo menos você sabe o que são todos os elementos raiz possíveis?
Harshal Khachane
@HarshalKhachane Sim, eu conheço o conjunto!
Siddharth Trikha
Se o conjunto não for muito longo, você poderá simplesmente usar a herança e mover todos os XMLElements na classe pai e criar classes filho para cada raiz.
Harshal Khachane 19/11/19

Respostas:

3

Esqueça o JAXB para isso e analise você mesmo com o StAX.

No código abaixo, alterei o campo mobileNode intpara String, pois o valor 9831138683é muito grande para um 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 + "]";
    }
}

Teste

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

Resultado

Row[id=1, mobileNo=9831138683, otherElements={A=1, B=2}]
Row[id=3, mobileNo=9831138683, otherElements={specific-attr1=1, specific-attr2=2}]
Andreas
fonte
Não tenho certeza se trabalhar em um nível mais baixo de abstração (via StAX) para esses tipos de xmldesarquivar vs via JAXB (para objetos de dados conhecidos onde o XSD é definido), o que seria melhor. Mesmo se eu usar o StAX, preciso serializar novamente o Rowobjeto para o xml adequado com o elemento raiz correspondente.
Siddharth Trikha 25/11/19
Além disso, eu tenho que cuidar das entradas XML e JSON.
Siddharth Trikha
1
@SiddharthTrikha Se você precisar manter o nome do elemento raiz ao serializar de volta para XML, será necessário que a Rowclasse se lembre do nome. --- Se você precisar cuidar da entrada JSON, use um analisador JSON.
Andreas
1
@ Andréas excelente ponto em fazer mobileNouma String. Mesmo se intfosse grande o suficiente para armazenar um número de telefone, o valor não representa um valor. Na IMO, isso garante que seja tratado como uma sequência, mesmo que o valor seja numérico.
hfontanez
1

Eu não acho que exista uma maneira de fazer o que você está pedindo. No XML, o nó raiz (o documento) deve ter um elemento (ou classe) definido. Em outras palavras, xs:anyfunciona apenas para subelementos. Mesmo se houvesse uma maneira de conseguir isso, IMHO esta é uma má decisão. Em vez de criar um elemento raiz variável ("dinâmico"), você deve adicionar um atributo name ao mesmo elemento para distinguir os arquivos XML. Por exemplo:

<?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 isso, basta adicionar um atributo de nome ao seu 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
fonte
Estou consumindo XML de um cliente, portanto não posso alterar a estrutura XML. Eu tentei remover XmlRootElementa partir Rowe unmarshalling a JAXBElementpor: JAXBElement<Row> element = unmarshaller.unmarshal(new StreamSource(xml), Row.class); Row root = element.getValue();. Isso permite preencher todos os campos e consumir XMLs com diferentes elementos raiz.
Siddharth Trikha
@SiddharthTrikha, nesse caso, você precisa de várias classes base para representar seu elemento raiz, o que será uma bagunça e provavelmente não aumentará muito. Seu nó raiz não pode ser xs:any. Eu tentei isso com o esquema XML também. Se você definir seu elemento raiz como basicamente qualquer coisa, seu esquema será interrompido. Em outras palavras, xs:anysó pode ser usado no contexto de um nó filho. Por fim, você precisa ir ao seu cliente e mostrar por que essa é uma implementação ruim.
hfontanez
@SiddharthTrikha Entendo que a remoção XmlRootElementde Rowfuncionará ao desarmar. Minha pergunta para você é: como você o organizaria se fosse necessário? Você pode fazer isso com outros mecanismos, mas não com o JAXB. A desvantagem dessa abordagem é que você precisará escrever seus próprios validadores. Possível, mas você perde os recursos nativos do JAXB para fazer isso.
hfontanez