JAXB - 将动态生成的命名空间移动到文档根目录

人气:692 发布:2022-10-16 标签: xml java xml-namespaces jaxb

问题描述

我有这个 POJO,它封装了一个 Atom 条目的动态、非嵌套元素:

公共类 SimpleElement {私有命名空间命名空间;私有字符串标签名;私有字符串值;私有集合<属性>属性;/* getter/setter/... */

为了完整起见,属性

公共类属性{私有字符串名称;私有字符串值;私有命名空间命名空间;/* getter/setter/... */

还有命名空间:

公共类命名空间{私有最终字符串 uri;私有最终字符串前缀;/* getter/setter/... */

SimpleElementAdapterSimpleElement 序列化为其 org.w3c.dom.Element 对应项.

这种方法的唯一问题是命名空间总是在元素级别结束,而不是在文档根目录.

有没有办法在文档根目录动态声明命名空间?

解决方案

我的建议

我的建议是让 JAXB 实现编写它认为合适的名称空间声明.只要元素具有正确的命名空间限定,命名空间声明出现的位置并不重要.

如果您忽略我的建议,以下是您可以使用的方法.

原始答案

指定要包含在根元素上的命名空间

您可以使用 NamespacePrefixMapper 扩展向根元素添加额外的命名空间声明(请参阅:https://jaxb.java.net/nonav/2.2.11/docs/ch05.html#prefixmapper).您需要从您自己的对象模型中派生出应该在根目录中声明的命名空间.

注意: NamespacePrefixMappercom.sun.xml.bind.marshaller 包中.这意味着您将需要在类路径中使用 JAXB 引用实现 jar(请参阅:https://jaxb.java.net/).

import com.sun.xml.bind.marshaller.*;公共类 MyNamespacePrefixMapper 扩展 NamespacePrefixMapper {@覆盖公共字符串 getPreferredPrefix(字符串 arg0,字符串 arg1,布尔 arg2){返回空值;}@覆盖公共字符串[] getPreDeclaredNamespaceUris2() {return new String[] {"ns1", "http://www.example.com/FOO", "ns2", "http://www.example.com/BAR"};}}

Marshaller

上指定 NamespacePrefixMapper

com.sun.xml.bind.namespacePrefixMapper 属性用于指定 Marshaller 上的 NamespacePrefixMapper.

marshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", new MyNamespacePrefixMapper());

演示代码

Java 模型(Foo)

import javax.xml.bind.annotation.*;@XmlRootElement公共类 Foo {私有对象对象;@XmlAnyElement公共对象 getObject() {返回对象;}公共无效setObject(对象对象){this.object = 对象;}}

演示

import javax.xml.bind.*;导入 javax.xml.parsers.*;导入 org.w3c.dom.*;导入 org.w3c.dom.Element;公共类演示{公共静态 void main(String[] args) 抛出异常 {JAXBContext jc = JAXBContext.newInstance(Foo.class);Foo foo = new Foo();DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();DocumentBuilder db = dbf.newDocumentBuilder();文档文档 = db.newDocument();元素 element = document.createElementNS("http://www.example.com/FOO", "ns1:foo");foo.setObject(元素);编组器编组器 = jc.createMarshaller();marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);marshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", new MyNamespacePrefixMapper());marshaller.marshal(foo, System.out);}}

输出

以下是将产生的示例输出:

<?xml version="1.0" encoding="UTF-8" Standalone="yes"?><foo xmlns:ns1="http://www.example.com/FOO" xmlns:ns2="http://www.example.com/BAR"><ns1:foo/></foo>

更新

明确的答案,谢谢.但是,我需要从简单元素适配器.你有什么建议?我看到正确的唯一方法现在正在使 NSMapper 成为可变单例,以便SimpleElementAdapter 可以根据需要添加命名空间.

我忘记了你的 XmlAdapter.

Java 模型

下面是模型的一个更复杂的迭代,其中 Foo 不是持有一个 DOM 元素的实例,而是持有一个 Bar 的实例,该实例被改编成一个DOM 元素的实例.

Foo

import javax.xml.bind.annotation.*;导入 javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;@XmlRootElement公共类 Foo {私人酒吧酒吧;@XmlAnyElement@XmlJavaTypeAdapter(BarAdapter.class)公共酒吧 getBar() {返回栏;}公共无效setBar(酒吧栏){this.bar = bar;}}

条形

公共类栏{私有字符串值;公共字符串 getValue() {返回值;}公共无效setValue(字符串值){this.value = 值;}}

条形适配器

import javax.xml.bind.annotation.adapters.XmlAdapter;导入 javax.xml.parsers.*;导入 org.w3c.dom.*;公共类 BarAdapter 扩展 XmlAdapter<Object, Bar>{@覆盖公共对象元帅(酒吧栏)抛出异常{DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();DocumentBuilder db = dbf.newDocumentBuilder();文档文档 = db.newDocument();元素 element = document.createElementNS("http://www.example.com/BAR", "ns:bar");element.setTextContent(bar.getValue());返回元素;}@覆盖公共酒吧解组(对象arg0)抛出异常{//TODO 自动生成的方法存根返回空值;}}

获取命名空间声明

由于您的对象模型不直接保存 DOM 元素,因此您无法遍历它来获取命名空间声明.相反,我们可以对 ContentHandler 进行编组以收集它们.以下是编组到 ContentHandler 的原因:

它为我们提供了一个简单的事件,我们可以使用它来收集命名空间声明.它实际上不会产生任何东西,因此它是我们可以使用的最轻的元帅目标.

NsContentHandler contentHandler = new NsContentHandler();marshaller.marshal(foo, contentHandler);

NsContentHandler

ContentHandler 的实现如下所示:

import java.util.*;导入 org.xml.sax.SAXException;导入 org.xml.sax.helpers.DefaultHandler;公共类 NsContentHandler 扩展 DefaultHandler {私有映射<字符串,字符串>命名空间 = 新 TreeMap<String, String>();@覆盖公共无效 startPrefixMapping(字符串前缀,字符串 uri)抛出 SAXException {if(!namespaces.containsKey(prefix)) {namespaces.put(前缀, uri);}}公共地图<字符串,字符串>获取命名空间(){返回命名空间;}}

指定要包含在根元素上的命名空间

MyNamespacePrefixMapper 的实现稍作更改,以使用从我们的 ContentHandler 捕获的名称空间.

导入java.util.Map;导入 java.util.Map.Entry;导入 com.sun.xml.bind.marshaller.*;公共类 MyNamespacePrefixMapper 扩展 NamespacePrefixMapper {私有字符串 [] 命名空间;public MyNamespacePrefixMapper(Map<String, String> 命名空间) {this.namespaces = new String[namespaces.size() * 2];整数索引 = 0;for(Entry entry : namespaces.entrySet()) {this.namespaces[index++] = entry.getKey();this.namespaces[index++] = entry.getValue();}}@覆盖公共字符串 getPreferredPrefix(字符串 arg0,字符串 arg1,布尔 arg2){返回空值;}@覆盖公共字符串[] getPreDeclaredNamespaceUris2() {返回命名空间;}}

演示代码

import javax.xml.bind.*;公共类演示{公共静态 void main(String[] args) 抛出异常 {JAXBContext jc = JAXBContext.newInstance(Foo.class);酒吧酒吧 = 新酒吧();bar.setValue("Hello World");Foo foo = new Foo();foo.setBar(bar);编组器编组器 = jc.createMarshaller();//Marshal 首次获取命名空间声明NsContentHandler contentHandler = new NsContentHandler();marshaller.marshal(foo, contentHandler);//真正的第二次元帅marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);marshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", new MyNamespacePrefixMapper(contentHandler.getNamespaces()));marshaller.marshal(foo, System.out);}}

输出

<?xml version="1.0" encoding="UTF-8" Standalone="yes"?><foo xmlns:ns="http://www.example.com/BAR"><ns:bar>你好世界</ns:bar></foo>

I've got this POJO, encapsulating a dynamic, non-nested element of an Atom entry:

public class SimpleElement {

    private Namespace namespace;
    private String tagName;
    private String value;
    private Collection<Attribute> attributes;

    /* getters/setters/... */

And for completeness, Attribute

public class Attribute {

    private String name;
    private String value;
    private Namespace namespace;  

    /* getters/setters/... */

And Namespace:

public class Namespace {

    private final String uri;
    private final String prefix;

    /* getters/setters/... */

SimpleElementAdapter serializes a SimpleElement into its org.w3c.dom.Element counterpart.

The only problem with this approach is that namespaces always end up at element level, never at document root.

Is there a way to dynamically declare namespaces at document root?

解决方案

MY RECOMMENDATION

My recommendation is to let the JAXB implementation write the namespace declarations as it sees fit. As long as the elements are properly namespace qualified it does not really matter where the namespace declarations occur.

If you ignore my recommendation, below is an approach you can use.

ORIGINAL ANSWER

Specify the Namespaces to Include on Root Element

You can use the NamespacePrefixMapper extension to add extra namespace declarations to the root element (see: https://jaxb.java.net/nonav/2.2.11/docs/ch05.html#prefixmapper). You will need to derive from your own object model what namespaces should be declared at the root.

Note: NamespacePrefixMapper is in the com.sun.xml.bind.marshaller package. This means you will need the JAXB refereince implementation jar on your classpath (see: https://jaxb.java.net/).

import com.sun.xml.bind.marshaller.*;

public class MyNamespacePrefixMapper extends NamespacePrefixMapper {

    @Override
    public String getPreferredPrefix(String arg0, String arg1, boolean arg2) {
        return null;
    }

    @Override
    public String[] getPreDeclaredNamespaceUris2() {
        return new String[] {"ns1", "http://www.example.com/FOO", "ns2", "http://www.example.com/BAR"};
    }


}

Specify the NamespacePrefixMapper on the Marshaller

The com.sun.xml.bind.namespacePrefixMapper property is used to specify the NamespacePrefixMapper on the Marshaller.

marshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", new MyNamespacePrefixMapper());

Demo Code

Java Model (Foo)

import javax.xml.bind.annotation.*;

@XmlRootElement
public class Foo {

    private Object object;

    @XmlAnyElement
    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }

}

Demo

import javax.xml.bind.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.w3c.dom.Element;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Foo.class);

        Foo foo = new Foo();

        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        Document document = db.newDocument();
        Element element = document.createElementNS("http://www.example.com/FOO", "ns1:foo");
        foo.setObject(element);

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", new MyNamespacePrefixMapper());
        marshaller.marshal(foo, System.out);
    }

}

Output

Below is sample output that will be produced:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<foo xmlns:ns1="http://www.example.com/FOO" xmlns:ns2="http://www.example.com/BAR">
    <ns1:foo/>
</foo>

UPDATE

Clear answer, thanks. However, I need access to the NSMapper from SimpleElementAdapter. What do you suggest? The only way I see right now is making the NSMapper a mutable singleton so that SimpleElementAdapter can add namespaces if needed.

I forgot about your XmlAdapter.

Java Model

Below is a more complicated iteration of the model, where instead of Foo holding an instance of a DOM element, it holds and instance of Bar that gets adapted into an instance of a DOM element.

Foo

import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlRootElement
public class Foo {

    private Bar bar;

    @XmlAnyElement
    @XmlJavaTypeAdapter(BarAdapter.class)
    public Bar getBar() {
        return bar;
    }

    public void setBar(Bar bar) {
        this.bar = bar;
    }

}

Bar

public class Bar {

    private String value;

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

}

BarAdapter

import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.parsers.*;
import org.w3c.dom.*;

public class BarAdapter extends XmlAdapter<Object, Bar>{

    @Override
    public Object marshal(Bar bar) throws Exception {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        Document document = db.newDocument();
        Element element = document.createElementNS("http://www.example.com/BAR", "ns:bar");
        element.setTextContent(bar.getValue());
        return element;
    }

    @Override
    public Bar unmarshal(Object arg0) throws Exception {
        // TODO Auto-generated method stub
        return null;
    }

}

Grab Namespace Declarations

Since your object model does not hold the DOM elements directly you can't traverse it to get the namespace declarations. Instead we could do a marshal to a ContentHandler to collect them. Below are the reasons for marshalling to a ContentHandler:

It gives us an easy event which we can use to collection the namespace declarations. It doesn't actually produce anything so it is the lightest marshal target we can use.

NsContentHandler contentHandler = new NsContentHandler();
marshaller.marshal(foo, contentHandler);

NsContentHandler

The implementation of ContentHandler will look something like:

import java.util.*;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

public class NsContentHandler extends DefaultHandler {

    private Map<String, String> namespaces = new TreeMap<String, String>();

    @Override
    public void startPrefixMapping(String prefix, String uri) throws SAXException {
        if(!namespaces.containsKey(prefix)) {
            namespaces.put(prefix, uri);
        }
    }

    public Map<String, String> getNamespaces() {
        return namespaces;
    }

}

Specify the Namespaces to Include on Root Element

The implementation of MyNamespacePrefixMapper changes a little to use the namrespaces captured from our ContentHandler.

import java.util.Map;
import java.util.Map.Entry;
import com.sun.xml.bind.marshaller.*;

public class MyNamespacePrefixMapper extends NamespacePrefixMapper {

    private String[] namespaces;

    public MyNamespacePrefixMapper(Map<String, String> namespaces) {
        this.namespaces = new String[namespaces.size() * 2];
        int index = 0;
        for(Entry<String, String> entry : namespaces.entrySet()) {
            this.namespaces[index++] = entry.getKey();
            this.namespaces[index++] = entry.getValue();
        }
    }

    @Override
    public String getPreferredPrefix(String arg0, String arg1, boolean arg2) {
        return null;
    }

    @Override
    public String[] getPreDeclaredNamespaceUris2() {
        return namespaces;
    }

}

Demo Code

import javax.xml.bind.*;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(Foo.class);

        Bar bar = new Bar();
        bar.setValue("Hello World");
        Foo foo = new Foo();
        foo.setBar(bar);

        Marshaller marshaller = jc.createMarshaller();

        // Marshal First Time to Get Namespace Declarations
        NsContentHandler contentHandler = new NsContentHandler();
        marshaller.marshal(foo, contentHandler);

        // Marshal Second Time for Real
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", new MyNamespacePrefixMapper(contentHandler.getNamespaces()));
        marshaller.marshal(foo, System.out);
    }

}

Output

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<foo xmlns:ns="http://www.example.com/BAR">
    <ns:bar>Hello World</ns:bar>
</foo>

542