JAXB在解组包含相对路径DTD文档时出现SAXParseException错误

10

我有一个类,用于将来自第三方源(我无法控制其内容)的 XML 进行取消编组。以下是取消编组的片段:

JAXBContext jContext = JAXBContext.newInstance("com.optimumlightpath.it.aspenoss.xsd"); 
Unmarshaller unmarshaller = jContext.createUnmarshaller() ;
StringReader xmlStr = new StringReader(str.value);
Connections conns = (Connections) unmarshaller.unmarshal(xmlStr); 

Connections 是一个使用 xjc 从 dtd->xsd->class 自动生成的类。包 com.optimumlightpath.it.aspenoss.xsd 包含所有这样的类。

我收到的 xml 包含 DOCTYPE 中的相对路径。基本上,str.value 包含:

<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
<!DOCTYPE Connections SYSTEM "./dtd/Connections.dtd">
<Connections>
...
</Connections>

这个Java 1.5应用程序可以成功运行。为了避免上述错误,我不得不在项目根目录下创建一个./dtd目录,并包含所有的dtd文件(不确定为什么要这样做,但我们会解决这个问题)。

后来,我在Tomcat 5.5上创建了一个使用上述类的Web服务。在unmarshal行上,我收到了 [org.xml.sax.SAXParseException: Relative URI "./dtd/Connections.dtd"; can not be resolved without a document URI.] 的错误。我已经尝试在每个相关文件夹(项目根目录、WebContent、WEB-INF、Tomcat工作目录等)中创建./dtd,但都没有成功。

问题#1:我应该将./dtd放在哪里,以便类在作为Tomcat Web服务运行时可以找到它?是否需要进行任何Tomcat或服务配置以使目录被识别?

问题#2:这个类为什么需要dtd文件?难道它没有在dtd->xsd->class的注释中获得反序列化所需的所有信息吗?我已阅读过许多关于禁用验证、设置EntityResource和其他解决方案的帖子,但是这个类并不总是部署为Web服务,我不想有两个代码分支。

3个回答

11
从InputStream或Reader反序列化时,解析器无法知道文档的系统ID(URI /位置),因此无法解析相对路径。看起来解析器尝试使用当前工作目录来解析引用,但仅在从IDE或命令行运行时有效。为了覆盖此行为并自行解析,您需要实现EntityResolver,正如Blaise Doughan所提到的那样。
经过一些实验,我找到了一种标准的方法来做到这一点。您需要从SAXSource进行反序列化,SAXSource又由XMLReader和InputSource构成。在此示例中,DTD位于带注释的类旁边,因此可以在类路径中找到。
Main.java
public class Main {
    private static final String FEATURE_NAMESPACES = "http://xml.org/sax/features/namespaces";
    private static final String FEATURE_NAMESPACE_PREFIXES = "http://xml.org/sax/features/namespace-prefixes";

    public static void main(String[] args) throws JAXBException, IOException, SAXException {
        JAXBContext ctx = JAXBContext.newInstance(Root.class);
        Unmarshaller unmarshaller = ctx.createUnmarshaller();

        XMLReader xmlreader = XMLReaderFactory.createXMLReader();
        xmlreader.setFeature(FEATURE_NAMESPACES, true);
        xmlreader.setFeature(FEATURE_NAMESPACE_PREFIXES, true);
        xmlreader.setEntityResolver(new EntityResolver() {
            public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
                // TODO: Check if systemId really references root.dtd
                return new InputSource(Root.class.getResourceAsStream("root.dtd"));
            }
        });

        String xml = "<!DOCTYPE root SYSTEM './root.dtd'><root><element>test</element></root>";
        InputSource input = new InputSource(new StringReader(xml));
        Source source = new SAXSource(xmlreader, input);

        Root root = (Root)unmarshaller.unmarshal(source);
        System.out.println(root.getElement());
    }
}

Root.java

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Root {
    @XmlElement
    private String element;

    public String getElement() {
        return element;
    }

    public void setElement(String element) {
        this.element = element;
    }
}

