Compare commits
7 Commits
de55636d20
...
5e89193244
Author | SHA1 | Date |
---|---|---|
Johannes Frohnmeyer | 5e89193244 | |
Johannes Frohnmeyer | 846bb1a982 | |
Johannes Frohnmeyer | 0f8a549495 | |
Johannes Frohnmeyer | 30f18ecef9 | |
Johannes Frohnmeyer | 7bcde68b9f | |
Johannes Frohnmeyer | 615ff12c27 | |
Johannes Frohnmeyer | b8d9b52ab5 |
|
@ -182,7 +182,12 @@ public class JsonWriter extends SerializeWriter<IOException, JsonWriter> impleme
|
|||
private void writeDeferredComment() throws IOException {
|
||||
if (!deferredComments.isEmpty()) {
|
||||
if (newline.isEmpty()) {
|
||||
out.append("/* ").append(String.join(" / ", deferredComments)).append(" */");
|
||||
out.append("/* ")
|
||||
.append(String.join(" / ", deferredComments.stream()
|
||||
.filter(s -> s != null && !s.isBlank())
|
||||
.map(s -> s.replace("/*", "#/"))
|
||||
.toList())
|
||||
).append(" */");
|
||||
} else {
|
||||
boolean first = true;
|
||||
for (String s : deferredComments) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package io.gitlab.jfronny.commons.serialize.xml;
|
|||
|
||||
import io.gitlab.jfronny.commons.serialize.MalformedDataException;
|
||||
import io.gitlab.jfronny.commons.serialize.StringEscapeUtil;
|
||||
import io.gitlab.jfronny.commons.serialize.xml.impl.NameCheck;
|
||||
import io.gitlab.jfronny.commons.serialize.xml.impl.XmlScope;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
@ -230,7 +231,7 @@ public class NativeXmlReader implements Closeable {
|
|||
// fall through
|
||||
} else if (pos < limit || fillBuffer(1)) {
|
||||
char chNext = buffer[pos + 1];
|
||||
var check = isNameStart((char) c, chNext);
|
||||
var check = NameCheck.isNameStart((char) c, chNext);
|
||||
pos--;
|
||||
if (check != NameCheck.NONE) {
|
||||
return peeked = PEEKED_ATTRIBUTE_NAME;
|
||||
|
@ -289,7 +290,7 @@ public class NativeXmlReader implements Closeable {
|
|||
}
|
||||
}
|
||||
} else if (pos + 2 <= limit || fillBuffer(2)) {
|
||||
var check = isNameStart(chNext, buffer[pos + 1]);
|
||||
var check = NameCheck.isNameStart(chNext, buffer[pos + 1]);
|
||||
if (check != NameCheck.NONE) {
|
||||
return peeked = PEEKED_BEGIN_TAG;
|
||||
}
|
||||
|
@ -302,33 +303,6 @@ public class NativeXmlReader implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
private enum NameCheck { FIRST, BOTH, NONE }
|
||||
private NameCheck isNameStart(char ch, char chNext) {
|
||||
if ('A' <= ch && ch <= 'Z') return NameCheck.FIRST;
|
||||
if ('a' <= ch && ch <= 'z') return NameCheck.FIRST;
|
||||
return switch (ch) {
|
||||
case ':', '_' -> NameCheck.FIRST;
|
||||
case '\u2070' -> chNext == '\u218F' ? NameCheck.BOTH : NameCheck.NONE;
|
||||
case '\u2C00' -> chNext == '\u2FEF' ? NameCheck.BOTH : NameCheck.NONE;
|
||||
case '\u3001' -> chNext == '\uD7FF' ? NameCheck.BOTH : NameCheck.NONE;
|
||||
case '\uF900' -> chNext == '\uFDCF' ? NameCheck.BOTH : NameCheck.NONE;
|
||||
case '\uFDF0' -> chNext == '\uFFFD' ? NameCheck.BOTH : NameCheck.NONE;
|
||||
default -> NameCheck.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
private NameCheck isName(char ch, char chNext) {
|
||||
var nameStart = isNameStart(ch, chNext);
|
||||
if (nameStart != NameCheck.NONE) return nameStart;
|
||||
if ('0' <= ch && ch <= '9') return NameCheck.FIRST;
|
||||
return switch (ch) {
|
||||
case '-', '.', '\u00B7' -> NameCheck.FIRST;
|
||||
case '\u0300' -> chNext == '\u036F' ? NameCheck.BOTH : NameCheck.NONE;
|
||||
case '\u203F' -> chNext == '\u2040' ? NameCheck.BOTH : NameCheck.NONE;
|
||||
default -> NameCheck.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
public String nextAttributeName() throws IOException {
|
||||
int p = peeked;
|
||||
if (p == PEEKED_NONE) {
|
||||
|
@ -473,7 +447,7 @@ public class NativeXmlReader implements Closeable {
|
|||
}
|
||||
}
|
||||
case PEEKED_ATTRIBUTE_NAME -> {
|
||||
skipUntil((c, i) -> isName(c, pos + i + 1 < limit ? buffer[pos + i + 1] : '\0') == NameCheck.NONE);
|
||||
skipUntil((c, i) -> NameCheck.isName(c, pos + i + 1 < limit ? buffer[pos + i + 1] : '\0') == NameCheck.NONE);
|
||||
if (count == 0) pathNames[stackSize - 1] = "<skipped>";
|
||||
peeked = PEEKED_NONE;
|
||||
}
|
||||
|
@ -494,7 +468,7 @@ public class NativeXmlReader implements Closeable {
|
|||
}
|
||||
|
||||
private String nextName() throws IOException {
|
||||
return readUntil((c, i) -> isName(c, pos + i + 1 < limit ? buffer[pos + i + 1] : '\0') == NameCheck.NONE, false);
|
||||
return readUntil((c, i) -> NameCheck.isName(c, pos + i + 1 < limit ? buffer[pos + i + 1] : '\0') == NameCheck.NONE, false);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
|
|
|
@ -0,0 +1,392 @@
|
|||
package io.gitlab.jfronny.commons.serialize.xml;
|
||||
|
||||
import io.gitlab.jfronny.commons.serialize.xml.impl.NameCheck;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.Flushable;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.*;
|
||||
|
||||
import static io.gitlab.jfronny.commons.serialize.xml.impl.XmlScope.*;
|
||||
|
||||
public class NativeXmlWriter implements Closeable, Flushable {
|
||||
private final Writer out;
|
||||
private int[] stack = new int[32];
|
||||
private int stackSize = 0;
|
||||
|
||||
{
|
||||
push(EMPTY_DOCUMENT);
|
||||
}
|
||||
|
||||
private String[] pathNames = new String[32];
|
||||
|
||||
private String newline;
|
||||
private String indent;
|
||||
private boolean usesEmptyNewlineAndIndent;
|
||||
|
||||
private final List<String> deferredComments = new LinkedList<>();
|
||||
private String deferredText = null;
|
||||
private boolean wasText = false;
|
||||
|
||||
private boolean lenient = false;
|
||||
private boolean escapeNonAscii = true;
|
||||
public NativeXmlWriter(Writer out) {
|
||||
this.out = Objects.requireNonNull(out, "out == null");
|
||||
newline = indent = "";
|
||||
setIndent("");
|
||||
setNewline("");
|
||||
}
|
||||
|
||||
public NativeXmlWriter setLenient(boolean lenient) {
|
||||
this.lenient = lenient;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isLenient() {
|
||||
return lenient;
|
||||
}
|
||||
|
||||
public NativeXmlWriter setEscapeNonAscii(boolean escapeNonAscii) {
|
||||
this.escapeNonAscii = escapeNonAscii;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isEscapeNonAscii() {
|
||||
return escapeNonAscii;
|
||||
}
|
||||
|
||||
public NativeXmlWriter setIndent(String indent) {
|
||||
if (indent == null || indent.isEmpty()) {
|
||||
this.indent = "";
|
||||
this.usesEmptyNewlineAndIndent = newline.isEmpty();
|
||||
} else {
|
||||
this.newline = "\n"; // if someone sets an indent, this is probably intended
|
||||
this.indent = indent;
|
||||
this.usesEmptyNewlineAndIndent = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getIndent() {
|
||||
return indent;
|
||||
}
|
||||
|
||||
public NativeXmlWriter setNewline(String newline) {
|
||||
if (newline == null || newline.isEmpty()) {
|
||||
this.newline = "";
|
||||
this.usesEmptyNewlineAndIndent = indent.isEmpty();
|
||||
} else {
|
||||
this.newline = newline;
|
||||
this.usesEmptyNewlineAndIndent = false;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getNewline() {
|
||||
return newline;
|
||||
}
|
||||
|
||||
public NativeXmlWriter beginTag(String name) throws IOException {
|
||||
Objects.requireNonNull(name, "name == null");
|
||||
wasText = false;
|
||||
beforeValue();
|
||||
pathNames[stackSize - 1] = name;
|
||||
push(TAG_HEAD);
|
||||
out.write('<');
|
||||
name(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
wasText = false;
|
||||
boolean simple = false; // true if the last tag only contained simple text
|
||||
|
||||
if (!deferredComments.isEmpty()) {
|
||||
if (context == TAG_HEAD && deferredComments.size() == 1) {
|
||||
out.write('>');
|
||||
if (deferredText == null) simple = true;
|
||||
else newline();
|
||||
writeDeferredComment();
|
||||
} else {
|
||||
if (context == TAG_HEAD) out.write('>');
|
||||
newline();
|
||||
writeDeferredComment();
|
||||
}
|
||||
context = TAG_BODY;
|
||||
}
|
||||
|
||||
if (deferredText != null) {
|
||||
if (context == TAG_HEAD) {
|
||||
out.write('>');
|
||||
escapeText(deferredText, true);
|
||||
context = TAG_BODY;
|
||||
simple = true;
|
||||
} else text(deferredText);
|
||||
deferredText = null;
|
||||
}
|
||||
|
||||
stackSize--;
|
||||
if (context == TAG_BODY) {
|
||||
if (!simple) newline();
|
||||
out.write("</");
|
||||
name(pathNames[stackSize - 1]);
|
||||
out.write('>');
|
||||
} else {
|
||||
out.write("/>");
|
||||
}
|
||||
pathNames[stackSize - 1] = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
private void push(int newTop) {
|
||||
if (stackSize == stack.length) {
|
||||
int newLength = stackSize * 2;
|
||||
stack = Arrays.copyOf(stack, newLength);
|
||||
pathNames = Arrays.copyOf(pathNames, newLength);
|
||||
}
|
||||
stack[stackSize++] = newTop;
|
||||
}
|
||||
|
||||
/** Returns the value on the top of the stack. */
|
||||
private int peek() {
|
||||
if (stackSize == 0) {
|
||||
throw new IllegalStateException("JsonWriter is closed.");
|
||||
}
|
||||
return stack[stackSize - 1];
|
||||
}
|
||||
|
||||
/** Replace the value on the top of the stack with the given value. */
|
||||
private void replaceTop(int topOfStack) {
|
||||
stack[stackSize - 1] = topOfStack;
|
||||
}
|
||||
|
||||
public NativeXmlWriter comment(String comment) throws IOException {
|
||||
if (comment == null || comment.isBlank()) return this;
|
||||
wasText = false;
|
||||
comment = comment.replace("-->", "-->");
|
||||
String[] parts = comment.split("\n");
|
||||
Collections.addAll(deferredComments, parts);
|
||||
if (peek() == NONEMPTY_DOCUMENT) {
|
||||
newline();
|
||||
writeDeferredComment();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private void writeDeferredComment() throws IOException {
|
||||
if (!deferredComments.isEmpty()) {
|
||||
if (newline.isEmpty()) {
|
||||
out.append("<!-- ")
|
||||
.append(String.join(" / ", deferredComments.stream()
|
||||
.filter(s -> s != null && !s.isBlank()).toList())
|
||||
).append(" -->");
|
||||
} else {
|
||||
boolean first = true;
|
||||
for (String s : deferredComments) {
|
||||
if (!first) newline();
|
||||
first = false;
|
||||
if (s == null || s.isBlank()) out.append("<!---->");
|
||||
else out.append("<!-- ").append(s).append(" -->");
|
||||
}
|
||||
}
|
||||
deferredComments.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public NativeXmlWriter attribute(String name, String value) throws IOException {
|
||||
return attributeName(name).attributeValue(value);
|
||||
}
|
||||
|
||||
public NativeXmlWriter attributeName(String name) throws IOException {
|
||||
Objects.requireNonNull(name, "name == null");
|
||||
if (peek() != TAG_HEAD) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
wasText = false;
|
||||
replaceTop(DANGLING_NAME);
|
||||
out.write(' ');
|
||||
name(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
public NativeXmlWriter attributeValue(String value) throws IOException {
|
||||
value = value == null ? "null" : value;
|
||||
if (peek() != DANGLING_NAME) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
wasText = false;
|
||||
replaceTop(TAG_HEAD);
|
||||
out.write('=');
|
||||
out.write('"');
|
||||
escapeText(value, lenient);
|
||||
out.write('"');
|
||||
return this;
|
||||
}
|
||||
|
||||
public NativeXmlWriter text(String text) throws IOException {
|
||||
text = text == null ? "null" : text;
|
||||
if (peek() == TAG_HEAD && !text.contains("\n") && deferredText == null && deferredComments.isEmpty()) {
|
||||
deferredText = text;
|
||||
wasText = true;
|
||||
return this;
|
||||
}
|
||||
if (wasText && deferredComments.isEmpty()) deferredComments.add("");
|
||||
beforeValue();
|
||||
escapeText(text, true);
|
||||
wasText = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public NativeXmlWriter reference(String reference) throws IOException {
|
||||
reference = reference == null ? "" : reference;
|
||||
if (!reference.matches("[a-zA-Z_:][a-zA-Z0-9._:-]*")) {
|
||||
throw new IllegalArgumentException("Invalid reference: " + reference);
|
||||
}
|
||||
wasText = false;
|
||||
beforeValue();
|
||||
out.write("&");
|
||||
out.write(reference);
|
||||
out.write(";");
|
||||
return this;
|
||||
}
|
||||
|
||||
public NativeXmlWriter cdata(String cdata) throws IOException {
|
||||
cdata = cdata == null ? "" : cdata;
|
||||
if (cdata.contains("]]>")) throw new IllegalArgumentException("CDATA cannot contain ']]>'");
|
||||
wasText = false;
|
||||
beforeValue();
|
||||
out.write("<![CDATA[");
|
||||
out.write(cdata);
|
||||
out.write("]]>");
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
if (stackSize == 0) {
|
||||
throw new IllegalStateException("JsonWriter is closed.");
|
||||
}
|
||||
out.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
out.close();
|
||||
|
||||
int size = stackSize;
|
||||
if (size > 1 || (size == 1 && stack[size - 1] != NONEMPTY_DOCUMENT)) {
|
||||
throw new IOException("Incomplete document");
|
||||
}
|
||||
stackSize = 0;
|
||||
}
|
||||
|
||||
private void name(String name) throws IOException {
|
||||
if (name == null) throw new NullPointerException("name == null");
|
||||
if (stackSize == 0) throw new IllegalStateException("JsonWriter is closed.");
|
||||
// Check name for illegal characters
|
||||
int last = 0;
|
||||
int length = name.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
char c = name.charAt(i);
|
||||
char n = i + 1 < length ? name.charAt(i + 1) : '\0';
|
||||
NameCheck check = i == 0 ? NameCheck.isNameStart(c, n) : NameCheck.isName(c, n);
|
||||
switch (check) {
|
||||
case NONE -> {
|
||||
if (!lenient) {
|
||||
throw new IllegalArgumentException("Illegal character in name: " + name);
|
||||
}
|
||||
if (last < i) out.write(name, last, i - last);
|
||||
out.write('_');
|
||||
last = i + 1;
|
||||
}
|
||||
case BOTH -> i++;
|
||||
case FIRST -> {}
|
||||
}
|
||||
}
|
||||
if (last < length) out.write(name, last, length - last);
|
||||
}
|
||||
|
||||
private void escapeText(String text, boolean permitControl) throws IOException {
|
||||
int last = 0;
|
||||
int length = text.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
char c = text.charAt(i);
|
||||
if (!permitControl && c < 0x20 && c != 0x09) throw new IllegalArgumentException("Illegal control character in text: " + text);
|
||||
String replacement = switch(c) {
|
||||
case '&' -> "&";
|
||||
case '<' -> "<";
|
||||
case '>' -> ">";
|
||||
case '"' -> """;
|
||||
case '\'' -> "'";
|
||||
default -> escapeNonAscii && c > 127 ? "&#" + (int) c + ";" : null;
|
||||
};
|
||||
if (replacement == null) continue;
|
||||
if (last < i) out.write(text, last, i - last);
|
||||
out.write(replacement);
|
||||
last = i + 1;
|
||||
}
|
||||
if (last < length) out.write(text, last, length - last);
|
||||
}
|
||||
|
||||
private void newline() throws IOException {
|
||||
if (usesEmptyNewlineAndIndent) {
|
||||
return;
|
||||
}
|
||||
|
||||
out.write(newline);
|
||||
for (int i = 1, size = stackSize; i < size; i++) {
|
||||
out.write(indent);
|
||||
}
|
||||
}
|
||||
|
||||
private void beforeValue() throws IOException {
|
||||
switch (peek()) {
|
||||
case NONEMPTY_DOCUMENT:
|
||||
if (!lenient) {
|
||||
throw new IllegalStateException("XML must have only one top-level value.");
|
||||
}
|
||||
case EMPTY_DOCUMENT:
|
||||
replaceTop(NONEMPTY_DOCUMENT);
|
||||
if (!deferredComments.isEmpty()) {
|
||||
writeDeferredComment();
|
||||
newline();
|
||||
}
|
||||
if (deferredText != null) {
|
||||
escapeText(deferredText, true);
|
||||
newline();
|
||||
deferredText = null;
|
||||
}
|
||||
break;
|
||||
case DANGLING_NAME:
|
||||
if (!lenient) {
|
||||
throw new IllegalStateException("Attribute name must be followed by a value");
|
||||
}
|
||||
attributeValue("null");
|
||||
case TAG_HEAD:
|
||||
out.write('>');
|
||||
replaceTop(TAG_BODY);
|
||||
case TAG_BODY:
|
||||
newline();
|
||||
if (deferredText != null) {
|
||||
escapeText(deferredText, true);
|
||||
newline();
|
||||
deferredText = null;
|
||||
}
|
||||
if (!deferredComments.isEmpty()) {
|
||||
writeDeferredComment();
|
||||
newline();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package io.gitlab.jfronny.commons.serialize.xml.impl;
|
||||
|
||||
public enum NameCheck {
|
||||
FIRST, BOTH, NONE;
|
||||
|
||||
public static NameCheck isNameStart(char ch, char chNext) {
|
||||
if ('A' <= ch && ch <= 'Z') return NameCheck.FIRST;
|
||||
if ('a' <= ch && ch <= 'z') return NameCheck.FIRST;
|
||||
return switch (ch) {
|
||||
case ':', '_' -> NameCheck.FIRST;
|
||||
case '\u2070' -> chNext == '\u218F' ? NameCheck.BOTH : NameCheck.NONE;
|
||||
case '\u2C00' -> chNext == '\u2FEF' ? NameCheck.BOTH : NameCheck.NONE;
|
||||
case '\u3001' -> chNext == '\uD7FF' ? NameCheck.BOTH : NameCheck.NONE;
|
||||
case '\uF900' -> chNext == '\uFDCF' ? NameCheck.BOTH : NameCheck.NONE;
|
||||
case '\uFDF0' -> chNext == '\uFFFD' ? NameCheck.BOTH : NameCheck.NONE;
|
||||
default -> NameCheck.NONE;
|
||||
};
|
||||
}
|
||||
|
||||
public static NameCheck isName(char ch, char chNext) {
|
||||
var nameStart = isNameStart(ch, chNext);
|
||||
if (nameStart != NameCheck.NONE) return nameStart;
|
||||
if ('0' <= ch && ch <= '9') return NameCheck.FIRST;
|
||||
return switch (ch) {
|
||||
case '-', '.', '\u00B7' -> NameCheck.FIRST;
|
||||
case '\u0300' -> chNext == '\u036F' ? NameCheck.BOTH : NameCheck.NONE;
|
||||
case '\u203F' -> chNext == '\u2040' ? NameCheck.BOTH : NameCheck.NONE;
|
||||
default -> NameCheck.NONE;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package io.gitlab.jfronny.commons.serialize.xml.wrapper;
|
||||
|
||||
public interface NameEncoding {
|
||||
String encode(String name);
|
||||
String decode(String name);
|
||||
|
||||
NameEncoding DEFAULT = new NameEncoding() {
|
||||
@Override
|
||||
public String encode(String name) {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decode(String name) {
|
||||
return name;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -38,6 +38,7 @@ public class XmlReader extends SerializeReader<IOException, XmlReader> implement
|
|||
private String[] pathNames = new String[32];
|
||||
private int[] pathIndices = new int[32];
|
||||
private Heuristics heuristics = Heuristics.DEFAULT;
|
||||
private NameEncoding nameEncoding = NameEncoding.DEFAULT;
|
||||
private String nextTagNamePath = null;
|
||||
private String nextTagName = null;
|
||||
|
||||
|
@ -83,6 +84,15 @@ public class XmlReader extends SerializeReader<IOException, XmlReader> implement
|
|||
return heuristics;
|
||||
}
|
||||
|
||||
public XmlReader setNameEncoding(NameEncoding nameEncoding) {
|
||||
this.nameEncoding = Objects.requireNonNull(nameEncoding);
|
||||
return this;
|
||||
}
|
||||
|
||||
public NameEncoding getNameEncoding() {
|
||||
return nameEncoding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlReader beginArray() throws IOException {
|
||||
int p = peeked;
|
||||
|
@ -263,15 +273,15 @@ public class XmlReader extends SerializeReader<IOException, XmlReader> implement
|
|||
}
|
||||
return switch (p) {
|
||||
case PEEKED_NAME_ATT -> {
|
||||
String res = reader.nextAttributeName();
|
||||
String result = reader.nextAttributeName();
|
||||
peeked = PEEKED_NONE;
|
||||
yield res;
|
||||
yield nameEncoding.decode(result);
|
||||
}
|
||||
case PEEKED_NAME_TAG -> {
|
||||
String res = nextTagName;
|
||||
if (res == null) throw unexpectedTokenError("a name");
|
||||
String result = nextTagName;
|
||||
if (result == null) throw unexpectedTokenError("a name");
|
||||
peeked = PEEKED_NONE;
|
||||
yield res;
|
||||
yield nameEncoding.decode(result);
|
||||
}
|
||||
case PEEKED_NAME_VIRTUAL_TEXT -> {
|
||||
String result = heuristics.guessElementName(reader.getPath(), XmlToken.TEXT);
|
||||
|
@ -285,11 +295,11 @@ public class XmlReader extends SerializeReader<IOException, XmlReader> implement
|
|||
}
|
||||
case PEEKED_NAME_BEGIN_OBJECT -> {
|
||||
peeked = PEEKED_BEGIN_OBJECT;
|
||||
yield nextTagName;
|
||||
yield nameEncoding.decode(nextTagName);
|
||||
}
|
||||
case PEEKED_NAME_BEGIN_ARRAY -> {
|
||||
peeked = PEEKED_BEGIN_ARRAY;
|
||||
yield nextTagName;
|
||||
yield nameEncoding.decode(nextTagName);
|
||||
}
|
||||
default -> throw unexpectedTokenError("a name");
|
||||
};
|
||||
|
@ -369,14 +379,41 @@ public class XmlReader extends SerializeReader<IOException, XmlReader> implement
|
|||
return result;
|
||||
}
|
||||
|
||||
private String getPath(boolean usePreviousPath) {
|
||||
if (nextTagNamePath != null) return nextTagNamePath;
|
||||
StringBuilder result = new StringBuilder().append('$');
|
||||
for (int i = 0; i < stackSize; i++) {
|
||||
int scope = stack[i];
|
||||
switch (scope) {
|
||||
case WrapperScope.ARRAY -> {
|
||||
int pathIndex = pathIndices[i];
|
||||
// If index is last path element it points to next array element; have to decrement
|
||||
if (usePreviousPath && pathIndex > 0 && i == stackSize - 1) {
|
||||
pathIndex--;
|
||||
}
|
||||
result.append('[').append(pathIndex).append(']');
|
||||
}
|
||||
case WrapperScope.OBJECT -> {
|
||||
result.append('.');
|
||||
if (pathNames[i] != null) {
|
||||
result.append(pathNames[i]);
|
||||
}
|
||||
}
|
||||
case WrapperScope.OBJECT_VALUE_WRAPPER, WrapperScope.OBJECT_VALUE_WRAPPER_USED, WrapperScope.DOCUMENT -> {}
|
||||
default -> throw new AssertionError("Unknown scope value: " + scope);
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
return nextTagName == null ? reader.getPath() : nextTagNamePath;
|
||||
return getPath(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPreviousPath() {
|
||||
return getPath(); // TODO this should be different when handling arrays
|
||||
return getPath(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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 = """
|
||||
<!-- comment at file head -->
|
||||
<root>
|
||||
<!-- comment directly after context -->
|
||||
<a>true</a>
|
||||
<!-- comment before context -->
|
||||
<inner att="value">
|
||||
<!-- comment directly after attribute name -->
|
||||
<b att="value">
|
||||
<!-- comment after attribute value -->
|
||||
false
|
||||
</b>
|
||||
<cnt><!-- only comment inside tag --></cnt>
|
||||
</inner>
|
||||
<!-- comment before context end -->
|
||||
</root>
|
||||
<!-- comment behind the object -->""";
|
||||
|
||||
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("<empty/>");
|
||||
}
|
||||
|
||||
@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("<tag/><tag2/>");
|
||||
}
|
||||
|
||||
@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("<tag a=\"null\"/>");
|
||||
}
|
||||
|
||||
@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("<body>
 
</body>");
|
||||
}
|
||||
|
||||
@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(
|
||||
"<a><a><a><a><a><a><a><a><a><a>"
|
||||
+ "<a><a><a><a><a><a><a><a><a>"
|
||||
+ "<a/>"
|
||||
+ "</a></a></a></a></a></a></a></a></a>"
|
||||
+ "</a></a></a></a></a></a></a></a></a></a>");
|
||||
}
|
||||
|
||||
@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("<tag a=\"true\" a=\"false\"><a>true</a><a>false</a></tag>");
|
||||
}
|
||||
|
||||
@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 =
|
||||
"""
|
||||
<tag a="b">
|
||||
true
|
||||
<!---->
|
||||
false
|
||||
<!---->
|
||||
5.0
|
||||
<!---->
|
||||
null
|
||||
<object a="6.0">
|
||||
<b>7.0</b>
|
||||
</object>
|
||||
<tag>
|
||||
8.0
|
||||
<!---->
|
||||
9.0
|
||||
</tag>
|
||||
</tag>""";
|
||||
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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue