将 QName 作为字符串添加到 @XmlMixed@XmlAnyElement(lax = true) 列表

Posted

技术标签:

【中文标题】将 QName 作为字符串添加到 @XmlMixed@XmlAnyElement(lax = true) 列表【英文标题】:Add QName as string to @XmlMixed@XmlAnyElement(lax = true) list 【发布时间】:2021-09-30 23:05:26 【问题描述】:

对不起,这个模糊的标题,我知道它并不能说明太多。

请考虑以下 xsd 类型定义:

    <xsd:complexType name="TopicExpressionType" mixed="true">
        <xsd:sequence>
            <xsd:any processContents="lax" minOccurs="0"/>
        </xsd:sequence>
        <xsd:attribute name="Dialect" type="xsd:anyURI" use="required"/>
        <xsd:anyAttribute/>
    </xsd:complexType>

完整的 XSD:http://docs.oasis-open.org/wsn/b-2.xsd

对应的JAXB生成的Java类:

package org.oasis_open.docs.wsn.b_2;

import org.w3c.dom.Element;

import javax.xml.bind.annotation.*;
import javax.xml.namespace.QName;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "TopicExpressionType", propOrder = 
        "content"
)
public class TopicExpressionType 
    
    @XmlMixed
    @XmlAnyElement(lax = true)
    protected List<Object> content;

    @XmlAttribute(name = "Dialect", required = true)
    @XmlSchemaType(name = "anyURI")
    protected String dialect;
    @XmlAnyAttribute
    private Map<QName, String> otherAttributes = new HashMap<QName, String>();

    public List<Object> getContent() 
        if (content == null) 
            content = new ArrayList<Object>();
        
        return this.content;
    

    public String getDialect() 
        return dialect;
    

    public void setDialect(String value) 
        this.dialect = value;
    

    public Map<QName, String> getOtherAttributes() 
        return otherAttributes;
    

第一个目标是使用 JAXB 生成这样的 XML:

<wsnt:TopicExpression Dialect="http://docs.oasis-open.org/wsn/t-1/TopicExpression/Concrete" xmlns:tns="http://my.org/TopicNamespace">
tns:t1/*/t3
</wsnt:TopicExpression>

请注意以下几点:

TopicExpression 元素的值基本上是一个引用 QName 的查询字符串。示例:tns:t1/*/t3 TopicExpression 元素的值包含一个或多个类似 QName 的字符串 (tns:t1)。它必须是示例中的字符串,不能是元素(例如:&lt;my-expresseion&gt;tns:t1/*/t3&lt;my-expresseion/&gt;TopicExpression 元素的值是任意字符串(至少从架构的角度来看,它遵循此处定义的规则:https://docs.oasis-open.org/wsn/wsn-ws_topics-1.3-spec-os.pdf 第 18 页) 即使值是字符串,我也需要定义相应的名称空间声明。因此,如果我有这样的表达式:tns:t1 则必须声明 xmlns:tns。如果我的表达式是tns:t1/*/tns2:t3,那么xmlns:tnsxmlns:tns2 都必须声明。

第二个目标是使用 JAXB 获取另一侧的 TopicExpression 的值以及命名空间。

我完全被卡住了,我不知道如何实现这一点。我唯一的想法是手动构建 TopicExpression 的值,并以某种方式告诉编组器包含相关的命名空间声明,尽管没有使用它的实际元素。

更新 包含前面提到的TopicExpression 的完整 SOAP 请求示例:

<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope">
    <env:Header>
        <Action xmlns="http://www.w3.org/2005/08/addressing">http://docs.oasis-open.org/wsn/bw-2/NotificationProducer/SubscribeRequest</Action>
        <MessageID xmlns="http://www.w3.org/2005/08/addressing">urn:uuid:57182d32-4e07-4f5f-8ab3-24838b3e33ac</MessageID>
    </env:Header>
    <env:Body>
        <ns3:Subscribe xmlns:ns3="http://docs.oasis-open.org/wsn/b-2" xmlns:ns4="http://www.w3.org/2005/08/addressing" >
            <ns3:ConsumerReference>
                <ns4:Address>http://my-notification-consumer-url</ns4:Address>
            </ns3:ConsumerReference>
            <ns3:Filter>
                <ns3:TopicExpression Dialect="http://docs.oasis-open.org/wsn/t-1/TopicExpression/Simple" xmlns:ns5="http://my.org/TopicNamespace" xmlns:ns6="http://extension.org/TopicNamespace">
                    ns5:t1/*/ns6:t3
                <ns3:TopicExpression/>
            </ns3:Filter>
        </ns3:Subscribe>
    </env:Body>
</env:Envelope>

【问题讨论】:

尝试将namespace="http://my.org/TopicNamespace" 添加到@XmlType @LMC 不幸的是,命名空间不是固定的,它在运行时会发生变化,所以我无法对其进行硬编码。 如果架构不是永久性的,我会使用普通的 JAXP/XML API 而不是 JAXB 来处理这个简单的 XML,或者我会运行 XSL 转换为更标准化的 XML,并使用 JAXB 读取它。 @m4gic 当然,这不是整个 XML 文档,这只是相关部分。我将研究 XSL 转换,感谢您的提示。 【参考方案1】:

不确定,如果我正确理解了您的要求。看看这个代码示例是否对您有帮助。如果不是,那么也许可以尝试稍微编辑您的问题,让我了解您到底在寻找什么。我将尝试相应地修改和更新代码。尝试一个简单的例子比提供完整的 XSD 更好。另外,请查看以下方法:beforeMarshalafterUnmarshal

以下是我尝试编组和解组的 XML

<tns:TopicExpression Dialect="http://docs.oasis-open.org/wsn/t-1/TopicExpression/Concrete" xmlns:tns="http://my.org/TopicNamespace">
    tns:t1/*/t3
</tns:TopicExpression>

TopicExpressionType.class:

@Data
@XmlAccessorType(XmlAccessType.FIELD)
public class TestPojo 

    @XmlValue
    private String TopicExpression;

    @XmlAnyAttribute
    private Map<String, Object> anyAttributes = new HashMap<>();

主类:

public class Main 
    public static void main(String[] args) throws JAXBException, XMLStreamException 
        final InputStream inputStream = Unmarshalling.class.getClassLoader().getResourceAsStream("topic.xml");
        final XMLStreamReader xmlStreamReader = XMLInputFactory.newInstance().createXMLStreamReader(inputStream);
        final Unmarshaller unmarshaller = JAXBContext.newInstance(TestPojo.class).createUnmarshaller();
        final TestPojo topic = unmarshaller.unmarshal(xmlStreamReader, TestPojo.class).getValue();
        System.out.println(topic.toString());

        StringWriter stringWriter = new StringWriter();
        Map<String, String> namespaces = new HashMap<String, String>();
        namespaces.put("http://my.org/TopicNamespace", "tns");
        Marshaller marshaller = JAXBContext.newInstance(TestPojo.class).createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        marshaller.setProperty(MarshallerProperties.NAMESPACE_PREFIX_MAPPER, namespaces);
        QName qName = new QName("http://my.org/TopicNamespace","TopicExpression","tns");
        JAXBElement<TestPojo> root = new JAXBElement<TestPojo>(qName, TestPojo.class, topic);
        marshaller.marshal(root, stringWriter);
        String result = stringWriter.toString();
        System.out.println(result);
    

如您所见,我已经直接填充了命名空间映射。如果这是动态的,那么您可以在地图中填充相同的内容,因此您可以在编组时添加它。

这将在解组期间提供以下输出:

TestPojo(TopicExpression=
    tns:t1/*/t3
, anyAttributes=Dialect=http://docs.oasis-open.org/wsn/t-1/TopicExpression/Concrete)

在编组期间:

<tns:TopicExpression xmlns:tns="http://my.org/TopicNamespace" Dialect="http://docs.oasis-open.org/wsn/t-1/TopicExpression/Concrete">
    tns:t1/*/t3
</tns:TopicExpression>

【讨论】:

TopicExpression 元素不得包含子元素,如&lt;google:field1&gt;Value1&lt;/google:field1&gt;。它应该包含上面指定的字符串,这是在 ws-topics 文档(上面链接)中定义的查询表达式,如果我可以将实际元素添加到 TopicExpression,那将非常好且易于实现。 @bakcsa83 好的,我已经编辑了答案和代码示例以匹配您的 XML。现在检查。 现在在您的解决方案中,TopicExpression 元素与其值 (tns) 具有相同的名称空间。这是错误的。 TopicExpression 有自己的命名空间,在我的问题中是 wsnt。另请注意,表达式值可能有多个 QName(问题中的第 4 个要点。)。 但我没有看到 wsnt 的任何已声明命名空间。如果您使用了前缀 wsnt,那么它应该在其自身的父 XML 元素中声明。我没有看到任何与wsnt 对应的命名空间。我验证了您的 XML 并得到了 The prefix "wsnt" for element "wsnt:TopicExpression" is not bound. 的结果。如果您有与其对应的命名空间,那么您可以在编组之前将其添加到 Map 并使用它,我相信这应该不是问题。 它不存在,因为它的值不相关,我不想引入整个上下文(SOAP、Spring 等)。关键是 TopicExpression 元素与值 (tns:t1)。【参考方案2】:

所以我实施的解决方案:

创建了一个新的TopicExpressionType 类,它不仅包含表达式字段,还包含命名空间字段,用于表达式:

public class TopicExpressionType 
    String dialect;
    String expression;
    List<Namespace> namespaces;

    public TopicExpressionType(String dialect, String expression, List<Namespace> namespaces) 
        this.dialect = dialect;
        this.expression = expression;
        this.namespaces = namespaces;
    

    public static class Namespace 
        String prefix;
        String namespace;

        public Namespace(String prefix, String namespace) 
            this.prefix = prefix;
            this.namespace = namespace;
        

        public String getPrefix() 
            return prefix;
        

        public String getNamespace() 
            return namespace;
        
    

然后实现了一个XmlAdapter,它知道细节,知道如何从表达式字符串中提取命名空间前缀,它可以在 TopicExpression XML 元素上读/写命名空间声明:

public class TopicExpressionTypeAdapter extends XmlAdapter<Element, TopicExpressionType> 

    @Override
    public Element marshal(TopicExpressionType topicExpression) throws Exception 
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = dbf.newDocumentBuilder();
        Document document = db.newDocument();
        Element element = document.createElementNS("http://docs.oasis-open.org/wsn/b-2", "mns1:TopicExpression");
        element.setAttribute("Dialect", topicExpression.getDialect());

        element.setTextContent(topicExpression.getExpression());
        for (var ns : topicExpression.namespaces) 
            element.setAttribute("xmlns:" + ns.prefix, ns.namespace);
        

        return element;
    

    @Override
    public TopicExpressionType unmarshal(Element arg0) throws Exception 
        if (arg0.getFirstChild() instanceof Text text) 
            var expression = text.getData();
            if (expression == null || expression.isBlank())
                throw new TopicExpressionAdapterException("Empty content");

            // Extract the prefixes from the expression
            var namespacePrefixes = new ArrayList<String>();
            getNamespacePrefixes(expression, namespacePrefixes);

            //Now get the namespaces for the prefixes
            var nsMap = new ArrayList<TopicExpressionType.Namespace>();
            for (var prefix : namespacePrefixes) 
                var namespace = arg0.getAttribute("xmlns:" + prefix);
                if (namespace == null || namespace.isBlank())
                    throw new TopicExpressionAdapterException("Missing namespace declaration for the following prefix: " + prefix);
                nsMap.add(new TopicExpressionType.Namespace(prefix, namespace));
            

            var dialect = arg0.getAttribute("Dialect");

            if (dialect == null || dialect.isBlank())
                throw new TopicExpressionAdapterException("Missing Dialect attribute");

            return new TopicExpressionType(dialect, expression, nsMap);
         else 
            throw new TopicExpressionAdapterException("Unexpected child element type: " + arg0.getFirstChild().getClass().getName());
        

    

    public static class TopicExpressionAdapterException extends Exception 
        public TopicExpressionAdapterException(String message) 
            super(message);
        
    

注意:此答案有意省略了 getNamespacePrefixes() 方法的实现。

最后一步是在 JAXB 生成的类中使用 TopicExpressionType 的任何地方添加以下注释:

@XmlJavaTypeAdapter(TopicExpressionTypeAdapter.class)
TopicExpressionType topicExpression;

【讨论】:

以上是关于将 QName 作为字符串添加到 @XmlMixed@XmlAnyElement(lax = true) 列表的主要内容,如果未能解决你的问题,请参考以下文章

Worklight javax/xml/名称空间/QName

QName

QName

Java SAXParser:`localName` 和 `qName` 之间的区别

如何将子元素作为解码字符串添加到 XElement?

节点:将字符串作为文件添加到 tar 存档中(没有临时文件)