2024-04-13 20:41:13 +02:00
|
|
|
package io.gitlab.jfronny.commons.serialize.xml.wrapper;
|
|
|
|
|
2024-04-13 21:48:19 +02:00
|
|
|
import io.gitlab.jfronny.commons.data.LazilyParsedNumber;
|
|
|
|
import io.gitlab.jfronny.commons.serialize.MalformedDataException;
|
2024-04-13 20:41:13 +02:00
|
|
|
import io.gitlab.jfronny.commons.serialize.SerializeReader;
|
|
|
|
import io.gitlab.jfronny.commons.serialize.Token;
|
|
|
|
import io.gitlab.jfronny.commons.serialize.xml.NativeXmlReader;
|
2024-04-13 21:48:19 +02:00
|
|
|
import io.gitlab.jfronny.commons.serialize.xml.XmlToken;
|
|
|
|
import io.gitlab.jfronny.commons.serialize.xml.impl.WrapperScope;
|
2024-04-13 20:41:13 +02:00
|
|
|
|
2024-04-13 21:48:19 +02:00
|
|
|
import java.io.Closeable;
|
2024-04-13 20:41:13 +02:00
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.Reader;
|
2024-04-13 21:48:19 +02:00
|
|
|
import java.util.Arrays;
|
2024-04-13 20:41:13 +02:00
|
|
|
import java.util.Objects;
|
|
|
|
|
2024-04-13 21:48:19 +02:00
|
|
|
public class XmlReader extends SerializeReader<IOException, XmlReader> implements Closeable {
|
2024-04-13 20:41:13 +02:00
|
|
|
private final NativeXmlReader reader;
|
2024-04-13 21:48:19 +02:00
|
|
|
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;
|
|
|
|
}
|
2024-04-13 20:41:13 +02:00
|
|
|
|
|
|
|
public XmlReader(NativeXmlReader reader) {
|
|
|
|
this.reader = Objects.requireNonNull(reader);
|
2024-04-13 21:48:19 +02:00
|
|
|
this.heuristics = Objects.requireNonNull(heuristics);
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public XmlReader(Reader source) {
|
|
|
|
this(new NativeXmlReader(source));
|
|
|
|
}
|
|
|
|
|
2024-04-13 21:48:19 +02:00
|
|
|
@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;
|
|
|
|
}
|
|
|
|
|
2024-04-13 20:41:13 +02:00
|
|
|
@Override
|
|
|
|
public XmlReader beginArray() throws IOException {
|
2024-04-13 21:48:19 +02:00
|
|
|
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");
|
|
|
|
}
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public XmlReader endArray() throws IOException {
|
2024-04-13 21:48:19 +02:00
|
|
|
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");
|
|
|
|
}
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public XmlReader beginObject() throws IOException {
|
2024-04-13 21:48:19 +02:00
|
|
|
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");
|
|
|
|
}
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public XmlReader endObject() throws IOException {
|
2024-04-13 21:48:19 +02:00
|
|
|
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");
|
|
|
|
}
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean hasNext() throws IOException {
|
2024-04-13 21:48:19 +02:00
|
|
|
return nextTagName != null || reader.hasNext();
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Token peek() throws IOException {
|
2024-04-13 21:48:19 +02:00
|
|
|
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;
|
|
|
|
};
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public String nextName() throws IOException {
|
2024-04-13 21:48:19 +02:00
|
|
|
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");
|
|
|
|
}
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public String nextString() throws IOException {
|
2024-04-13 21:48:19 +02:00
|
|
|
return nextValue("a string");
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean nextBoolean() throws IOException {
|
2024-04-13 21:48:19 +02:00
|
|
|
String res = nextValue("a boolean");
|
|
|
|
if (res.equalsIgnoreCase("true")) return true;
|
|
|
|
if (res.equalsIgnoreCase("false")) return false;
|
|
|
|
throw unexpectedTokenError("a boolean");
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void nextNull() throws IOException {
|
2024-04-13 21:48:19 +02:00
|
|
|
String res = nextValue("null");
|
|
|
|
if (!res.equalsIgnoreCase("null")) throw unexpectedTokenError("null");
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Number nextNumber() throws IOException {
|
2024-04-13 21:48:19 +02:00
|
|
|
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;
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void skipValue() throws IOException {
|
2024-04-13 21:48:19 +02:00
|
|
|
nextValue("a value");
|
|
|
|
}
|
2024-04-13 20:41:13 +02:00
|
|
|
|
2024-04-13 21:48:19 +02:00
|
|
|
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);
|
|
|
|
};
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public String getPath() {
|
2024-04-13 21:48:19 +02:00
|
|
|
return nextTagName == null ? reader.getPath() : nextTagNamePath;
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public String getPreviousPath() {
|
2024-04-13 21:48:19 +02:00
|
|
|
return getPath(); // TODO this should be different when handling arrays
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2024-04-13 21:48:19 +02:00
|
|
|
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());
|
|
|
|
}
|
2024-04-13 20:41:13 +02:00
|
|
|
|
2024-04-13 21:48:19 +02:00
|
|
|
private IllegalStateException unexpectedTokenError(String expected) throws IOException {
|
|
|
|
return new IllegalStateException("Expected " + expected + " but was " + peek() + locationString());
|
2024-04-13 20:41:13 +02:00
|
|
|
}
|
|
|
|
}
|