Compare commits

...

8 Commits

7 changed files with 356 additions and 17 deletions

View File

@ -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());
}

View File

@ -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);

View File

@ -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) {

View File

@ -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;

View File

@ -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";
}
};
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}