代码之家  ›  专栏  ›  技术社区  ›  Fernando Miguélez

DOM处理后XML属性的顺序

  •  47
  • Fernando Miguélez  · 技术社区  · 17 年前

    当使用标准DOM处理XML时,在序列化后无法保证属性顺序。最后,当我使用标准的javaXMLTransformneneneba API来序列化输出时,我实现了这一点。

    更新

    不同。目标是一个包含数百个警报的配置文件(每个警报都由一组属性定义)。随着时间的推移,此文件通常很少有修改,但保持有序很方便,因为当我们需要修改某些内容时,它是手动编辑的。有时,一些项目需要对此文件进行轻微修改,例如将其中一个属性设置为客户特定的代码。

    我刚刚开发了一个小应用程序,将原始文件(所有项目通用)与每个项目的特定部分合并(修改某些属性的值),因此特定于项目的文件会得到基本文件的更新(新的警报定义或一些属性值错误修复)。我要求有序属性的主要动机是能够通过文本比较工具(如Winmerge)重新检查应用程序的输出与原始文件。如果格式(主要是属性顺序)保持不变,则很容易发现差异。

    我真的认为这是可能的,因为XML处理程序,如XMLSpy,允许您编辑XML文件并应用一些排序(网格模式)。也许我唯一的选择就是使用这些程序之一 手动地 修改输出文件。

    12 回复  |  直到 17 年前
        1
  •  29
  •   John Saunders    12 年前

    很抱歉,但答案比“不,你不能”或“你为什么一开始就需要这样做?”更微妙。

    <xsl:copy*> .

    -Xalan-C:如果你按字面意思插入结果树标签(例如。 <elem att1={@att1} .../>

    SAXParserFactory spf = SAXParserFactoryImpl.newInstance();
    spf.setNamespaceAware(true);
    spf.setValidating(false);
    spf.setFeature("http://xml.org/sax/features/validation", false);
    spf.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
    spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
    SAXParser sp = spf.newSAXParser() ;
    Source src = new SAXSource ( sp.getXMLReader(), new InputSource( input.getAbsolutePath() ) ) ;
    String resultFileName = input.getAbsolutePath().replaceAll(".xml$", ".cooked.xml" ) ;
    Result result = new StreamResult( new File (resultFileName) ) ;
    TransformerFactory tf = TransformerFactory.newInstance();
    Source xsltSource = new StreamSource( new File ( COOKER_XSL ) );
    xsl = tf.newTransformer( xsltSource ) ;
    xsl.setParameter( "srcDocumentName", input.getName() ) ;
    xsl.setParameter( "srcDocumentPath", input.getAbsolutePath() ) ;
    
    xsl.transform(src, result );
    

    是 属性顺序的情况 做

    回归测试就是一个明显的例子。 在这些情况下,保持属性顺序非常有帮助。

        2
  •  24
  •   Robert Rossney    17 年前

    如果一个软件要求XML元素上的属性按特定顺序出现,那么该软件不是在处理XML,而是在处理表面上看起来像XML的文本。它需要被修复。

    如果无法修复,并且必须生成符合其要求的文件,则无法可靠地使用标准XML工具生成这些文件。例如,您可以尝试(如您所建议的)使用XSLT按定义的顺序生成属性,例如:

    <test>
       <xsl:attribute name="foo"/>
       <xsl:attribute name="bar"/>
       <xsl:attribute name="baz"/>
    </test>
    

    结果发现XSLT处理器发出以下命令:

    <test bar="" baz="" foo=""/>
    

    因为处理器使用的DOM按标签名的字母顺序排列属性。(这是XMLDOM中常见但不普遍的行为。)

        3
  •  8
  •   Jon Hanna    16 年前

        4
  •  7
  •   John Saunders    17 年前

    我们应该强烈反对任何编写不按照标准处理XML的代码的人。我明白,在这个经济时代,人们不愿意说“不”来冒犯客户和商业伙伴。但在这种情况下,我认为这是值得的。如果我们必须为每个业务伙伴手工制作XML,我们的财务状况会糟糕得多。

    因此,不要“启用”不了解XML的公司。向他们发送标准,并突出显示相应的行。他们需要停止认为XML只是带尖括号的文本。它根本不像带尖括号文本。

        5
  •  3
  •   Community Mohan Dere    9 年前

    • 您可能需要与一些(承认错误地)关心订单的工具或服务进行沟通;要求提供商更正其代码可能不是一种选择:当你的用户以电子方式提交一堆财政文件的最后期限越来越近时,试着向政府机构提出要求!

    Alain Pannetier's solution 这是我们应该走的路。

    此外,您可能想看看 DecentXML ;它使您可以完全控制XML的格式,即使它与DOM不兼容。如果你想在不丢失格式的情况下修改一些手工编辑的XML,这特别有用。

        6
  •  2
  •   BenMorel Manish Pradhan    12 年前

    我也遇到了同样的问题。我想修改XML属性,但由于存在差异,我想保持顺序 StAX

    看看这个 article/discussion 。您可以看到如何按顺序读取开始元素的属性。

        7
  •  0
  •   Soviut    17 年前

    您仍然可以使用标准的DOM和Transformation API来完成这项工作,方法是使用一个快速而肮脏的解决方案,如我所描述的解决方案:

    我们知道转换API解决方案按字母顺序排列属性。您可以在属性名称前添加一些易于删除的字符串,以便它们按照您想要的顺序输出。在大多数情况下,简单的前缀如“a_”、“b_”等应该就足够了,并且可以使用单行正则表达式轻松地从输出xml中删除。

    如果您正在加载xml并重新保存,并且希望保留属性顺序,则可以使用相同的原理,首先修改输入xml文本中的属性名称,然后将其解析为Document对象。再次,基于xml的文本处理进行此修改。这可能很棘手,但可以通过使用正则表达式检测元素及其属性字符串来完成。请注意,这是一个肮脏的解决方案。自己解析XML时有很多陷阱,即使是对于像这样简单的事情,所以如果你决定实现它,也要小心。

        8
  •  0
  •   svick Raja Nadar    14 年前

        9
  •  0
  •   Radu Simionescu    11 年前

    我能想到至少两个原因,你可能会关心属性排序。可能还有其他原因,但至少对于这两个原因我可以提出替代方案:

    1. 您正在使用多个同名属性实例:

      <foo myAttribute="a" myAttribute="b" myAttribute="c"/>
      

      这只是一个完全无效的XML;如果DOM处理器处理文档,它可能会删除这些值中的所有值,只保留一个。相反,您希望使用子元素:

      <foo>
          <myChild="a"/>
          <myChild="b"/>
          <myChild="c"/>
      </foo>
      
    2. 您假设某种区别适用于首先出现的属性。通过其他属性或子元素使其明确。例如:

      <foo attr1="a" attr2="b" attr3="c" theMostImportantAttribute="attr1" />
      
        10
  •  0
  •   Andrey Lebedenko    11 年前

    package mynewpackage;
    
    // for the method
    import java.lang.reflect.Constructor;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Comparator;
    import java.util.List;
    import org.w3c.dom.Element;
    import org.w3c.dom.Node;
    import org.w3c.dom.NodeList;
    
    // for the test example
    import org.xml.sax.InputSource;
    import javax.xml.parsers.DocumentBuilder;
    import javax.xml.parsers.DocumentBuilderFactory;
    import java.io.StringReader;
    import org.w3c.dom.Document;
    import java.math.BigDecimal;
    
    public class NodeTools {
        /**
         * Method sorts any NodeList by provided attribute.
         * @param nl NodeList to sort
         * @param attributeName attribute name to use
         * @param asc true - ascending, false - descending
         * @param B class must implement Comparable and have Constructor(String) - e.g. Integer.class , BigDecimal.class etc
         * @return 
         */
        public static Node[] sortNodes(NodeList nl, String attributeName, boolean asc, Class<? extends Comparable> B)
        {        
            class NodeComparator<T> implements Comparator<T>
            {
                @Override
                public int compare(T a, T b)
                {
                    int ret;
                    Comparable bda = null, bdb = null;
                    try{
                        Constructor bc = B.getDeclaredConstructor(String.class);
                        bda = (Comparable)bc.newInstance(((Element)a).getAttribute(attributeName));
                        bdb = (Comparable)bc.newInstance(((Element)b).getAttribute(attributeName));
                    }
                    catch(Exception e)
                    {
                        return 0; // yes, ugly, i know :)
                    }
                    ret = bda.compareTo(bdb);
                    return asc ? ret : -ret; 
                }
            }
    
            List<Node> x = new ArrayList<>();
            for(int i = 0; i < nl.getLength(); i++)
            {
                x.add(nl.item(i));
            }
            Node[] ret = new Node[x.size()];
            ret = x.toArray(ret);
            Arrays.sort(ret, new NodeComparator<Node>());
            return ret;
        }    
    
        public static void main(String... args)
        {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();  
            DocumentBuilder builder;
            String s = "<xml><item id=\"1\" price=\"100.00\" /><item id=\"3\" price=\"29.99\" /><item id=\"2\" price=\"5.10\" /></xml>";
            Document doc = null;
            try 
            {  
                builder = factory.newDocumentBuilder();  
                doc = builder.parse(new InputSource(new StringReader(s)));
            }
            catch(Exception e) { System.out.println("Alarm "+e); return; }
    
            System.out.println("*** Sort by id ***");
            Node[] ret = NodeTools.sortNodes(doc.getElementsByTagName("item"), "id", true, Integer.class);
    
            for(Node n: ret)
            {
                System.out.println(((Element)n).getAttribute("id")+" : "+((Element)n).getAttribute("price"));
            }
    
            System.out.println("*** Sort by price ***");
            ret = NodeTools.sortNodes(doc.getElementsByTagName("item"), "price", true, BigDecimal.class);
            for(Node n: ret)
            {
                System.out.println(((Element)n).getAttribute("id")+" : "+((Element)n).getAttribute("price"));
            }
        }
    }
    

    在我的简单测试中,它打印:

    *** Sort by id ***
    1 : 100.00
    2 : 5.10
    3 : 29.99
    *** Sort by price ***
    2 : 5.10
    3 : 29.99
    1 : 100.00
    
        11
  •  -1
  •   Roberto Taschetto    11 年前

    受到以下答案的启发 Andrey Lebedenko .

    已准备好在XML实用程序类中使用。

    public static Collection<Node> nodeListCollection(final NodeList nodeList) {
      if (nodeList == null) {
        return Collections.emptyList();
      }
      final int length = nodeList.getLength();
      if (length == 0) {
        return Collections.emptyList();
      }
      return IntStream.range(0, length)
          .mapToObj(nodeList::item)
          .collect(Collectors.toList());
    }
    
    private static int compareString(final String str1, final String str2, final boolean nullIsLess) {
      if (Objects.equals(str1, str2)) { 
        return 0;
      }
      if (str1 == null) {
        return nullIsLess ? -1 : 1;
      }
      if (str2 == null) {
        return nullIsLess ? 1 : -1;
      }
      return str1.compareTo(str2);
    }
    
    private static final Function<Boolean, Comparator<Node>> StringNodeValueComparatorSupplier = (asc) ->
        (Node a, Node b) -> {
          final String va = a == null ? null : a.getTextContent();
          final String vb = b == null ? null : b.getTextContent();
          return (asc ? 1 : -1) * compareString(va, vb,asc);
        };
    
    private static final BiFunction<Boolean, String, Comparator<Node>> StringNodeAttributeComparatorSupplier = (asc, attrName) ->
        (Node a, Node b) -> {
          final String va = a == null ? null : a.hasAttributes() ?
              ((Element) a).getAttribute(attrName) : null;
          final String vb = b == null ? null : b.hasAttributes() ?
              ((Element) b).getAttribute(attrName) : null;
          return (asc ? 1 : -1) * compareString(va, vb,asc);
        };
    
    private static <T extends Comparable<T>> Comparator<Node> nodeComparator(
        final boolean asc,
        final boolean useAttr,
        final String attribute,
        final Constructor<T> constructor
    ) {
      return (Node a, Node b) -> {
        if (a == null && b == null) {
          return 0;
        } else if (a == null) {
          return (asc ? -1 : 1);
        } else if (b == null) {
          return (asc ? 1 : -1);
        }
    
        T aV;
        try {
          final String aStr;
          if (useAttr) {
            aStr = a.hasAttributes() ? ((Element) a).getAttribute(attribute) : null;
          } else {
            aStr = a.getTextContent();
          }
          aV = aStr == null || aStr.matches("\\s+") ? null : constructor.newInstance(aStr);
        } catch (Exception ignored) {
          aV = null;
        }
    
        T bV;
        try {
          final String bStr;
          if (useAttr) {
            bStr = b.hasAttributes() ? ((Element) b).getAttribute(attribute) : null;
          } else {
            bStr = b.getTextContent();
          }
          bV = bStr == null || bStr.matches("\\s+") ? null : constructor.newInstance(bStr);
        } catch (Exception ignored) {
          bV = null;
        }
    
        final int ret;
        if (aV == null && bV == null) {
          ret = 0;
        } else if (aV == null) {
          ret = -1;
        } else if (bV == null) {
          ret = 1;
        } else {
          ret = aV.compareTo(bV);
        }
        return (asc ? 1 : -1) * ret;
      };
    }
    
    
    /**
     * Method to sort any NodeList by an attribute all nodes must have. <br>If the attribute is absent for a signle
     * {@link Node} or the {@link NodeList} does contain elements without Attributes, null is used instead. <br>If
     * <code>asc</code> is
     * <code>true</code>, nulls first, else nulls last.
     *
     * @param nodeList The {@link NodeList} containing all {@link Node} to sort.
     * @param attribute Name of the attribute to extract and compare
     * @param asc <code>true</code>: ascending, <code>false</code>: descending
     * @param compareType Optional class to use for comparison. Must implement {@link Comparable} and have Constructor
     * that takes a single {@link String} argument. If <code>null</code> is supplied, {@link String} is used.
     * @return A collection of the {@link Node}s passed as {@link NodeList}
     * @throws RuntimeException If <code>compareType</code> does not have a constructor taking a single {@link String}
     * argument. Also, if the comparator created does violate the {@link Comparator} contract, an
     * {@link IllegalArgumentException} is raised.
     * @implNote Exceptions during calls of the single String argument constructor of <code>compareType</code> are
     * ignored. Values are substituted by <code>null</code>
     */
    public static <T extends Comparable<T>> Collection<Node> sortNodesByAttribute(
        final NodeList nodeList,
        String attribute,
        boolean asc,
        Class<T> compareType) {
    
      final Comparator<Node> nodeComparator;
      if (compareType == null) {
        nodeComparator = StringNodeAttributeComparatorSupplier.apply(asc, attribute);
      } else {
        final Constructor<T> constructor;
        try {
          constructor = compareType.getDeclaredConstructor(String.class);
        } catch (NoSuchMethodException e) {
          throw new RuntimeException(
              "Cannot compare Node Attribute '" + attribute + "' using the Type '" + compareType.getName()
                  + "': No Constructor available that takes a single String argument.", e);
        }
        nodeComparator = nodeComparator(asc, true, attribute, constructor);
      }
      final List<Node> nodes = new ArrayList<>(nodeListCollection(nodeList));
      nodes.sort(nodeComparator);
      return nodes;
    }
    
    /**
     * Method to sort any NodeList by their text content using an optional type. <br>If
     * <code>asc</code> is
     * <code>true</code>, nulls first, else nulls last.
     *
     * @param nodeList The {@link NodeList} containing all {@link Node}s to sort.
     * @param asc <code>true</code>: ascending, <code>false</code>: descending
     * @param compareType Optional class to use for comparison. Must implement {@link Comparable} and have Constructor
     * that takes a single {@link String} argument. If <code>null</code> is supplied, {@link String} is used.
     * @return A collection of the {@link Node}s passed as {@link NodeList}
     * @throws RuntimeException If <code>compareType</code> does not have a constructor taking a single {@link String}
     * argument. Also, if the comparator created does violate the {@link Comparator} contract, an
     * {@link IllegalArgumentException} is raised.
     * @implNote Exceptions during calls of the single String argument constructor of <code>compareType</code> are
     * ignored. Values are substituted by <code>null</code>
     */
    public static <T extends Comparable<T>> Collection<Node> sortNodes(
        final NodeList nodeList,
        boolean asc,
        Class<T> compareType) {
    
      final Comparator<Node> nodeComparator;
      if (compareType == null) {
        nodeComparator = StringNodeValueComparatorSupplier.apply(asc);
      } else {
        final Constructor<T> constructor;
        try {
          constructor = compareType.getDeclaredConstructor(String.class);
        } catch (NoSuchMethodException e) {
          throw new RuntimeException(
              "Cannot compare Nodes using the Type '" + compareType.getName()
                  + "': No Constructor available that takes a single String argument.", e);
        }
        nodeComparator = nodeComparator(asc, false, null, constructor);
      }
      final List<Node> nodes = new ArrayList<>(nodeListCollection(nodeList));
      nodes.sort(nodeComparator);
      return nodes;
    }