diff --git a/commons-serialize-xml/src/main/java/io/gitlab/jfronny/commons/serialize/xml/NativeXmlWriter.java b/commons-serialize-xml/src/main/java/io/gitlab/jfronny/commons/serialize/xml/NativeXmlWriter.java
index a9b9c4f..6ad344b 100644
--- a/commons-serialize-xml/src/main/java/io/gitlab/jfronny/commons/serialize/xml/NativeXmlWriter.java
+++ b/commons-serialize-xml/src/main/java/io/gitlab/jfronny/commons/serialize/xml/NativeXmlWriter.java
@@ -29,8 +29,8 @@ public class NativeXmlWriter implements Closeable, Flushable {
private String deferredText = null;
private boolean wasText = false;
- private boolean lenient;
- private boolean escapeNonAscii;
+ private boolean lenient = false;
+ private boolean escapeNonAscii = true;
public NativeXmlWriter(Writer out) {
this.out = Objects.requireNonNull(out, "out == null");
newline = indent = "";
@@ -100,6 +100,9 @@ public class NativeXmlWriter implements Closeable, Flushable {
public NativeXmlWriter endTag() throws IOException {
int context = peek();
+ if (context == DANGLING_NAME) {
+ throw new IllegalStateException("Dangling name.");
+ }
if (context != TAG_HEAD && context != TAG_BODY) {
throw new IllegalStateException("Nesting problem.");
}
@@ -204,10 +207,10 @@ public class NativeXmlWriter implements Closeable, Flushable {
public NativeXmlWriter attributeName(String name) throws IOException {
Objects.requireNonNull(name, "name == null");
- wasText = false;
if (peek() != TAG_HEAD) {
throw new IllegalStateException("Nesting problem.");
}
+ wasText = false;
replaceTop(DANGLING_NAME);
out.write(' ');
name(name);
@@ -216,10 +219,10 @@ public class NativeXmlWriter implements Closeable, Flushable {
public NativeXmlWriter attributeValue(String value) throws IOException {
value = value == null ? "null" : value;
- wasText = false;
if (peek() != DANGLING_NAME) {
throw new IllegalStateException("Nesting problem.");
}
+ wasText = false;
replaceTop(TAG_HEAD);
out.write('=');
out.write('"');
@@ -229,12 +232,13 @@ public class NativeXmlWriter implements Closeable, Flushable {
}
public NativeXmlWriter text(String text) throws IOException {
- if (peek() == TAG_HEAD && !text.contains("\n") && deferredComments.isEmpty()) {
+ text = text == null ? "null" : text;
+ if (peek() == TAG_HEAD && !text.contains("\n") && deferredText == null && deferredComments.isEmpty()) {
deferredText = text;
+ wasText = true;
return this;
}
- text = text == null ? "" : text;
- if (wasText) comment("");
+ if (wasText && deferredComments.isEmpty()) deferredComments.add("");
beforeValue();
escapeText(text, true);
wasText = true;
diff --git a/commons-serialize-xml/src/test/java/io/gitlab/jfronny/commons/serialize/xml/test/NativeXmlWriterTest.java b/commons-serialize-xml/src/test/java/io/gitlab/jfronny/commons/serialize/xml/test/NativeXmlWriterTest.java
new file mode 100644
index 0000000..de8f971
--- /dev/null
+++ b/commons-serialize-xml/src/test/java/io/gitlab/jfronny/commons/serialize/xml/test/NativeXmlWriterTest.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.gitlab.jfronny.commons.serialize.xml.test;
+
+import io.gitlab.jfronny.commons.serialize.xml.NativeXmlWriter;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+@SuppressWarnings("resource")
+public final class NativeXmlWriterTest {
+ @Test
+ public void testWriteComments() throws IOException {
+ String expectedJson = """
+
+
+
+ true
+
+
+
+
+
+ false
+
+
+
+
+
+ """;
+
+ StringWriter sw = new StringWriter();
+ NativeXmlWriter jw = new NativeXmlWriter(sw);
+ jw.setLenient(true);
+ jw.setIndent(" ");
+
+ jw.comment("comment at file head")
+ .beginTag("root")
+ .comment("comment directly after context")
+ .beginTag("a")
+ .text("true")
+ .endTag()
+ .comment("comment before context")
+ .beginTag("inner")
+ .attributeName("att")
+ .comment("comment directly after attribute name")
+ .attributeValue("value")
+ .beginTag("b")
+ .attributeName("att")
+ .attributeValue("value")
+ .comment("comment after attribute value")
+ .text("false")
+ .endTag()
+ .beginTag("cnt")
+ .comment("only comment inside tag")
+ .endTag()
+ .endTag()
+ .comment("comment before context end")
+ .endTag()
+ .comment("comment behind the object");
+
+ jw.close();
+ assertThat(sw.toString()).isEqualTo(expectedJson);
+ sw.close();
+ }
+
+ @Test
+ public void testDefaultStrictness() throws IOException {
+ NativeXmlWriter jsonWriter = new NativeXmlWriter(new StringWriter());
+ assertThat(jsonWriter.isLenient()).isEqualTo(false);
+ jsonWriter.text("false");
+ jsonWriter.close();
+ }
+
+ // for NativeXmlWriter.setLenient
+ @Test
+ public void testSetLenientTrue() throws IOException {
+ NativeXmlWriter jsonWriter = new NativeXmlWriter(new StringWriter());
+ jsonWriter.setLenient(true);
+ assertThat(jsonWriter.isLenient()).isEqualTo(true);
+ jsonWriter.text("false");
+ jsonWriter.close();
+ }
+
+ // for NativeXmlWriter.setLenient
+ @Test
+ public void testSetLenientFalse() throws IOException {
+ NativeXmlWriter jsonWriter = new NativeXmlWriter(new StringWriter());
+ jsonWriter.setLenient(false);
+ assertThat(jsonWriter.isLenient()).isEqualTo(false);
+ jsonWriter.text("false");
+ jsonWriter.close();
+ }
+
+ @Test
+ public void testEmptyTag() throws IOException {
+ StringWriter sw = new StringWriter();
+ NativeXmlWriter jw = new NativeXmlWriter(sw);
+ jw.beginTag("empty").endTag();
+ jw.close();
+ assertThat(sw.toString()).isEqualTo("");
+ }
+
+ @Test
+ public void testTopLevelValueTypes() throws IOException {
+ StringWriter string5 = new StringWriter();
+ NativeXmlWriter writert = new NativeXmlWriter(string5);
+ writert.text("a");
+ writert.close();
+ assertThat(string5.toString()).isEqualTo("a");
+ }
+
+ @Test
+ public void testNameWithoutValue() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter jsonWriter = new NativeXmlWriter(stringWriter);
+ jsonWriter.beginTag("tag");
+ jsonWriter.attributeName("a");
+ try {
+ jsonWriter.endTag();
+ fail();
+ } catch (IllegalStateException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("Dangling name.");
+ }
+ }
+
+ @Test
+ public void testValueWithoutName() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter jsonWriter = new NativeXmlWriter(stringWriter);
+ jsonWriter.beginTag("tag");
+ try {
+ jsonWriter.attributeValue("a");
+ fail();
+ } catch (IllegalStateException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("Nesting problem.");
+ }
+ }
+
+ @Test
+ public void testMultipleTopLevelValuesStrict() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter jsonWriter = new NativeXmlWriter(stringWriter);
+ jsonWriter.setLenient(false);
+ jsonWriter.beginTag("tag").endTag();
+
+ IllegalStateException expected =
+ assertThrows(IllegalStateException.class, () -> jsonWriter.beginTag("tag"));
+ assertThat(expected).hasMessageThat().isEqualTo("XML must have only one top-level value.");
+ }
+
+ @Test
+ public void testMultipleTopLevelValuesLenient() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter writer = new NativeXmlWriter(stringWriter);
+ writer.setLenient(true);
+ writer.beginTag("tag");
+ writer.endTag();
+ writer.beginTag("tag2");
+ writer.endTag();
+ writer.close();
+ assertThat(stringWriter.toString()).isEqualTo("");
+ }
+
+ @Test
+ public void testNullName() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter jsonWriter = new NativeXmlWriter(stringWriter);
+ jsonWriter.beginTag("tag");
+ try {
+ jsonWriter.attributeName(null);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test
+ public void testNullStringValue() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter jsonWriter = new NativeXmlWriter(stringWriter);
+ jsonWriter.beginTag("tag");
+ jsonWriter.attributeName("a");
+ jsonWriter.attributeValue(null);
+ jsonWriter.endTag();
+ assertThat(stringWriter.toString()).isEqualTo("");
+ }
+
+ @Test
+ public void testUnicodeLineBreaksEscaped() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter jsonWriter = new NativeXmlWriter(stringWriter);
+ jsonWriter.beginTag("body");
+ jsonWriter.text("\u2028 \u2029");
+ jsonWriter.endTag();
+ // JSON specification does not require that they are escaped, but Gson escapes them for
+ // compatibility with JavaScript where they are considered line breaks
+ assertThat(stringWriter.toString()).isEqualTo("
");
+ }
+
+ @Test
+ public void testDeepNesting() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter jsonWriter = new NativeXmlWriter(stringWriter);
+ for (int i = 0; i < 20; i++) {
+ jsonWriter.beginTag("a");
+ }
+ for (int i = 0; i < 20; i++) {
+ jsonWriter.endTag();
+ }
+ assertThat(stringWriter.toString())
+ .isEqualTo(
+ ""
+ + ""
+ + ""
+ + ""
+ + "");
+ }
+
+ @Test
+ public void testRepeatedName() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter jsonWriter = new NativeXmlWriter(stringWriter);
+ jsonWriter.beginTag("tag");
+ jsonWriter.attributeName("a");
+ jsonWriter.attributeValue("true");
+ jsonWriter.attributeName("a");
+ jsonWriter.attributeValue("false");
+ jsonWriter.beginTag("a").text("true").endTag();
+ jsonWriter.beginTag("a").text("false").endTag();
+ jsonWriter.endTag();
+ // NativeXmlWriter doesn't attempt to detect duplicate names
+ assertThat(stringWriter.toString()).isEqualTo("truefalse");
+ }
+
+ @Test
+ public void testPrettyPrintArray() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter jsonWriter = new NativeXmlWriter(stringWriter);
+ jsonWriter.setIndent(" ");
+
+ jsonWriter.beginTag("tag");
+ jsonWriter.attribute("a", "b");
+ jsonWriter.text("true");
+ jsonWriter.text("false");
+ jsonWriter.text("5.0");
+ jsonWriter.text(null);
+ jsonWriter.beginTag("object");
+ jsonWriter.attributeName("a").attributeValue("6.0");
+ jsonWriter.beginTag("b").text("7.0").endTag();
+ jsonWriter.endTag();
+ jsonWriter.beginTag("tag");
+ jsonWriter.text("8.0");
+ jsonWriter.text("9.0");
+ jsonWriter.endTag();
+ jsonWriter.endTag();
+
+ String expected =
+ """
+
+ true
+
+ false
+
+ 5.0
+
+ null
+
+
+ 8.0
+
+ 9.0
+
+ """;
+ assertThat(stringWriter.toString()).isEqualTo(expected);
+ }
+
+ @Test
+ public void testClosedWriterThrowsOnStructure() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter writer = new NativeXmlWriter(stringWriter);
+ writer.beginTag("tag");
+ writer.endTag();
+ writer.close();
+ try {
+ writer.beginTag("tag");
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ try {
+ writer.endTag();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void testClosedWriterThrowsOnName() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter writer = new NativeXmlWriter(stringWriter);
+ writer.beginTag("tag");
+ writer.endTag();
+ writer.close();
+ try {
+ writer.attributeName("a");
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void testClosedWriterThrowsOnValue() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter writer = new NativeXmlWriter(stringWriter);
+ writer.beginTag("tag");
+ writer.endTag();
+ writer.close();
+ try {
+ writer.attributeValue("a");
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void testClosedWriterThrowsOnFlush() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter writer = new NativeXmlWriter(stringWriter);
+ writer.beginTag("tag");
+ writer.endTag();
+ writer.close();
+ try {
+ writer.flush();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test
+ public void testWriterCloseIsIdempotent() throws IOException {
+ StringWriter stringWriter = new StringWriter();
+ NativeXmlWriter writer = new NativeXmlWriter(stringWriter);
+ writer.beginTag("tag");
+ writer.endTag();
+ writer.close();
+ writer.close();
+ }
+}