动态根元素JAXB?

8

我正在尝试与第三方系统集成,并且根据对象类型,返回的XML文档的根元素会发生变化。我使用JAXB库进行编组/解组。

根元素1:

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

根号2:

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

我正在消费所有不同的XML,并将它们映射到一个通用对象

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

还有一个将未知值转换为映射的适配器:

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

这将在字段中放置id和手机号码,并将其余部分(未知)放入映射中。如果根元素像上面的示例一样固定为ROW,则可以使用此方法。
如何使其在每个XML中的根元素都不同的情况下工作?可能的一种方法是解组时只是对根元素持无所谓态度,从而达到同样的效果。

1
我认为这是毫无用处的通用性。这里没有任何契约。你可以返回任何你想要的东西。你让你的用户猜测。我要么会创建一个更好的API,为每种类型返回明确的方法,要么放弃这个要求。 - duffymo
你至少知道所有可能的根元素是什么吗? - Harshal Khachane
@HarshalKhachane 是的,我知道这个集合! - Siddharth Trikha
如果集合不太长,则可以简单地使用继承,将所有的XMLElement移动到父类中,并为每个根元素创建子类。 - Harshal Khachane
2个回答

3

不需要使用JAXB,可以使用StAX自行解析。

在下面的代码中,由于值9831138683过大无法表示成int类型,已将字段mobileNoint类型更改为String类型。

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 + "]";
    }
}

测试

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

输出

Row[id=1, mobileNo=9831138683, otherElements={A=1, B=2}]
Row[id=3, mobileNo=9831138683, otherElements={specific-attr1=1, specific-attr2=2}]

我不确定在处理这种类型的 xml 时,通过 StAX 在更低的抽象级别上工作是否比通过 JAXB 对已知数据对象进行取消编组(其中 XSD 已定义)更好。即使我使用 StAX,我也需要将 Row 对象序列化回适当的 xml,并匹配根元素。 - Siddharth Trikha
此外,我还需要处理XML和JSON输入。 - Siddharth Trikha
1
@SiddharthTrikha 如果您需要在序列化回XML时保留根元素名称,则需要让Row类记住该名称。--- 如果您需要处理JSON输入,请使用JSON解析器。 - Andreas
1
@Andreas 的观点非常好,将 mobileNo 设为字符串。即使 int 大到足以存储电话号码,该值也不代表金额。在我看来,这应该被视为一个字符串,即使该值是数字的。 - hfontanez

2
我认为没有办法做到你所要求的。在 XML 中,根节点(文档)必须具有定义的元素或类。换句话说,xs:any 只适用于子元素。即使有实现这个的方式,这也是一个不好的决定。相反,你应该给同一元素添加一个名称属性来区分 XML 文件。例如:
<?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>

为此,您只需要在现有元素中添加一个name属性即可:

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

我正在从客户端消费XML,因此无法更改XML结构。我尝试从“Row”中删除“XmlRootElement”,并通过以下方式取消编组到“JAXBElement”:JAXBElement<Row> element = unmarshaller.unmarshal(new StreamSource(xml), Row.class); Row root = element.getValue();。这将填充所有字段并消费具有不同根元素的XML。 - Siddharth Trikha
@SiddharthTrikha 如果是这样,您需要多个基类来表示您的根元素,这将会很混乱,而且很可能无法很好地扩展。您的根节点不能是 xs:any。我也尝试过在 XML 模式中使用它。一旦您将根元素设置为基本上任何内容,您的模式就会破裂。换句话说,xs:any 只能在子节点的上下文中使用。最后,您需要去向客户展示为什么这是一个糟糕的实现。 - hfontanez
@SiddharthTrikha 我明白从 Row 中删除 XmlRootElement 在取消编组时会起作用。我想问你的是,如果必须进行编组,你会如何处理?你可以使用其他机制来完成,但不能使用JAXB。这种方法的缺点是你需要编写自己的验证器。虽然可行,但你将失去JAXB的本地功能。 - hfontanez

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接