JAXB:如何在解组XML文档时忽略命名空间?

78

我的模式规定了一个命名空间,但是文档中没有。在JAXB取消编组(XML -> 对象)期间忽略命名空间的最简单方法是什么?

换句话说,我有:

<foo><bar></bar></foo>

而不是,

<foo xmlns="http://tempuri.org/"><bar></bar></foo>

我的问题实际上是相反的 - 我有一些带有 xmlns 属性(在一个或多个元素上)的文档,还有一些没有。@lunicon 的解决方案使我能够读取这两种样式。 - Lambart
6个回答

109
这里是 VonCs 解决方案的扩展/修改,以防有人不想费力实现自己的过滤器来完成此操作。它还展示了如何输出一个没有命名空间的 JAXB 元素。所有这些都是通过使用 SAX 过滤器来实现的。
过滤器实现:
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;

import org.xml.sax.helpers.XMLFilterImpl;

public class NamespaceFilter extends XMLFilterImpl {

    private String usedNamespaceUri;
    private boolean addNamespace;

    //State variable
    private boolean addedNamespace = false;

    public NamespaceFilter(String namespaceUri,
            boolean addNamespace) {
        super();

        if (addNamespace)
            this.usedNamespaceUri = namespaceUri;
        else 
            this.usedNamespaceUri = "";
        this.addNamespace = addNamespace;
    }



    @Override
    public void startDocument() throws SAXException {
        super.startDocument();
        if (addNamespace) {
            startControlledPrefixMapping();
        }
    }



    @Override
    public void startElement(String arg0, String arg1, String arg2,
            Attributes arg3) throws SAXException {

        super.startElement(this.usedNamespaceUri, arg1, arg2, arg3);
    }

    @Override
    public void endElement(String arg0, String arg1, String arg2)
            throws SAXException {

        super.endElement(this.usedNamespaceUri, arg1, arg2);
    }

    @Override
    public void startPrefixMapping(String prefix, String url)
            throws SAXException {


        if (addNamespace) {
            this.startControlledPrefixMapping();
        } else {
            //Remove the namespace, i.e. don´t call startPrefixMapping for parent!
        }

    }

    private void startControlledPrefixMapping() throws SAXException {

        if (this.addNamespace && !this.addedNamespace) {
            //We should add namespace since it is set and has not yet been done.
            super.startPrefixMapping("", this.usedNamespaceUri);

            //Make sure we dont do it twice
            this.addedNamespace = true;
        }
    }

}

此过滤器旨在能够添加命名空间(如果不存在):

new NamespaceFilter("http://www.example.com/namespaceurl", true);

并且要移除任何已有的命名空间:

new NamespaceFilter(null, false);

过滤器可以在解析时按以下方式使用:
//Prepare JAXB objects
JAXBContext jc = JAXBContext.newInstance("jaxb.package");
Unmarshaller u = jc.createUnmarshaller();

//Create an XMLReader to use with our filter
XMLReader reader = XMLReaderFactory.createXMLReader();

//Create the filter (to add namespace) and set the xmlReader as its parent.
NamespaceFilter inFilter = new NamespaceFilter("http://www.example.com/namespaceurl", true);
inFilter.setParent(reader);

//Prepare the input, in this case a java.io.File (output)
InputSource is = new InputSource(new FileInputStream(output));

//Create a SAXSource specifying the filter
SAXSource source = new SAXSource(inFilter, is);

//Do unmarshalling
Object myJaxbObject = u.unmarshal(source);

如果要使用此过滤器从JAXB对象输出XML,请查看下面的代码。

//Prepare JAXB objects
JAXBContext jc = JAXBContext.newInstance("jaxb.package");
Marshaller m = jc.createMarshaller();

//Define an output file
File output = new File("test.xml");

//Create a filter that will remove the xmlns attribute      
NamespaceFilter outFilter = new NamespaceFilter(null, false);

//Do some formatting, this is obviously optional and may effect performance
OutputFormat format = new OutputFormat();
format.setIndent(true);
format.setNewlines(true);

//Create a new org.dom4j.io.XMLWriter that will serve as the 
//ContentHandler for our filter.
XMLWriter writer = new XMLWriter(new FileOutputStream(output), format);

//Attach the writer to the filter       
outFilter.setContentHandler(writer);

//Tell JAXB to marshall to the filter which in turn will call the writer
m.marshal(myJaxbObject, outFilter);

希望这篇文章能够帮到有需要的人,因为我花了一天时间去做这件事情,差点两次放弃 ;)


2
这个解决方案是否适用于在整个文档中使用多个命名空间的多个嵌套XML对象?我尝试在这样的情况下使用此示例,并发现虽然它能够删除XML文档中前两个级别(根元素和根元素的子元素)的命名空间,但似乎无法过滤掉更深层次的命名空间。为了对这样的XML文档进行取消编组,我不得不使用根元素的孙子及以下的命名空间声明。 - Bionic_Geek
如果您愿意分享您改进的过滤器,我相信人们也会很乐意看到。 - Kristofer
3
为什么JAXB不能提供更好的错误信息,还需要这些花招,这让我很困惑。这是一个非常普遍的问题,几乎每个人都会遇到! - user798719
非常感谢!运行得很好...有点荒谬,所有这些都是为了忽略供应商文件中的死亡命名空间所需的 :-) - echen
这个方法很好用,但如果你只想删除命名空间,请尝试来自Jaxb ignore the namespace on unmarshalling的选项3),它使用了一个设置setNamespaceAware(false)的SAXParserFactory。 - tanderson

42

我在使用XMLFilter解决方案时遇到编码问题,因此我使用XMLStreamReader来忽略命名空间:

class XMLReaderWithoutNamespace extends StreamReaderDelegate {
    public XMLReaderWithoutNamespace(XMLStreamReader reader) {
      super(reader);
    }
    @Override
    public String getAttributeNamespace(int arg0) {
      return "";
    }
    @Override
    public String getNamespaceURI() {
      return "";
    }
}

InputStream is = new FileInputStream(name);
XMLStreamReader xsr = XMLInputFactory.newFactory().createXMLStreamReader(is);
XMLReaderWithoutNamespace xr = new XMLReaderWithoutNamespace(xsr);
Unmarshaller um = jc.createUnmarshaller();
Object res = um.unmarshal(xr);

4
当我注意到您的解决方案时,我正要尝试实施Kristofer的解决方案。您的方案更简单,并且对我有用,谢谢!但它仍然太复杂了,为什么我们必须这样做?JAXB应该为这种常见情况提供内置解决方案,例如属性设置。 - Lambart
也不要忘记关闭 FileInputStream :) - Lambart
1
这并没有忽略 package.info 中包含的命名空间。因此,应该使 getNamespaceURI 方法返回 package.info 的内容。在这种情况下,XMLReaderWithoutNamespace 应该改为 XMLReaderWithNamespaceInMyPackageDotInfo。 - Net Dawg

19
我相信您必须在xml文档中添加命名空间,例如使用SAX过滤器
这意味着:
  • 定义一个ContentHandler接口和一个新类,该类将在JAXB获取事件之前截获SAX事件。
  • 定义一个XMLReader,它将设置内容处理程序
然后将两者链接在一起:
public static Object unmarshallWithFilter(Unmarshaller unmarshaller,
java.io.File source) throws FileNotFoundException, JAXBException 
{
    FileReader fr = null;
    try {
        fr = new FileReader(source);
        XMLReader reader = new NamespaceFilterXMLReader();
        InputSource is = new InputSource(fr);
        SAXSource ss = new SAXSource(reader, is);
        return unmarshaller.unmarshal(ss);
    } catch (SAXException e) {
        //not technically a jaxb exception, but close enough
        throw new JAXBException(e);
    } catch (ParserConfigurationException e) {
        //not technically a jaxb exception, but close enough
        throw new JAXBException(e);
    } finally {
        FileUtil.close(fr); //replace with this some safe close method you have
    }
}

1
为什么这篇文章里会有垃圾广告链接? - TomWolk
7
@TomWolk 对不起,我已经恢复了正确的链接(使用web.archive.org)。请记住,当我七年前写下这个答案时,这个链接并不是垃圾广告…。 - VonC
@Macilias 我这边没有更新。如果你找到了任何更新,请不要犹豫更新此回答。 - VonC
好的,也许不完全过时,但我错过了NamespaceFilterXMLReader。实际上,Kristofer的高分帖子提供了一个。 - Macilias

5

在我的情况下,我有很多命名空间,在一些调试后,我找到了另一个解决方案,只需要更改NamespaceFitler类。对于我的情况(只需取消标注),这个解决方案运行良好。

 import javax.xml.namespace.QName;
 import org.xml.sax.Attributes;
 import org.xml.sax.ContentHandler;
 import org.xml.sax.SAXException;
 import org.xml.sax.helpers.XMLFilterImpl;
 import com.sun.xml.bind.v2.runtime.unmarshaller.SAXConnector;

 public class NamespaceFilter extends XMLFilterImpl {
    private SAXConnector saxConnector;

    @Override
    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
        if(saxConnector != null) {
            Collection<QName> expected = saxConnector.getContext().getCurrentExpectedElements();
            for(QName expectedQname : expected) {
                if(localName.equals(expectedQname.getLocalPart())) {
                    super.startElement(expectedQname.getNamespaceURI(), localName, qName, atts);
                    return;
                }
            }
        }
        super.startElement(uri, localName, qName, atts);
    }

    @Override
    public void setContentHandler(ContentHandler handler) {
        super.setContentHandler(handler);
        if(handler instanceof SAXConnector) {
            saxConnector = (SAXConnector) handler;
        }
    }
}

