feat(serialize): initial prototype for XML SerializeReader
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
840f154110
commit
4a1944f792
@ -0,0 +1,9 @@
|
||||
package io.gitlab.jfronny.commons.serialize.xml.impl;
|
||||
|
||||
public class WrapperScope {
|
||||
public static final int TAG_HEAD = 1;
|
||||
public static final int TAG_HEAD_DANGLING_NAME = 2;
|
||||
public static final int TAG_BODY_ARRAY = 3;
|
||||
public static final int TAG_BODY_OBJECT = 4;
|
||||
public static final int DOCUMENT = 6;
|
||||
}
|
@ -1,96 +1,262 @@
|
||||
package io.gitlab.jfronny.commons.serialize.xml.wrapper;
|
||||
|
||||
import io.gitlab.jfronny.commons.data.LazilyParsedNumber;
|
||||
import io.gitlab.jfronny.commons.serialize.MalformedDataException;
|
||||
import io.gitlab.jfronny.commons.serialize.SerializeReader;
|
||||
import io.gitlab.jfronny.commons.serialize.Token;
|
||||
import io.gitlab.jfronny.commons.serialize.xml.NativeXmlReader;
|
||||
import io.gitlab.jfronny.commons.serialize.xml.XmlToken;
|
||||
import io.gitlab.jfronny.commons.serialize.xml.impl.WrapperScope;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public class XmlReader extends SerializeReader<IOException, XmlReader> {
|
||||
public class XmlReader extends SerializeReader<IOException, XmlReader> implements Closeable {
|
||||
private final NativeXmlReader reader;
|
||||
private int[] stack = new int[32];
|
||||
private int stackSize = 0;
|
||||
private String[] pathNames = new String[32];
|
||||
private int[] pathIndices = new int[32];
|
||||
private Heuristics heuristics = Heuristics.DEFAULT;
|
||||
private String nextTagNamePath = null;
|
||||
private String nextTagName = null;
|
||||
|
||||
{
|
||||
stack[stackSize++] = WrapperScope.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;
|
||||
}
|
||||
|
||||
public XmlReader(NativeXmlReader reader) {
|
||||
this.reader = Objects.requireNonNull(reader);
|
||||
this.heuristics = Objects.requireNonNull(heuristics);
|
||||
}
|
||||
|
||||
public XmlReader(Reader source) {
|
||||
this(new NativeXmlReader(source));
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlReader setLenient(boolean lenient) {
|
||||
reader.setLenient(lenient);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLenient() {
|
||||
return reader.isLenient();
|
||||
}
|
||||
|
||||
public XmlReader setHeuristics(Heuristics heuristics) {
|
||||
this.heuristics = Objects.requireNonNull(heuristics);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Heuristics getHeuristics() {
|
||||
return heuristics;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlReader beginArray() throws IOException {
|
||||
return null;
|
||||
if (nextTagName != null || stack[stackSize - 1] == WrapperScope.TAG_HEAD) {
|
||||
// Tag was just created, interpret it as an array
|
||||
nextTagName = null;
|
||||
stack[stackSize - 1] = WrapperScope.TAG_BODY_ARRAY;
|
||||
return this;
|
||||
} else if (stack[stackSize - 1] == WrapperScope.TAG_BODY_ARRAY) {
|
||||
// We are inside an array, interpret the next tag as the root of our array
|
||||
reader.beginTag();
|
||||
push(WrapperScope.TAG_BODY_ARRAY);
|
||||
return this;
|
||||
} else {
|
||||
throw unexpectedTokenError("an array");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlReader endArray() throws IOException {
|
||||
return null;
|
||||
if (nextTagName != null) throw unexpectedTokenError("the end of an array");
|
||||
if (stack[stackSize - 1] == WrapperScope.TAG_BODY_ARRAY) {
|
||||
reader.endTag();
|
||||
stackSize--;
|
||||
return this;
|
||||
} else {
|
||||
throw unexpectedTokenError("the end of an array");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlReader beginObject() throws IOException {
|
||||
return null;
|
||||
if (nextTagName != null || stack[stackSize - 1] == WrapperScope.TAG_HEAD) {
|
||||
// Tag was just created, interpret it as an object
|
||||
nextTagName = null;
|
||||
stack[stackSize - 1] = WrapperScope.TAG_BODY_OBJECT;
|
||||
return this;
|
||||
} else if (stack[stackSize - 1] == WrapperScope.TAG_BODY_ARRAY) {
|
||||
// We are inside an array, interpret the next tag as the root of our object
|
||||
reader.beginTag();
|
||||
push(WrapperScope.TAG_BODY_OBJECT);
|
||||
return this;
|
||||
} else {
|
||||
throw unexpectedTokenError("an object");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlReader endObject() throws IOException {
|
||||
return null;
|
||||
if (nextTagName != null) throw unexpectedTokenError("the end of an object");
|
||||
if (stack[stackSize - 1] == WrapperScope.TAG_BODY_OBJECT) {
|
||||
reader.endTag();
|
||||
stackSize--;
|
||||
return this;
|
||||
} else {
|
||||
throw unexpectedTokenError("the end of an object");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext() throws IOException {
|
||||
return false;
|
||||
return nextTagName != null || reader.hasNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Token peek() throws IOException {
|
||||
return null;
|
||||
if (nextTagName != null) {
|
||||
return switch (heuristics.guessKind(reader.getPath())) {
|
||||
case OBJECT -> Token.BEGIN_OBJECT;
|
||||
case ARRAY -> Token.BEGIN_ARRAY;
|
||||
};
|
||||
}
|
||||
return switch (reader.peek()) {
|
||||
case ATTRIBUTE_NAME -> Token.NAME;
|
||||
case ATTRIBUTE_VALUE, TEXT, CDATA -> Token.STRING;
|
||||
case BEGIN_TAG -> {
|
||||
nextTagNamePath = getPath();
|
||||
nextTagName = reader.beginTag();
|
||||
yield peek();
|
||||
}
|
||||
case END_TAG -> stack[stackSize - 1] == WrapperScope.TAG_BODY_ARRAY ? Token.END_ARRAY : Token.END_OBJECT;
|
||||
case EOF -> Token.END_DOCUMENT;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nextName() throws IOException {
|
||||
return "";
|
||||
if (nextTagName != null) {
|
||||
String res = nextTagName;
|
||||
pathNames[stackSize - 1] = res;
|
||||
push(WrapperScope.TAG_HEAD);
|
||||
nextTagName = null;
|
||||
return res;
|
||||
}
|
||||
if (reader.peek() == XmlToken.ATTRIBUTE_NAME) {
|
||||
stack[stackSize - 1] = WrapperScope.TAG_HEAD_DANGLING_NAME;
|
||||
return reader.nextAttributeName();
|
||||
} else if (reader.peek() == XmlToken.BEGIN_TAG) {
|
||||
// ordinarily, this would also require a check whether we are in an object,
|
||||
// but doing it this way provides users with more flexibility
|
||||
String res = reader.beginTag();
|
||||
pathNames[stackSize - 1] = res;
|
||||
push(WrapperScope.TAG_HEAD);
|
||||
return res;
|
||||
} else {
|
||||
throw unexpectedTokenError("a name");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nextString() throws IOException {
|
||||
return "";
|
||||
return nextValue("a string");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean nextBoolean() throws IOException {
|
||||
return false;
|
||||
String res = nextValue("a boolean");
|
||||
if (res.equalsIgnoreCase("true")) return true;
|
||||
if (res.equalsIgnoreCase("false")) return false;
|
||||
throw unexpectedTokenError("a boolean");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nextNull() throws IOException {
|
||||
|
||||
String res = nextValue("null");
|
||||
if (!res.equalsIgnoreCase("null")) throw unexpectedTokenError("null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number nextNumber() throws IOException {
|
||||
return null;
|
||||
String res = nextValue("a number");
|
||||
LazilyParsedNumber number = new LazilyParsedNumber(res);
|
||||
if (!serializeSpecialFloatingPointValues && (res.equals("NaN") || res.equals("Infinity") || res.equals("-Infinity"))) {
|
||||
throw new IllegalStateException("Special floating point values are not allowed: " + res);
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void skipValue() throws IOException {
|
||||
nextValue("a value");
|
||||
}
|
||||
|
||||
private String nextValue(String kind) throws IOException {
|
||||
if (nextTagName != null) throw unexpectedTokenError(kind);
|
||||
return switch (reader.peek()) {
|
||||
case ATTRIBUTE_VALUE -> {
|
||||
stack[stackSize - 1] = WrapperScope.TAG_HEAD;
|
||||
yield reader.nextAttributeValue();
|
||||
}
|
||||
case TEXT -> reader.nextText();
|
||||
case CDATA -> reader.nextCData();
|
||||
case BEGIN_TAG, END_TAG, ATTRIBUTE_NAME, EOF -> throw unexpectedTokenError(kind);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPath() {
|
||||
return "";
|
||||
return nextTagName == null ? reader.getPath() : nextTagNamePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPreviousPath() {
|
||||
return "";
|
||||
return getPath(); // TODO this should be different when handling arrays
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
public void close() throws IOException {
|
||||
nextTagName = null;
|
||||
reader.close();
|
||||
}
|
||||
|
||||
public interface Heuristics {
|
||||
enum Kind {OBJECT, ARRAY}
|
||||
Kind guessKind(String path);
|
||||
|
||||
Heuristics DEFAULT = path -> {
|
||||
if (path.endsWith("s")) return Kind.ARRAY;
|
||||
return Kind.OBJECT;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws a new {@link MalformedDataException} with the given message and information about the
|
||||
* current location.
|
||||
*/
|
||||
private MalformedDataException syntaxError(String message) throws MalformedDataException {
|
||||
throw new MalformedDataException(message + locationString());
|
||||
}
|
||||
|
||||
private IllegalStateException unexpectedTokenError(String expected) throws IOException {
|
||||
return new IllegalStateException("Expected " + expected + " but was " + peek() + locationString());
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user