Compare commits
8 Commits
5e89193244
...
a23af078e1
Author | SHA1 | Date |
---|---|---|
Johannes Frohnmeyer | a23af078e1 | |
Johannes Frohnmeyer | 0b6783c9bd | |
Johannes Frohnmeyer | fcc3d97150 | |
Johannes Frohnmeyer | dc48cc436c | |
Johannes Frohnmeyer | 8992e42393 | |
Johannes Frohnmeyer | 45eee07a29 | |
Johannes Frohnmeyer | ae854cc040 | |
Johannes Frohnmeyer | 2a9a6300ef |
|
@ -574,7 +574,10 @@ public class JsonReader extends SerializeReader<IOException, JsonReader> impleme
|
|||
default -> {
|
||||
// If we are in an array, allow reading an in inferred name once
|
||||
if (!wroteName) {
|
||||
if (stack[stackSize - 1] == JsonScope.EMPTY_ARRAY || stack[stackSize - 1] == JsonScope.NONEMPTY_ARRAY) {
|
||||
if (stack[stackSize - 1] == JsonScope.EMPTY_ARRAY
|
||||
|| stack[stackSize - 1] == JsonScope.NONEMPTY_ARRAY
|
||||
|| stack[stackSize - 1] == JsonScope.EMPTY_DOCUMENT
|
||||
|| stack[stackSize - 1] == JsonScope.NONEMPTY_DOCUMENT) {
|
||||
wroteName = true;
|
||||
return heuristics.guessArrayElementName(getPath());
|
||||
}
|
||||
|
|
|
@ -209,7 +209,9 @@ public class JsonWriter extends SerializeWriter<IOException, JsonWriter> impleme
|
|||
int context = peek();
|
||||
if (context != EMPTY_OBJECT && context != NONEMPTY_OBJECT) {
|
||||
if (lenient) {
|
||||
if (context != EMPTY_ARRAY && context != NONEMPTY_ARRAY) throw new IllegalStateException("Please begin an object or array before writing a name.");
|
||||
if (context != EMPTY_ARRAY && context != NONEMPTY_ARRAY
|
||||
&& context != EMPTY_DOCUMENT && context != NONEMPTY_DOCUMENT)
|
||||
throw new IllegalStateException("Please begin an object or array before writing a name.");
|
||||
} else {
|
||||
throw new IllegalStateException("Please begin an object before writing a name.");
|
||||
}
|
||||
|
@ -221,7 +223,8 @@ public class JsonWriter extends SerializeWriter<IOException, JsonWriter> impleme
|
|||
private void writeDeferredName() throws IOException {
|
||||
if (deferredName != null) {
|
||||
int context = peek();
|
||||
if (context == EMPTY_ARRAY || context == NONEMPTY_ARRAY) {
|
||||
if (context == EMPTY_ARRAY || context == NONEMPTY_ARRAY
|
||||
|| context == EMPTY_DOCUMENT || context == NONEMPTY_DOCUMENT) {
|
||||
if (commentUnexpectedNames) {
|
||||
// Write the name as a comment instead of literally
|
||||
comment(deferredName);
|
||||
|
|
|
@ -190,6 +190,20 @@ public class NativeXmlReader implements Closeable {
|
|||
return p != PEEKED_EOF && p != PEEKED_END_TAG && p != PEEKED_END_TAG_CONCISE;
|
||||
}
|
||||
|
||||
public boolean isConciseEndTag() throws IOException {
|
||||
int p = peeked;
|
||||
if (p == PEEKED_NONE) {
|
||||
p = doPeek();
|
||||
}
|
||||
if (p == PEEKED_END_TAG_CONCISE) {
|
||||
return true;
|
||||
} else if (p == PEEKED_END_TAG) {
|
||||
return false;
|
||||
} else {
|
||||
throw unexpectedTokenError("END_TAG");
|
||||
}
|
||||
}
|
||||
|
||||
public XmlToken peek() throws IOException {
|
||||
int p = peeked;
|
||||
if (p == PEEKED_NONE) {
|
||||
|
|
|
@ -29,7 +29,8 @@ public class XmlReader extends SerializeReader<IOException, XmlReader> implement
|
|||
private static final int PEEKED_CDATA = 11;
|
||||
private static final int PEEKED_NAME_VIRTUAL_TEXT = 12;
|
||||
private static final int PEEKED_NAME_VIRTUAL_CDATA = 13;
|
||||
private static final int PEEKED_EOF = 14;
|
||||
private static final int PEEKED_NULL_VIRTUAL = 14;
|
||||
private static final int PEEKED_EOF = 15;
|
||||
|
||||
int peeked = PEEKED_NONE;
|
||||
private final NativeXmlReader reader;
|
||||
|
@ -43,7 +44,7 @@ public class XmlReader extends SerializeReader<IOException, XmlReader> implement
|
|||
private String nextTagName = null;
|
||||
|
||||
{
|
||||
stack[stackSize++] = WrapperScope.DOCUMENT;
|
||||
push(WrapperScope.DOCUMENT);
|
||||
}
|
||||
|
||||
private void push(int newTop) {
|
||||
|
@ -66,6 +67,7 @@ public class XmlReader extends SerializeReader<IOException, XmlReader> implement
|
|||
|
||||
@Override
|
||||
public XmlReader setLenient(boolean lenient) {
|
||||
super.setLenient(lenient);
|
||||
reader.setLenient(lenient);
|
||||
return this;
|
||||
}
|
||||
|
@ -175,6 +177,7 @@ public class XmlReader extends SerializeReader<IOException, XmlReader> implement
|
|||
case PEEKED_CDATA -> heuristics.guessValueKind(getPath(), XmlToken.CDATA);
|
||||
case PEEKED_ATT_VALUE -> heuristics.guessValueKind(getPath(), XmlToken.ATTRIBUTE_VALUE);
|
||||
case PEEKED_TEXT -> heuristics.guessValueKind(getPath(), XmlToken.TEXT);
|
||||
case PEEKED_NULL_VIRTUAL -> Token.NULL;
|
||||
case PEEKED_EOF -> Token.END_DOCUMENT;
|
||||
default -> throw new AssertionError();
|
||||
};
|
||||
|
@ -259,6 +262,12 @@ public class XmlReader extends SerializeReader<IOException, XmlReader> implement
|
|||
reader.endTag();
|
||||
yield doPeek();
|
||||
}
|
||||
case WrapperScope.OBJECT_VALUE_WRAPPER -> {
|
||||
if (!reader.isConciseEndTag()) throw syntaxError("Unexpected end tag");
|
||||
stackSize--;
|
||||
reader.endTag();
|
||||
yield PEEKED_NULL_VIRTUAL;
|
||||
}
|
||||
default -> throw syntaxError("Unexpected end tag");
|
||||
};
|
||||
case EOF -> PEEKED_EOF;
|
||||
|
@ -373,6 +382,7 @@ public class XmlReader extends SerializeReader<IOException, XmlReader> implement
|
|||
}
|
||||
yield reader.nextCData();
|
||||
}
|
||||
case PEEKED_NULL_VIRTUAL -> "null";
|
||||
default -> throw unexpectedTokenError(kind);
|
||||
};
|
||||
peeked = PEEKED_NONE;
|
||||
|
|
|
@ -1,62 +1,272 @@
|
|||
package io.gitlab.jfronny.commons.serialize.xml.wrapper;
|
||||
|
||||
import io.gitlab.jfronny.commons.serialize.SerializeWriter;
|
||||
import io.gitlab.jfronny.commons.serialize.xml.NativeXmlWriter;
|
||||
import io.gitlab.jfronny.commons.serialize.xml.impl.WrapperScope;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import static io.gitlab.jfronny.commons.serialize.xml.impl.WrapperScope.*;
|
||||
|
||||
public class XmlWriter extends SerializeWriter<IOException, XmlWriter> implements Closeable {
|
||||
private final NativeXmlWriter writer;
|
||||
private int[] stack = new int[32];
|
||||
private int stackSize = 0;
|
||||
private String[] pathNames = new String[32];
|
||||
private int[] pathIndices = new int[32];
|
||||
|
||||
private String deferredName;
|
||||
|
||||
private Heuristics heuristics = Heuristics.DEFAULT;
|
||||
private NameEncoding nameEncoding = NameEncoding.DEFAULT;
|
||||
|
||||
{
|
||||
push(DOCUMENT);
|
||||
}
|
||||
|
||||
private void push(int newTop) {
|
||||
if (stackSize == stack.length) {
|
||||
int newLength = stackSize * 2;
|
||||
stack = Arrays.copyOf(stack, newLength);
|
||||
pathIndices = Arrays.copyOf(pathIndices, 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 XmlWriter(NativeXmlWriter writer) {
|
||||
this.writer = Objects.requireNonNull(writer);
|
||||
}
|
||||
|
||||
public class XmlWriter extends SerializeWriter<IOException, XmlWriter> {
|
||||
public XmlWriter(Writer target) {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
this(new NativeXmlWriter(target));
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlWriter setLenient(boolean lenient) {
|
||||
super.setLenient(lenient);
|
||||
writer.setLenient(lenient);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLenient() {
|
||||
return writer.isLenient();
|
||||
}
|
||||
|
||||
public XmlWriter setHeuristics(Heuristics heuristics) {
|
||||
this.heuristics = Objects.requireNonNull(heuristics);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Heuristics getHeuristics() {
|
||||
return heuristics;
|
||||
}
|
||||
|
||||
public XmlWriter setNameEncoding(NameEncoding nameEncoding) {
|
||||
this.nameEncoding = Objects.requireNonNull(nameEncoding);
|
||||
return this;
|
||||
}
|
||||
|
||||
public NameEncoding getNameEncoding() {
|
||||
return nameEncoding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlWriter beginArray() throws IOException {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
beforeValue();
|
||||
writer.beginTag(nameEncoding.encode(consumeName()));
|
||||
push(ARRAY);
|
||||
pathIndices[stackSize - 1] = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlWriter endArray() throws IOException {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
int context = peek();
|
||||
if (context != ARRAY) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
if (deferredName != null) {
|
||||
if (lenient) nullValue();
|
||||
else throw new IllegalStateException("Dangling name: " + deferredName);
|
||||
}
|
||||
writer.endTag();
|
||||
stackSize--;
|
||||
pathNames[stackSize] = null;
|
||||
pathIndices[stackSize - 1]++;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlWriter beginObject() throws IOException {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
beforeValue();
|
||||
writer.beginTag(nameEncoding.encode(consumeName()));
|
||||
push(OBJECT);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlWriter endObject() throws IOException {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
int context = peek();
|
||||
if (context != OBJECT) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
if (deferredName != null) {
|
||||
if (lenient) nullValue();
|
||||
else throw new IllegalStateException("Dangling name: " + deferredName);
|
||||
}
|
||||
writer.endTag();
|
||||
stackSize--;
|
||||
pathNames[stackSize] = null;
|
||||
pathIndices[stackSize - 1]++;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlWriter comment(String comment) throws IOException {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
writer.comment(comment);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlWriter name(String name) throws IOException {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
Objects.requireNonNull(name, "name == null");
|
||||
if (deferredName != null) {
|
||||
throw new IllegalStateException("Already wrote a name, expecting a value.");
|
||||
}
|
||||
int context = peek();
|
||||
if (context == OBJECT || context == DOCUMENT || context == ARRAY) {
|
||||
deferredName = name;
|
||||
return this;
|
||||
} else throw new IllegalStateException("Name cannot be used in this context.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlWriter value(String value) throws IOException {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
return literalValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlWriter nullValue() throws IOException {
|
||||
beforeValue();
|
||||
String name = consumeName();
|
||||
if (pathIndices[stackSize - 1] == 0 && heuristics.shouldUseAttribute(name)) {
|
||||
writer.attribute(nameEncoding.encode(name), null);
|
||||
} else {
|
||||
writer.beginTag(nameEncoding.encode(name));
|
||||
writer.endTag();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlWriter literalValue(String value) throws IOException {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
beforeValue();
|
||||
String name = consumeName();
|
||||
if (pathIndices[stackSize - 1] == 0 && heuristics.shouldUseAttribute(name)) {
|
||||
writer.attribute(name, value);
|
||||
} else if (heuristics.shouldUseCData(value, name)) {
|
||||
writer.beginTag(nameEncoding.encode(name));
|
||||
writer.cdata(value);
|
||||
writer.endTag();
|
||||
} else {
|
||||
writer.beginTag(nameEncoding.encode(name));
|
||||
writer.text(value);
|
||||
writer.endTag();
|
||||
}
|
||||
pathIndices[stackSize - 1]++;
|
||||
return this;
|
||||
}
|
||||
|
||||
private void beforeValue() throws IOException {
|
||||
switch (peek()) {
|
||||
case DOCUMENT, ARRAY, OBJECT_VALUE_WRAPPER -> {}
|
||||
case OBJECT -> {
|
||||
if (deferredName == null) throw new IllegalStateException("Name not set.");
|
||||
}
|
||||
default -> throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
}
|
||||
|
||||
private String consumeName() throws IOException {
|
||||
if (deferredName != null) {
|
||||
String result = deferredName;
|
||||
deferredName = null;
|
||||
pathNames[stackSize - 1] = result;
|
||||
return result;
|
||||
}
|
||||
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
|
||||
result.append('[').append(pathIndex).append(']');
|
||||
}
|
||||
case WrapperScope.OBJECT -> {
|
||||
result.append('.');
|
||||
if (pathNames[i] != null) {
|
||||
result.append(pathNames[i]);
|
||||
}
|
||||
}
|
||||
case WrapperScope.DOCUMENT -> {}
|
||||
default -> throw new AssertionError("Unknown scope value: " + scope);
|
||||
}
|
||||
}
|
||||
String guess = heuristics.guessArrayElementName(result.toString());
|
||||
pathNames[stackSize - 1] = guess;
|
||||
return guess;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
writer.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
writer.close();
|
||||
}
|
||||
|
||||
public interface Heuristics {
|
||||
boolean shouldUseAttribute(String path);
|
||||
boolean shouldUseCData(String value, String path);
|
||||
String guessArrayElementName(String path);
|
||||
|
||||
Heuristics DEFAULT = new Heuristics() {
|
||||
@Override
|
||||
public boolean shouldUseAttribute(String path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldUseCData(String value, String path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String guessArrayElementName(String path) {
|
||||
return "item";
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -365,4 +365,62 @@ public final class NativeXmlWriterTest {
|
|||
writer.close();
|
||||
writer.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetGetFormattingStyle() throws IOException {
|
||||
String lineSeparator = "\r\n";
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
NativeXmlWriter jsonWriter = new NativeXmlWriter(stringWriter);
|
||||
// Default should be FormattingStyle.COMPACT
|
||||
jsonWriter.setIndent(" \t ").setNewline(lineSeparator);
|
||||
|
||||
jsonWriter.beginTag("tag");
|
||||
jsonWriter.text("true");
|
||||
jsonWriter.text("text");
|
||||
jsonWriter.text("5.0");
|
||||
jsonWriter.text(null);
|
||||
jsonWriter.endTag();
|
||||
|
||||
String expected = """
|
||||
<tag>\r
|
||||
\t true\r
|
||||
\t <!---->\r
|
||||
\t text\r
|
||||
\t <!---->\r
|
||||
\t 5.0\r
|
||||
\t <!---->\r
|
||||
\t null\r
|
||||
</tag>""";
|
||||
assertThat(stringWriter.toString()).isEqualTo(expected);
|
||||
|
||||
assertThat(jsonWriter.getNewline()).isEqualTo(lineSeparator);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIndentOverwritesFormattingStyle() throws IOException {
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
NativeXmlWriter jsonWriter = new NativeXmlWriter(stringWriter);
|
||||
// Should overwrite formatting style
|
||||
jsonWriter.setIndent(" ");
|
||||
|
||||
jsonWriter.beginTag("tag");
|
||||
jsonWriter.attributeName("a");
|
||||
jsonWriter.attributeValue("b");
|
||||
jsonWriter.beginTag("tag");
|
||||
jsonWriter.text("1");
|
||||
jsonWriter.text("2");
|
||||
jsonWriter.endTag();
|
||||
jsonWriter.endTag();
|
||||
|
||||
String expected = """
|
||||
<tag a="b">
|
||||
<tag>
|
||||
1
|
||||
<!---->
|
||||
2
|
||||
</tag>
|
||||
</tag>""";
|
||||
assertThat(stringWriter.toString()).isEqualTo(expected);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
package io.gitlab.jfronny.commons.serialize.xml.test;
|
||||
|
||||
import io.gitlab.jfronny.commons.serialize.xml.wrapper.XmlWriter;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
public class XmlWriterTest {
|
||||
@Test
|
||||
public void testWriteArray() throws IOException {
|
||||
StringWriter writer = new StringWriter();
|
||||
XmlWriter xmlWriter = new XmlWriter(writer);
|
||||
|
||||
xmlWriter.beginArray();
|
||||
xmlWriter.name("mytem");
|
||||
xmlWriter.value("value");
|
||||
xmlWriter.value("value2");
|
||||
xmlWriter.endArray();
|
||||
|
||||
String expected = "<item><mytem>value</mytem><item>value2</item></item>";
|
||||
assertThat(writer.toString()).isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWriteObject() throws IOException {
|
||||
StringWriter writer = new StringWriter();
|
||||
XmlWriter xmlWriter = new XmlWriter(writer);
|
||||
|
||||
xmlWriter.name("w");
|
||||
xmlWriter.beginObject();
|
||||
xmlWriter.name("test");
|
||||
xmlWriter.value("value");
|
||||
xmlWriter.endObject();
|
||||
|
||||
String expected = "<w><test>value</test></w>";
|
||||
assertThat(writer.toString()).isEqualTo(expected);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue