282 lines
9.6 KiB
Java
282 lines
9.6 KiB
Java
package io.gitlab.jfronny.commons.serialize.emulated;
|
|
|
|
import io.gitlab.jfronny.commons.serialize.MalformedDataException;
|
|
import io.gitlab.jfronny.commons.serialize.SerializeReader;
|
|
import io.gitlab.jfronny.commons.serialize.Token;
|
|
|
|
import java.io.IOException;
|
|
import java.util.Arrays;
|
|
import java.util.Iterator;
|
|
import java.util.Map;
|
|
|
|
public class EmulatedReader extends SerializeReader<MalformedDataException, EmulatedReader> {
|
|
private static final Object SENTINEL_CLOSED = new Object();
|
|
|
|
/*
|
|
* The nesting stack. Using a manual array rather than an ArrayList saves 20%.
|
|
*/
|
|
private Object[] stack = new Object[32];
|
|
private int stackSize = 0;
|
|
|
|
/*
|
|
* The path members. It corresponds directly to stack: At indices where the
|
|
* stack contains an object (EMPTY_OBJECT, DANGLING_NAME or NONEMPTY_OBJECT),
|
|
* pathNames contains the name at this scope. Where it contains an array
|
|
* (EMPTY_ARRAY, NONEMPTY_ARRAY) pathIndices contains the current index in
|
|
* that array. Otherwise the value is undefined, and we take advantage of that
|
|
* by incrementing pathIndices when doing so isn't useful.
|
|
*/
|
|
private String[] pathNames = new String[32];
|
|
private int[] pathIndices = new int[32];
|
|
|
|
public EmulatedReader(DataElement element) {
|
|
push(element);
|
|
}
|
|
|
|
private void push(Object 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;
|
|
}
|
|
|
|
private Object peekStack() {
|
|
return stack[stackSize - 1];
|
|
}
|
|
|
|
private Object popStack() {
|
|
Object result = stack[--stackSize];
|
|
stack[stackSize] = null;
|
|
return result;
|
|
}
|
|
|
|
private void expect(Token expected) throws MalformedDataException {
|
|
if (peek() != expected) {
|
|
throw new IllegalStateException(
|
|
"Expected " + expected + " but was " + peek() + locationString());
|
|
}
|
|
}
|
|
|
|
private String nextName(boolean skipName) throws MalformedDataException {
|
|
expect(Token.NAME);
|
|
Iterator<?> i = (Iterator<?>) peekStack();
|
|
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) i.next();
|
|
String result = (String) entry.getKey();
|
|
pathNames[stackSize - 1] = skipName ? "<skipped>" : result;
|
|
push(entry.getValue());
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public EmulatedReader beginArray() throws MalformedDataException {
|
|
expect(Token.BEGIN_ARRAY);
|
|
DataElement.Array array = (DataElement.Array) peekStack();
|
|
push(array.elements().iterator());
|
|
pathIndices[stackSize - 1] = 0;
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public EmulatedReader endArray() throws MalformedDataException {
|
|
expect(Token.END_ARRAY);
|
|
popStack(); // empty iterator
|
|
popStack(); // array
|
|
if (stackSize > 0) {
|
|
pathIndices[stackSize - 1]++;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public EmulatedReader beginObject() throws MalformedDataException {
|
|
expect(Token.BEGIN_OBJECT);
|
|
DataElement.Object object = (DataElement.Object) peekStack();
|
|
push(object.members().entrySet().iterator());
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public EmulatedReader endObject() throws MalformedDataException {
|
|
expect(Token.END_OBJECT);
|
|
pathNames[stackSize - 1] = null; // Free the last path name so that it can be garbage collected
|
|
popStack(); // empty iterator
|
|
popStack(); // object
|
|
if (stackSize > 0) {
|
|
pathIndices[stackSize - 1]++;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public boolean hasNext() throws MalformedDataException {
|
|
Token token = peek();
|
|
return token != Token.END_OBJECT
|
|
&& token != Token.END_ARRAY
|
|
&& token != Token.END_DOCUMENT;
|
|
}
|
|
|
|
@Override
|
|
public Token peek() throws MalformedDataException {
|
|
if (stackSize == 0) {
|
|
return Token.END_DOCUMENT;
|
|
}
|
|
|
|
Object o = peekStack();
|
|
if (o instanceof Iterator) {
|
|
boolean isObject = stack[stackSize - 2] instanceof DataElement.Object;
|
|
Iterator<?> iterator = (Iterator<?>) o;
|
|
if (iterator.hasNext()) {
|
|
if (isObject) {
|
|
return Token.NAME;
|
|
} else {
|
|
push(iterator.next());
|
|
return peek();
|
|
}
|
|
} else {
|
|
return isObject ? Token.END_OBJECT : Token.END_ARRAY;
|
|
}
|
|
} else if (o instanceof DataElement e) {
|
|
return switch (e) {
|
|
case DataElement.Object l -> Token.BEGIN_OBJECT;
|
|
case DataElement.Array l -> Token.BEGIN_ARRAY;
|
|
case DataElement.Primitive p -> switch (p) {
|
|
case DataElement.Primitive.String s -> Token.STRING;
|
|
case DataElement.Primitive.Boolean b -> Token.BOOLEAN;
|
|
case DataElement.Primitive.Number n -> Token.NUMBER;
|
|
};
|
|
case DataElement.Null l -> Token.NULL;
|
|
};
|
|
} else if (o == SENTINEL_CLOSED) {
|
|
throw new IllegalStateException("JsonReader is closed");
|
|
} else {
|
|
throw new MalformedDataException("Custom JsonElement subclass " + o.getClass().getName() + " is not supported");
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String nextName() throws MalformedDataException {
|
|
return nextName(false);
|
|
}
|
|
|
|
@Override
|
|
public String nextString() throws MalformedDataException {
|
|
Token token = peek();
|
|
if (token != Token.STRING && token != Token.NUMBER) {
|
|
throw new IllegalStateException(
|
|
"Expected " + Token.STRING + " but was " + token + locationString());
|
|
}
|
|
String result = ((DataElement.Primitive) popStack()).asString();
|
|
if (stackSize > 0) {
|
|
pathIndices[stackSize - 1]++;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@Override
|
|
public boolean nextBoolean() throws MalformedDataException {
|
|
expect(Token.BOOLEAN);
|
|
if (!(popStack() instanceof DataElement.Primitive.Boolean result)) {
|
|
throw new IllegalStateException("Expected a boolean");
|
|
}
|
|
if (stackSize > 0) {
|
|
pathIndices[stackSize - 1]++;
|
|
}
|
|
return result.value();
|
|
}
|
|
|
|
@Override
|
|
public void nextNull() throws MalformedDataException {
|
|
expect(Token.NULL);
|
|
popStack();
|
|
if (stackSize > 0) {
|
|
pathIndices[stackSize - 1]++;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Number nextNumber() throws MalformedDataException {
|
|
Token token = peek();
|
|
if (token != Token.NUMBER && token != Token.STRING) {
|
|
throw new IllegalStateException(
|
|
"Expected " + Token.NUMBER + " but was " + token + locationString());
|
|
}
|
|
if (!(popStack() instanceof DataElement.Primitive.Number result)) {
|
|
throw new IllegalStateException("Expected a number");
|
|
}
|
|
if (stackSize > 0) {
|
|
pathIndices[stackSize - 1]++;
|
|
}
|
|
return result.value();
|
|
}
|
|
|
|
@Override
|
|
public void skipValue() throws MalformedDataException {
|
|
Token peeked = peek();
|
|
switch (peeked) {
|
|
case NAME:
|
|
@SuppressWarnings("unused")
|
|
String unused = nextName(true);
|
|
break;
|
|
case END_ARRAY:
|
|
endArray();
|
|
throw new IllegalStateException("Attempt to skip led outside its parent");
|
|
case END_OBJECT:
|
|
endObject();
|
|
throw new IllegalStateException("Attempt to skip led outside its parent");
|
|
case END_DOCUMENT:
|
|
throw new IllegalStateException("Attempt to skip led outside the document");
|
|
default:
|
|
popStack();
|
|
if (stackSize > 0) {
|
|
pathIndices[stackSize - 1]++;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private String getPath(boolean usePreviousPath) {
|
|
StringBuilder result = new StringBuilder().append('$');
|
|
for (int i = 0; i < stackSize; i++) {
|
|
if (stack[i] instanceof DataElement.Array) {
|
|
if (++i < stackSize && stack[i] instanceof Iterator) {
|
|
int pathIndex = pathIndices[i];
|
|
// If index is last path element it points to next array element; have to decrement
|
|
// `- 1` covers case where iterator for next element is on stack
|
|
// `- 2` covers case where peek() already pushed next element onto stack
|
|
if (usePreviousPath && pathIndex > 0 && (i == stackSize - 1 || i == stackSize - 2)) {
|
|
pathIndex--;
|
|
}
|
|
result.append('[').append(pathIndex).append(']');
|
|
}
|
|
} else if (stack[i] instanceof DataElement.Object) {
|
|
if (++i < stackSize && stack[i] instanceof Iterator) {
|
|
result.append('.');
|
|
if (pathNames[i] != null) {
|
|
result.append(pathNames[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result.toString();
|
|
}
|
|
|
|
@Override
|
|
public String getPath() {
|
|
return getPath(false);
|
|
}
|
|
|
|
@Override
|
|
public String getPreviousPath() {
|
|
return getPath(true);
|
|
}
|
|
|
|
@Override
|
|
public void close() {
|
|
stack = new Object[] {SENTINEL_CLOSED};
|
|
stackSize = 1;
|
|
}
|
|
}
|