javax.xml.transform.Transformer行尾不再遵守系统属性“ line.separator”

时间:2018-12-03 14:51:14

标签: java xml pretty-print

How to control line endings that javax.xml.transform.Transformer creates?处的注释建议设置系统属性“ line.separator”。在Java 8(Oracle JDK 1.8.0_171)中,这对我有用(并且对我的任务而言是可以接受的),但是在Java 11(openjdk 11.0.1)中却没有。

我从票证XALANJ-2137中做出了一个尝试(未受教育,因为我什至不知道我在使用哪个javax.xml实现),尝试尝试setOutputProperty("{http://xml.apache.org/xslt}line-separator", ..)或也许是setOutputProperty("{http://xml.apache.org/xalan}line-separator", ..),但是都没有有效。

如何在Java 11中控制转换器的换行符?

这里有一些演示代码,在带有Java 11的Windows下打印“ ...#13#10 ...”。

package test.xml;

import java.io.StringReader;
import java.io.StringWriter;
import java.util.stream.Collectors;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;


public class TestXmlTransformerLineSeparator {
    public static void main(String[] args) throws Exception {
        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><foo/></root>";
        final String lineSep = "\n";

        String oldLineSep = System.setProperty("line.separator", lineSep);
        try {
            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
            transformer.setOutputProperty("{http://xml.apache.org/xalan}line-separator", lineSep);
            transformer.setOutputProperty("{http://xml.apache.org/xslt}line-separator", lineSep);

            StreamSource source = new StreamSource(new StringReader(xml));
            StringWriter writer = new StringWriter();
            StreamResult target = new StreamResult(writer);

            transformer.transform(source, target);

            System.out.println(writer.toString().chars().mapToObj(c -> c <= ' ' ? "#" + c : "" + Character.valueOf((char) c))
                    .collect(Collectors.joining(" ")));
            System.out.println(writer);
        } finally {
            System.setProperty("line.separator", oldLineSep);
        }
    }
}

1 个答案:

答案 0 :(得分:1)

据我所知,控制Transformer接口的默认Java实现在Java 11中使用的行分隔符的唯一方法是在Java命令行上设置line.separator属性。对于此处的简单示例程序,您可以通过创建一个名为javaArgs reading的文本文件来做到这一点

-Dline.separator="\n"

并使用命令行执行程序

java @javaArgs TestXmlTransformerLineSeparator

在Java 9中引入的@语法在这里很有用,因为@-文件的解析方式会将“ \ n”转换为LF行分隔符。不用@文件也可以完成相同的事情,但是我所知道的唯一方法是需要更复杂的OS依赖语法来定义一个包含所需行分隔符的变量,并使用java命令行扩展该变量。

如果所需的行分隔符为CRLF,则javaArgs文件将改为读取

-Dline.separator="\r\n"

在较大的程序中,更改整个应用程序的line.separator变量可能是不可接受的。为了避免为整个应用程序设置line.separator,可以使用刚刚讨论过的命令行启动一个单独的Java进程,但是启动该进程以及与该单独的进程进行通信以传输{{ 1}}应该写到流中可能会使该解决方案不受欢迎。

因此,实际上,一个更好的解决方案可能是实现Transformer来过滤输出流,以将行分隔符转换为所需的行分隔符。此解决方案不会更改变压器本身中使用的行分隔符,并且可能会被视为对变压器的结果进行后处理,因此在某种意义上,它并不是您所要解决的特定问题的答案,但我认为它确实可以提供所需的结果很多开销。这是一个示例,该示例使用FilterWriter从输出编写器中删除所有CR字符(即回车)。

FilterWriter