root.dtd

<?xml version="1.0" encoding="UTF-8"?>
<!ELEMENT root (element)>
<!ELEMENT element (#PCDATA)>

Jorn - 感谢您的回复。我首先尝试了Blaise的建议,因为它需要最少的代码更改。但是你们两个都提供了非常有帮助的答案。我能否给两位都表示认可? - Bill Dolan
我的方法是可行的,但 Jorn 的方法的优势在于你可以保持对 JAXB 实现的独立性,这是最具可移植性的解决方案。 - bdoughan

2
问题 #2:为什么这个类需要首先有DTD文件?
答:不是JAXB实现正在寻找DTD文件,而是底层解析器。
问题 #1:我在哪里可以找到./dtd,以便在作为Tomcat Web服务运行时,该类可以找到它?
答:我不确定,但下面我将演示一种使用MOXy JAXB实现(我是技术负责人)的方法,可以在多个环境中工作。
建议的解决方案:
创建一个EntityResolver,从classpath加载DTD。这样,您可以将DTD与应用程序打包在一起,并始终知道它在部署环境中的位置。
public class DtdEntityResolver implements EntityResolver {

    public InputSource resolveEntity(String publicId, String systemId)
            throws SAXException, IOException {
        InputStream dtd = getClass().getClassLoader().getResourceAsStream("dtd/Connections.dtd");
        return new InputSource(dtd);
    }

}

然后使用MOXy JAXB实现,您可以向下转换到底层实现并设置EntityResolver。
import org.eclipse.persistence.jaxb.JAXBHelper;
...
JAXBContext jContext = JAXBContext.newInstance("com.optimumlightpath.it.aspenoss.xsd");
Unmarshaller unmarshaller = jContext.createUnmarshaller() ;
JAXBHelper.getUnmarshaller(unmarshaller).getXMLUnmarshaller().setEntityResolver(new DtdEntityResolver());
StringReader xmlStr = new StringReader(str.value);
Connections conns =(Connections) unmarshaller.unmarshal(xmlStr);

Blaise - 感谢您抽出时间回复。最初,JAXBHelper 抱怨 unmarshaller 不是 eclipselink 的 unmarshaller。所以我用 org.eclipse.persistence.jaxb.JAXBContext 替换了 javax.xml.bind.JAXBContext,并用 org.eclipse.persistence.jaxb.JAXBUnmarshaller 替换了 javax.xml.bind.Unmarshaller。然而,eclipselink JAXBContext 返回一个 javax JAXBContext 类型。JAXBUnmarshaller 需要一个 eclipselink 类型,如果我尝试重新转换,就会得到强制转换异常。有什么想法吗? - Bill Dolan
你需要在你的模型类中添加一个名为jaxb.properties的文件,并包含以下条目:javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory。 - bdoughan
我可能会在另一个时间尝试。虽然需要更多的代码更改,但我成功地使Jorn的答案起作用了。正如你所说,它是最便携的。非常感谢你们两个!!当我获得15个声望时,我也可以投票支持你的答案。 - Bill Dolan

1
这里是另一种使用 EntityResolver 接口的已给出答案的变化。我的情况是将一个 XML 文件中的相对外部 XML 实体解析到文件夹层次结构中的另一个 XML 文件。下面构造函数的参数是 XML 的“工作”文件夹,而不是进程的工作目录。
public class FileEntityResolver implements EntityResolver {
    private static final URI USER_DIR = SystemUtils.getUserDir().toURI();

    private URI root;

    public FileEntityResolver(File root) {
        this.root = root.toURI();
    }

    @Override @SuppressWarnings("resource")
    public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
        URI systemURI;
        try {
            systemURI = new URI(systemId);
        } catch (URISyntaxException e) {
            return null;
        }

        URI relative = USER_DIR.relativize(systemURI);
        URI resolved = root.resolve(relative);

        File f = new File(resolved);
        FileReader fr = new FileReader(f);
        // SAX will close the file reader for us
        return new InputSource(fr);
    }
}

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