1

在将XML文档提供给JAXB之前,将默认命名空间添加到文档的另一种方法是使用 JDom

  1. 将XML解析为文档
  2. 遍历并设置所有元素上的命名空间
  3. 使用JDOMSource取消编组

就像这样:

public class XMLObjectFactory {
    private static Namespace DEFAULT_NS = Namespace.getNamespace("http://tempuri.org/");

    public static Object createObject(InputStream in) {
        try {
            SAXBuilder sb = new SAXBuilder(false);
            Document doc = sb.build(in);
            setNamespace(doc.getRootElement(), DEFAULT_NS, true);
            Source src = new JDOMSource(doc);
            JAXBContext context = JAXBContext.newInstance("org.tempuri");
            Unmarshaller unmarshaller = context.createUnmarshaller();
            JAXBElement root = unmarshaller.unmarshal(src);
            return root.getValue();
        } catch (Exception e) {
            throw new RuntimeException("Failed to create Object", e);
        }
    }

    private static void setNamespace(Element elem, Namespace ns, boolean recurse) {
        elem.setNamespace(ns);
        if (recurse) {
            for (Object o : elem.getChildren()) {
                setNamespace((Element) o, ns, recurse);
            }
        }
    }

1
然而,唯一的问题是您必须将整个XML文件读入内存,这对于大型XML文件来说并不是一个选项。 - Brian

0

这只是对lunicon答案的修改(https://dev59.com/rHVC5IYBdhLWcg3wfhKL#24387115),如果您想在解析期间将一个命名空间替换为另一个命名空间。如果您想要查看正在发生的事情,请取消注释输出行并设置断点。

public class XMLReaderWithNamespaceCorrection extends StreamReaderDelegate {

    private final String wrongNamespace;
    private final String correctNamespace;

    public XMLReaderWithNamespaceCorrection(XMLStreamReader reader, String wrongNamespace, String correctNamespace) {
        super(reader);

        this.wrongNamespace = wrongNamespace;
        this.correctNamespace = correctNamespace;
    }

    @Override
    public String getAttributeNamespace(int arg0) {
//        System.out.println("--------------------------\n");
//        System.out.println("arg0: " + arg0);
//        System.out.println("getAttributeName: " + getAttributeName(arg0));
//        System.out.println("super.getAttributeNamespace: " + super.getAttributeNamespace(arg0));
//        System.out.println("getAttributeLocalName: " + getAttributeLocalName(arg0));
//        System.out.println("getAttributeType: " + getAttributeType(arg0));
//        System.out.println("getAttributeValue: " + getAttributeValue(arg0));
//        System.out.println("getAttributeValue(correctNamespace, LN):"
//                + getAttributeValue(correctNamespace, getAttributeLocalName(arg0)));
//        System.out.println("getAttributeValue(wrongNamespace, LN):"
//                + getAttributeValue(wrongNamespace, getAttributeLocalName(arg0)));

        String origNamespace = super.getAttributeNamespace(arg0);

        boolean replace = (((wrongNamespace == null) && (origNamespace == null))
                || ((wrongNamespace != null) && wrongNamespace.equals(origNamespace)));
        return replace ? correctNamespace : origNamespace;
    }

    @Override
    public String getNamespaceURI() {
//        System.out.println("getNamespaceCount(): " + getNamespaceCount());
//        for (int i = 0; i < getNamespaceCount(); i++) {
//            System.out.println(i + ": " + getNamespacePrefix(i));
//        }
//
//        System.out.println("super.getNamespaceURI: " + super.getNamespaceURI());

        String origNamespace = super.getNamespaceURI();

        boolean replace = (((wrongNamespace == null) && (origNamespace == null))
                || ((wrongNamespace != null) && wrongNamespace.equals(origNamespace)));
        return replace ? correctNamespace : origNamespace;
    }
}

用法:

InputStream is = new FileInputStream(xmlFile);
XMLStreamReader xsr = XMLInputFactory.newFactory().createXMLStreamReader(is);
XMLReaderWithNamespaceCorrection xr =
    new XMLReaderWithNamespaceCorrection(xsr, "http://wrong.namespace.uri", "http://correct.namespace.uri");
rootJaxbElem = (JAXBElement<SqgRootType>) um.unmarshal(xr);
handleSchemaError(rootJaxbElem, pmRes);

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