解决XML序列化问题的另一种实用解决方案是通过使用import java.io.FilterWriter; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.util.stream.Collectors; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; public class TransformWithFilter { private static class RemoveCRFilterWriter extends FilterWriter { RemoveCRFilterWriter(Writer wrappedWriter) { super(wrappedWriter); } @Override public void write(int c) throws IOException { if (c != (int)('\r')) { super.write(c); } } @Override public void write(char[] cbuf, int offset, int length) throws IOException { int localOffset = offset; for (int i = localOffset; i < offset + length; ++i) { if (cbuf[i] == '\r') { if (i > localOffset) { super.write(cbuf, localOffset, i - localOffset); } localOffset = i + 1; } } if (localOffset < offset + length) { super.write(cbuf, localOffset, offset + length - localOffset); } } @Override public void write(String str, int offset, int length) throws IOException { int localOffset = offset; for (int i = localOffset; i < offset + length; ++i) { if (str.charAt(i) == '\r') { if (i > localOffset) { super.write(str, localOffset, i - localOffset); } localOffset = i + 1; } } if (localOffset < offset + length) { super.write(str, localOffset, offset + length - localOffset); } } } public static void main(String[] args) throws Exception { String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root><foo/></root>"; TransformerFactory transformerFactory = TransformerFactory.newInstance(); Transformer transformer = transformerFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); StreamSource source = new StreamSource(new StringReader(xml)); StringWriter stringWriter = new StringWriter(); FilterWriter writer = new RemoveCRFilterWriter(stringWriter); StreamResult target = new StreamResult(writer); transformer.transform(source, target); System.out.println(stringWriter.toString().chars().mapToObj(c -> c <= ' ' ? "#" + c : "" + Character.valueOf((char) c)) .collect(Collectors.joining(" "))); System.out.println(stringWriter); } } 获得Transformer或通过直接解析为DOM并写出DOM来获取XML的DOM表示形式。 DOMResult,它为设置行分隔符提供了明确的支持。由于该方法不再使用LSSerializer,并且在Stack Overflow上还有其他示例,因此在此不再赘述。

但是,可能有用的是回顾Java 11中的更改​​以及为什么我认为没有其他方法来控制Java的Transformer默认实现所使用的行分隔符。 Java的Transformer接口的默认实现使用从Transformer继承并在同一包中实现的ToXMLStream类。回顾OpenJDK的提交历史,我发现here已从com.sun.org.apache.xml.internal.serializer.ToStream更改为here,从读取系统属性中当前定义的src/java.xml/share/classes/com/sun/org/apache/xml/internal/serializer/ToStream.java属性改为读取line.separator,这对应于Java虚拟机初始化时的行分隔符。该提交最初是在Java 11中发布的,因此问题中的代码的行为应与Java 8(包括Java 10)相同。

如果您花了一些时间阅读System.lineSeparator(),因为它在提交后改变了行分隔符的读取方式(可访问here),特别是着眼于第135至140和508至514行,将注意到,序列化程序实现确实支持使用其他行分隔符,实际上,输出属性标识为

ToStream.java

应该是控制使用哪个行分隔符的一种方法。

那么问题中的示例为什么不起作用?答案:在{http://xml.apache.org/xalan}line-separator 接口的当前Java默认实现中,仅用户设置的某些特定属性被传输到序列化程序。这些主要是XSLT规范中定义的属性,但是特殊的Transformer属性也会被传送。但是,行分隔符的输出属性不是转移到序列化程序的属性之一。

使用indent-amount在Transformer本身上明确设置的输出属性通过setOutputProperty的第1029-1128行定义的setOutputProperties方法(可访问{{3} }。如果改为定义一个显式XSLT转换并使用其com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl标记来设置输出属性,则首先通过第139-312行中定义的<xsl:output>方法对传输到序列化程序的属性进行过滤的parseContents(可访问的here)中进行过滤,并使用com.sun.org.apache.xalan.internal.xsltc.compiler.Output的第671-715行(可访问的here)中定义的transferOutputSettings方法再次进行过滤。

总而言之,似乎没有可以在com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet接口的默认Java实现上设置的输出属性来控制它使用的行分隔符。可能还有其他Transformer实现的提供程序确实提供了对行分隔符的控制,但是我没有Java 11中Transformer接口的任何实现的经验,只有随附的默认实现OpenJDK版本。