diff --git a/org.jhotdraw8.svg/src/main/java/org.jhotdraw8.svg/org/jhotdraw8/svg/io/AbstractFXSvgWriter.java b/org.jhotdraw8.svg/src/main/java/org.jhotdraw8.svg/org/jhotdraw8/svg/io/AbstractFXSvgWriter.java index f419870ae..6d1c367f0 100755 --- a/org.jhotdraw8.svg/src/main/java/org.jhotdraw8.svg/org/jhotdraw8/svg/io/AbstractFXSvgWriter.java +++ b/org.jhotdraw8.svg/src/main/java/org.jhotdraw8.svg/org/jhotdraw8/svg/io/AbstractFXSvgWriter.java @@ -462,7 +462,8 @@ public Document toDocument(@NonNull Node drawingNode, @Nullable CssDimension2D s builder.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader(""))); Document doc = builder.newDocument(); DOMResult result = new DOMResult(doc); - XMLStreamWriter w = XMLOutputFactory.newInstance().createXMLStreamWriter(result); + XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newInstance(); + XMLStreamWriter w = xmlOutputFactory.createXMLStreamWriter(result); writeDocument(w, drawingNode, size); w.close(); return doc; @@ -569,7 +570,7 @@ private void writeClassAttribute(@NonNull XMLStreamWriter w, @NonNull Node node) if (!styleClass.isEmpty()) { StringBuilder buf = new StringBuilder(); for (String clazz : styleClass) { - if (buf.length() != 0) { + if (!buf.isEmpty()) { buf.append(' '); } buf.append(clazz); diff --git a/org.jhotdraw8.svg/src/test/java/org.jhotdraw8.svg/io/FXSvgTinyWriterTest.java b/org.jhotdraw8.svg/src/test/java/org.jhotdraw8.svg/io/FXSvgTinyWriterTest.java index 85c7fcc4f..baa3eff08 100644 --- a/org.jhotdraw8.svg/src/test/java/org.jhotdraw8.svg/io/FXSvgTinyWriterTest.java +++ b/org.jhotdraw8.svg/src/test/java/org.jhotdraw8.svg/io/FXSvgTinyWriterTest.java @@ -28,20 +28,23 @@ public class FXSvgTinyWriterTest { public @NonNull List dynamicTestsExportToWriter() { return Arrays.asList( dynamicTest("rect", () -> testExportToWriter(new Rectangle(10, 20, 100, 200), - "\n" + - "\n" + - " \n" + - "")), + """ + + + + """)), dynamicTest("text", () -> testExportToWriter(new Text(10, 20, "Hello"), - "\n" + - "\n" + - " Hello\n" + - "")), + """ + + + Hello + """)), dynamicTest("text escape", () -> testExportToWriter(new Text(10, 20, "&<>\""), - "\n" + - "\n" + - " &<>\"\n" + - "")) + """ + + + &<>" + """)) ); } @@ -49,20 +52,23 @@ public class FXSvgTinyWriterTest { public @NonNull List dynamicTestsExportToDOM() { return Arrays.asList( dynamicTest("rect", () -> testExportToDOM(new Rectangle(10, 20, 100, 200), - "\n" + - "\n" + - " \n" + - "\n")), + """ + + + + """)), dynamicTest("text", () -> testExportToDOM(new Text(10, 20, "Hello"), - "\n" + - "\n" + - " Hello\n" + - "\n")), + """ + + + Hello + """)), dynamicTest("text escape", () -> testExportToWriter(new Text(10, 20, "&<>\""), - "\n" + - "\n" + - " &<>\"\n" + - "")) + """ + + + &<>" + """)) ); } diff --git a/org.jhotdraw8.xml/src/main/java/org.jhotdraw8.xml/org/jhotdraw8/xml/IndentingXMLStreamWriter.java b/org.jhotdraw8.xml/src/main/java/org.jhotdraw8.xml/org/jhotdraw8/xml/IndentingXMLStreamWriter.java index 3b42cd9bd..c3cddb262 100644 --- a/org.jhotdraw8.xml/src/main/java/org.jhotdraw8.xml/org/jhotdraw8/xml/IndentingXMLStreamWriter.java +++ b/org.jhotdraw8.xml/src/main/java/org.jhotdraw8.xml/org/jhotdraw8/xml/IndentingXMLStreamWriter.java @@ -26,9 +26,12 @@ import java.util.Deque; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.SortedSet; import java.util.TreeSet; /** @@ -273,13 +276,13 @@ public class IndentingXMLStreamWriter implements XMLStreamWriter, AutoCloseable private static final String XML_SPACE_PRESERVE_VALUE = "preserve"; private static final String XMLNS_NAMESPACE = "https://www.w3.org/TR/REC-xml-names/"; private String indentation = " "; - private static final String LINE_BREAK = "\n"; + private String lineSeparator = "\n"; private final Writer w; /** * Invariant: this stack always contains at least the root element. */ private final Deque stack = new ArrayDeque<>(); - private final TreeSet attributes = new TreeSet<>(Comparator.comparing(Attribute::getNamespace).thenComparing(Attribute::getLocalName)); + private Set attributes = new TreeSet<>(Comparator.comparing(Attribute::getNamespace).thenComparing(Attribute::getLocalName)); private final CharsetEncoder encoder; private boolean isStartTagOpen = false; private boolean escapeClosingAngleBracket = true; @@ -311,6 +314,15 @@ public String getIndentation() { return indentation; } + public void setSortAttributes(boolean b) { + attributes = b ? new TreeSet<>(Comparator.comparing(Attribute::getNamespace).thenComparing(Attribute::getLocalName)) + : new LinkedHashSet<>(); + } + + public boolean isSortAttributes() { + return attributes instanceof SortedSet; + } + /** * Whether to replace {@literal '<'} and {@literal '>} characters by * entity references. @@ -332,6 +344,14 @@ public void setIndentation(String indentation) { this.indentation = indentation; } + public String getLineSeparator() { + return lineSeparator; + } + + public void setLineSeparator(String lineSeparator) { + this.lineSeparator = lineSeparator; + } + private void closeStartTagOrCloseEmptyElemTag() throws XMLStreamException { charBuffer.setLength(0); if (isStartTagOpen) { @@ -548,7 +568,7 @@ public void writeCharacters(@NonNull String text) throws XMLStreamException { return; } else { setHasContent(true); - if (charBuffer.length() > 0) { + if (!charBuffer.isEmpty()) { writeXmlContent(charBuffer.toString(), false, false); charBuffer.setLength(0); } @@ -567,7 +587,7 @@ public void writeCharacters(char @NonNull [] text, int start, int len) throws XM return; } else { setHasContent(true); - if (charBuffer.length() > 0) { + if (!charBuffer.isEmpty()) { writeXmlContent(charBuffer.toString(), false, false); charBuffer.setLength(0); } @@ -685,7 +705,7 @@ public void writeEndElement() throws XMLStreamException { } private void writeEndElementLineBreakAndIndentation() throws XMLStreamException { - write(LINE_BREAK); + write(lineSeparator); for (int i = stack.size() - 2; i >= 0; i--) { write(indentation); } @@ -701,7 +721,7 @@ public void writeEntityRef(String name) throws XMLStreamException { } private void writeLineBreakAndIndentation() throws XMLStreamException { - write(LINE_BREAK); + write(lineSeparator); for (int i = stack.size() - 3; i >= 0; i--) { write(indentation); } @@ -712,7 +732,7 @@ public void writeNamespace(@NonNull String prefix, @NonNull String namespaceURI) Objects.requireNonNull(prefix, "prefix"); Objects.requireNonNull(namespaceURI, "namespaceURI"); requireStartTagOpened(); - attributes.add(new Attribute(prefix.isEmpty() ? "" : XMLNS_PREFIX, + attributes.add(new Attribute(prefix.isEmpty() || XMLNS_PREFIX.equals(prefix) ? "" : XMLNS_PREFIX, XMLNS_NAMESPACE, prefix.isEmpty() ? XMLNS_PREFIX : prefix, namespaceURI)); } diff --git a/org.jhotdraw8.xml/src/main/java/org.jhotdraw8.xml/org/jhotdraw8/xml/XmlUtil.java b/org.jhotdraw8.xml/src/main/java/org.jhotdraw8.xml/org/jhotdraw8/xml/XmlUtil.java index a7910531e..8b8fbf178 100755 --- a/org.jhotdraw8.xml/src/main/java/org.jhotdraw8.xml/org/jhotdraw8/xml/XmlUtil.java +++ b/org.jhotdraw8.xml/src/main/java/org.jhotdraw8.xml/org/jhotdraw8/xml/XmlUtil.java @@ -39,6 +39,7 @@ import javax.xml.transform.dom.DOMResult; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stax.StAXResult; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; @@ -52,6 +53,7 @@ import java.io.Writer; import java.net.URI; import java.net.URL; +import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -72,10 +74,17 @@ public class XmlUtil { private static final String SEPARATOR = "\0"; private static final Properties DEFAULT_PROPERTIES = new Properties(); + public static final String HTTP_XML_APACHE_ORG_XALAN_LINE_SEPARATOR = "{http://xml.apache.org/xalan}line-separator"; + + public static final String HTTP_XML_APACHE_ORG_XSLT_INDENT_AMOUNT = "{http://xml.apache.org/xslt}indent-amount"; + + public static final String CANONICAL_LINE_SEPARATOR = "\n"; + static { DEFAULT_PROPERTIES.put(OutputKeys.INDENT, "yes"); DEFAULT_PROPERTIES.put(OutputKeys.ENCODING, "UTF-8"); - DEFAULT_PROPERTIES.put("{http://xml.apache.org/xslt}indent-amount", "2"); + DEFAULT_PROPERTIES.put(HTTP_XML_APACHE_ORG_XSLT_INDENT_AMOUNT, "2"); + DEFAULT_PROPERTIES.put(HTTP_XML_APACHE_ORG_XALAN_LINE_SEPARATOR, CANONICAL_LINE_SEPARATOR); } private XmlUtil() { @@ -246,12 +255,16 @@ public static void write(@NonNull Path out, Document doc, Properties outputPrope write(result, doc, outputProperties); } - public static void write(Result result, Document doc) throws IOException { + public static void write(@NonNull Result result, @NonNull Document doc) throws IOException { write(result, doc, DEFAULT_PROPERTIES); } - public static void write(Result result, Document doc, @Nullable Properties outputProperties) throws IOException { + public static void write(@NonNull Result result, @NonNull Document doc, @Nullable Properties outputProperties) throws IOException { try { + // We replace the StreamResult by a StAXResult, + // because with a StreamResult we would produce platform-dependent line-breaks. + result = replaceStreamResultByStAXResult(result, outputProperties); + final TransformerFactory factory = TransformerFactory.newInstance(); Transformer t = factory.newTransformer(); if (outputProperties != null) { @@ -264,6 +277,27 @@ public static void write(Result result, Document doc, @Nullable Properties outpu } } + private static Result replaceStreamResultByStAXResult(@NonNull Result result, @Nullable Properties outputProperties) { + if (result instanceof StreamResult sr) { + IndentingXMLStreamWriter w; + if (sr.getOutputStream() != null) + w = new IndentingXMLStreamWriter(sr.getOutputStream(), Charset.forName((String) outputProperties.getOrDefault(OutputKeys.ENCODING, "UTF-8"))); + else { + w = new IndentingXMLStreamWriter(sr.getWriter()); + } + //w.setSortAttributes(false); + try { + int indentation = Integer.parseInt((String) outputProperties.getOrDefault(HTTP_XML_APACHE_ORG_XSLT_INDENT_AMOUNT, "0")); + w.setIndentation(" ".repeat(indentation)); + } catch (NumberFormatException e) { + // bail + } + w.setLineSeparator((String) outputProperties.getOrDefault(HTTP_XML_APACHE_ORG_XALAN_LINE_SEPARATOR, CANONICAL_LINE_SEPARATOR)); + result = new StAXResult(w); + } + return result; + } + /** * Returns a stream which iterates over the subtree starting at the * specified node in preorder sequence. @@ -361,38 +395,38 @@ public static String readNamespaceUri(Source source) throws IOException { for (XMLStreamReader r = dbf.createXMLStreamReader(source); r.hasNext(); ) { int next = r.next(); switch (next) { - case XMLStreamReader.START_ELEMENT: - return r.getNamespaceURI(); - case XMLStreamReader.END_ELEMENT: - return null; - case XMLStreamReader.PROCESSING_INSTRUCTION: - break; - case XMLStreamReader.CHARACTERS: - break; - case XMLStreamReader.COMMENT: - break; - case XMLStreamReader.SPACE: - break; - case XMLStreamReader.START_DOCUMENT: - break; - case XMLStreamReader.END_DOCUMENT: - break; - case XMLStreamReader.ENTITY_REFERENCE: - break; - case XMLStreamReader.ATTRIBUTE: - break; - case XMLStreamReader.DTD: - break; - case XMLStreamReader.CDATA: - break; - case XMLStreamReader.NAMESPACE: - break; - case XMLStreamReader.NOTATION_DECLARATION: - break; - case XMLStreamReader.ENTITY_DECLARATION: - break; - default: - throw new IOException("unsupported XMLStream event: " + next); + case XMLStreamReader.START_ELEMENT: + return r.getNamespaceURI(); + case XMLStreamReader.END_ELEMENT: + return null; + case XMLStreamReader.PROCESSING_INSTRUCTION: + break; + case XMLStreamReader.CHARACTERS: + break; + case XMLStreamReader.COMMENT: + break; + case XMLStreamReader.SPACE: + break; + case XMLStreamReader.START_DOCUMENT: + break; + case XMLStreamReader.END_DOCUMENT: + break; + case XMLStreamReader.ENTITY_REFERENCE: + break; + case XMLStreamReader.ATTRIBUTE: + break; + case XMLStreamReader.DTD: + break; + case XMLStreamReader.CDATA: + break; + case XMLStreamReader.NAMESPACE: + break; + case XMLStreamReader.NOTATION_DECLARATION: + break; + case XMLStreamReader.ENTITY_DECLARATION: + break; + default: + throw new IOException("unsupported XMLStream event: " + next); } } } catch (XMLStreamException e) {