553 lines
18 KiB
Java
553 lines
18 KiB
Java
package it.cavallium.strangedb.server;
|
|
|
|
import com.google.common.io.ByteArrayDataOutput;
|
|
import com.google.common.io.ByteStreams;
|
|
import it.cavallium.strangedb.database.IReferencesIO;
|
|
import it.cavallium.strangedb.database.references.DatabaseReferencesMetadata;
|
|
import org.json.JSONArray;
|
|
import org.json.JSONObject;
|
|
|
|
import java.io.IOException;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
|
|
import static it.cavallium.strangedb.server.TreePath.stringToNode;
|
|
|
|
public class DatabaseNodesIO {
|
|
private final IReferencesIO referencesIO;
|
|
private static final int isArrayMask = 0b10000000000000000000000000000000;
|
|
private static final int isValueMask = 0b01000000000000000000000000000000;
|
|
private static final int isValueNumberMask = 0b00000000000000000000000000000001;
|
|
private static final int isValueStringMask = 0b00000000000000000000000000000010;
|
|
private static final int isValueBooleanMask = 0b00000000000000000000000000000100;
|
|
private static final int nodeCountMask = 0b01111111111111111111111111111111;
|
|
private static final int arrayCountMask = 0b01111111111111111111111111111111;
|
|
|
|
public DatabaseNodesIO(IReferencesIO referencesIO, DatabaseReferencesMetadata referencesMetadata) throws IOException {
|
|
this.referencesIO = referencesIO;
|
|
if (referencesMetadata.getFirstFreeReference() == 0) {
|
|
long ref = this.referencesIO.allocateReference();
|
|
if (ref != 0) throw new IOException("Root must be 0");
|
|
ClassNode rootNode = new ClassNode(ref, new NodeProperty[0]);
|
|
this.setNode(rootNode);
|
|
}
|
|
}
|
|
|
|
public String get(CharSequence path) throws IOException {
|
|
return get(TreePath.get(path));
|
|
}
|
|
|
|
public String get(TreePath path) throws IOException {
|
|
try {
|
|
Node foundNode = getNode(path);
|
|
return toJson(foundNode);
|
|
} catch (NullPointerException ex) {
|
|
return "null";
|
|
}
|
|
}
|
|
|
|
private String toJson(Node foundNode) throws IOException {
|
|
StringBuilder sb = new StringBuilder();
|
|
toJson(sb, foundNode);
|
|
return sb.toString();
|
|
}
|
|
|
|
private void toJson(StringBuilder sb, long nodeReference) throws IOException {
|
|
toJson(sb, loadNode(nodeReference));
|
|
}
|
|
|
|
private void toJson(StringBuilder sb, Node foundNode) throws IOException {
|
|
switch (foundNode.getType()) {
|
|
case ARRAY: {
|
|
ArrayNode arrayNode = (ArrayNode) foundNode;
|
|
sb.append('[');
|
|
for (int i = 0; i < arrayNode.countItems(); i++) {
|
|
toJson(sb, arrayNode.getItem(i));
|
|
if (i + 1 < arrayNode.countItems()) {
|
|
sb.append(',');
|
|
}
|
|
}
|
|
sb.append(']');
|
|
break;
|
|
}
|
|
case CLASS: {
|
|
ClassNode classNode = (ClassNode) foundNode;
|
|
sb.append('{');
|
|
for (int i = 0; i < classNode.countProperties(); i++) {
|
|
NodeProperty property = classNode.getProperty(i);
|
|
sb.append(property.getNameAsString());
|
|
sb.append(':');
|
|
toJson(sb, property.getReference());
|
|
if (i + 1 < classNode.countProperties()) {
|
|
sb.append(',');
|
|
}
|
|
}
|
|
sb.append('}');
|
|
break;
|
|
}
|
|
case VALUE: {
|
|
ValueNode valueNode = (ValueNode) foundNode;
|
|
sb.append(loadValue(valueNode));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void setValue(long reference, String value) throws IOException {
|
|
ByteBuffer valueBytes = StandardCharsets.UTF_8.encode(value);
|
|
referencesIO.writeToReference(reference, valueBytes.limit(), valueBytes);
|
|
}
|
|
|
|
private String loadValue(ValueNode valueNode) throws IOException {
|
|
ByteBuffer buffer = referencesIO.readFromReference(valueNode.getReference());
|
|
ByteBuffer valueBuffer = referencesIO.readFromReference(valueNode.getValueReference());
|
|
switch (valueNode.getValueType()) {
|
|
case STRING:
|
|
return JSONObject.quote(StandardCharsets.UTF_8.decode(valueBuffer).toString());
|
|
case NUMBER:
|
|
return JSONObject.doubleToString(valueBuffer.getDouble());
|
|
case BOOLEAN:
|
|
return valueBuffer.get() == 1 ? "true" : "false";
|
|
default:
|
|
throw new RuntimeException();
|
|
}
|
|
}
|
|
|
|
private double loadNumber(long reference) throws IOException {
|
|
ByteBuffer buffer = referencesIO.readFromReference(reference);
|
|
return buffer.getDouble();
|
|
}
|
|
|
|
public void set(CharSequence path, String value) throws IOException {
|
|
TreePath treePath = TreePath.get(path);
|
|
if (treePath.isRoot()) {
|
|
set(treePath, importNode(value, 0));
|
|
} else {
|
|
set(treePath, importNode(value));
|
|
}
|
|
}
|
|
|
|
public void set(TreePath path, long valueReference) throws IOException {
|
|
Node node;
|
|
if (path.isRoot()) {
|
|
if (valueReference != 0)
|
|
throw new IOException("Root must be zero");
|
|
} else {
|
|
node = getNode(path.getParent());
|
|
switch (node.getType()) {
|
|
case ARRAY: {
|
|
if (!path.isArrayOffset()) {
|
|
throw new IOException("Required a property inside an array node");
|
|
}
|
|
ArrayNode arrayNode = (ArrayNode) node;
|
|
int index = path.getArrayOffset();
|
|
arrayNode.setItem(index, valueReference);
|
|
break;
|
|
}
|
|
case CLASS: {
|
|
if (!path.isNodeProperty())
|
|
throw new IOException("Required array offset inside a non-array node");
|
|
ClassNode classNode = (ClassNode) node;
|
|
byte[] propertyName = path.getNodeValue();
|
|
classNode.setProperty(propertyName, valueReference);
|
|
break;
|
|
}
|
|
case VALUE: {
|
|
throw new IOException("WTF you shouldn't be here!");
|
|
}
|
|
}
|
|
setNode(node);
|
|
}
|
|
}
|
|
|
|
@Deprecated
|
|
private long importNode(String value) throws IOException {
|
|
return importNode(value, referencesIO.allocateReference());
|
|
}
|
|
|
|
private long importNode(String value, long reference) throws IOException {
|
|
switch (value.charAt(0)) {
|
|
case '[':
|
|
JSONArray array = new JSONArray(value);
|
|
return importJsonNode(array, reference);
|
|
case '{':
|
|
JSONObject obj = new JSONObject(value);
|
|
return importJsonNode(obj, reference);
|
|
case 'u':
|
|
if (value.equals("undefined"))
|
|
return importJsonNode(value, reference);
|
|
case 'N':
|
|
if (value.equals("NaN"))
|
|
return importJsonNode(value, reference);
|
|
case 'n':
|
|
if (value.equals("null"))
|
|
return importJsonNode(value, reference);
|
|
case '"':
|
|
return importJsonNode(value.substring(1, value.length() - 1), reference);
|
|
case '0':
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
case '.':
|
|
return importJsonNode(Double.parseDouble(value), reference);
|
|
case 't':
|
|
if (value.equals("true"))
|
|
return importJsonNode(true, reference);
|
|
case 'f':
|
|
if (value.equals("false"))
|
|
return importJsonNode(false, reference);
|
|
default:
|
|
throw new IOException("Unrecognized input");
|
|
}
|
|
}
|
|
|
|
@Deprecated
|
|
private long importJsonNode(Object o) throws IOException {
|
|
return importJsonNode(o, referencesIO.allocateReference());
|
|
}
|
|
|
|
private long importJsonNode(Object o, long reference) throws IOException {
|
|
if (o instanceof ArrayList<?>) {
|
|
o = new JSONArray((ArrayList<?>) o);
|
|
}
|
|
if (o instanceof HashMap<?, ?>) {
|
|
o = new JSONObject((HashMap<?, ?>) o);
|
|
}
|
|
if (o instanceof JSONArray) {
|
|
JSONArray array = (JSONArray) o;
|
|
int length = array.length();
|
|
long[] arrayReferences = new long[length];
|
|
for (int i = 0; i < length; i++) {
|
|
arrayReferences[i] = importJsonNode(array.get(i));
|
|
}
|
|
Node node = createNewArrayNode(arrayReferences, reference);
|
|
setNode(node);
|
|
return node.getReference();
|
|
} else if (o instanceof JSONObject) {
|
|
JSONObject obj = (JSONObject) o;
|
|
Map<String, Object> jsonProperties = obj.toMap();
|
|
NodeProperty[] properties = new NodeProperty[jsonProperties.size()];
|
|
int i = 0;
|
|
for (Map.Entry<String, Object> entry : jsonProperties.entrySet()) {
|
|
NodeProperty nodeProperty = new NodeProperty(stringToNode(entry.getKey()), importJsonNode(entry.getValue()));
|
|
properties[i] = nodeProperty;
|
|
i++;
|
|
}
|
|
Node node = createNewNodeNode(properties, reference);
|
|
setNode(node);
|
|
return node.getReference();
|
|
} else if (o instanceof String) {
|
|
Node node = createNewValueNode(o, ValueType.STRING, reference);
|
|
setNode(node);
|
|
return node.getReference();
|
|
} else if (o instanceof Double || o instanceof Float || o instanceof Integer || o instanceof Long) {
|
|
double number;
|
|
if (o instanceof Integer) {
|
|
number = (Integer) o;
|
|
} else if (o instanceof Long) {
|
|
number = (Long) o;
|
|
} else if (o instanceof Float) {
|
|
number = (Float) o;
|
|
} else {
|
|
number = (Double) o;
|
|
}
|
|
Node node = createNewValueNode(number, ValueType.NUMBER, reference);
|
|
setNode(node);
|
|
return node.getReference();
|
|
} else if (o instanceof Boolean) {
|
|
Node node = createNewValueNode(o, ValueType.BOOLEAN, reference);
|
|
setNode(node);
|
|
return node.getReference();
|
|
} else {
|
|
throw new RuntimeException();
|
|
}
|
|
}
|
|
|
|
public boolean exists(CharSequence path) throws IOException {
|
|
return exists(TreePath.get(path));
|
|
}
|
|
|
|
public boolean exists(TreePath path) throws IOException {
|
|
TreePathWalker pathWalker = new TreePathWalker(path);
|
|
pathWalker.walkToRoot();
|
|
Node node = loadNode(0);
|
|
while (pathWalker.hasNext()) {
|
|
TreePath nodePath = pathWalker.next();
|
|
if (nodePath.isArrayOffset()) {
|
|
if (node.getType() != NodeType.ARRAY) {
|
|
return false;
|
|
}
|
|
int offset = nodePath.getArrayOffset();
|
|
if (!((ArrayNode) node).hasItem(offset)) {
|
|
return false;
|
|
}
|
|
long nodeReference = ((ArrayNode) node).getItem(offset);
|
|
node = loadNode(nodeReference);
|
|
} else if (nodePath.isNodeProperty()) {
|
|
if (node.getType() == NodeType.ARRAY) {
|
|
return false;
|
|
}
|
|
byte[] propertyName = nodePath.getNodeValue();
|
|
if (!((ClassNode) node).hasProperty(propertyName)) {
|
|
return false;
|
|
}
|
|
long nodeReference = ((ClassNode) node).getProperty(propertyName);
|
|
node = loadNode(nodeReference);
|
|
}
|
|
}
|
|
return node != null;
|
|
}
|
|
|
|
public int size(CharSequence path) throws IOException {
|
|
return size(TreePath.get(path));
|
|
}
|
|
|
|
public int size(TreePath path) throws IOException {
|
|
TreePathWalker pathWalker = new TreePathWalker(path);
|
|
pathWalker.walkToRoot();
|
|
Node node = loadNode(0);
|
|
while (pathWalker.hasNext()) {
|
|
TreePath nodePath = pathWalker.next();
|
|
if (nodePath.isArrayOffset()) {
|
|
if (node.getType() != NodeType.ARRAY) {
|
|
throw new NullPointerException("Node not found");
|
|
}
|
|
int offset = nodePath.getArrayOffset();
|
|
if (!((ArrayNode) node).hasItem(offset)) {
|
|
throw new NullPointerException("Node not found");
|
|
}
|
|
long nodeReference = ((ArrayNode) node).getItem(offset);
|
|
node = loadNode(nodeReference);
|
|
} else if (nodePath.isNodeProperty()) {
|
|
if (node.getType() == NodeType.ARRAY) {
|
|
throw new NullPointerException("Node not found");
|
|
}
|
|
byte[] propertyName = nodePath.getNodeValue();
|
|
if (!((ClassNode) node).hasProperty(propertyName)) {
|
|
throw new NullPointerException("Node not found");
|
|
}
|
|
long nodeReference = ((ClassNode) node).getProperty(propertyName);
|
|
node = loadNode(nodeReference);
|
|
}
|
|
}
|
|
if (node != null) {
|
|
switch (node.getType()) {
|
|
case ARRAY:
|
|
return ((ArrayNode) node).countItems();
|
|
case CLASS:
|
|
return ((ClassNode) node).countProperties();
|
|
default:
|
|
throw new NullPointerException("You can't get the size of this node!");
|
|
}
|
|
} else {
|
|
throw new NullPointerException("Node not found");
|
|
}
|
|
}
|
|
|
|
private Node getNode(TreePath path) throws IOException {
|
|
TreePathWalker pathWalker = new TreePathWalker(path);
|
|
pathWalker.walkToRoot();
|
|
Node node = loadNode(0);
|
|
while (pathWalker.hasNext()) {
|
|
TreePath nodePath = pathWalker.next();
|
|
if (nodePath.isArrayOffset()) {
|
|
if (node.getType() != NodeType.ARRAY) {
|
|
throw new IOException("Required array offset inside a non-array node");
|
|
}
|
|
int offset = nodePath.getArrayOffset();
|
|
long nodeReference = ((ArrayNode) node).getItem(offset);
|
|
node = loadNode(nodeReference);
|
|
} else if (nodePath.isNodeProperty()) {
|
|
if (node.getType() == NodeType.ARRAY) {
|
|
throw new IOException("Required a property inside an array node");
|
|
}
|
|
byte[] propertyName = nodePath.getNodeValue();
|
|
long nodeReference = ((ClassNode) node).getProperty(propertyName);
|
|
node = loadNode(nodeReference);
|
|
}
|
|
}
|
|
if (node == null)
|
|
throw new NullPointerException();
|
|
return node;
|
|
}
|
|
|
|
private Node createNewArrayNode() throws IOException {
|
|
long reference = referencesIO.allocateReference();
|
|
return new ArrayNode(reference, new long[0]);
|
|
}
|
|
|
|
private Node createNewArrayNode(long[] items) throws IOException {
|
|
return createNewArrayNode(items, referencesIO.allocateReference());
|
|
}
|
|
|
|
private Node createNewArrayNode(long[] items, long reference) throws IOException {
|
|
return new ArrayNode(reference, items);
|
|
}
|
|
|
|
private Node createNewValueNode(ValueType type) throws IOException {
|
|
long reference = referencesIO.allocateReference();
|
|
return new ValueNode(reference, referencesIO.allocateReference(), type);
|
|
}
|
|
|
|
private Node createNewValueNode(String value) throws IOException {
|
|
return createNewValueNode(value, ValueType.STRING, referencesIO.allocateReference());
|
|
}
|
|
|
|
private Node createNewValueNode(double value) throws IOException {
|
|
return createNewValueNode(value, ValueType.NUMBER, referencesIO.allocateReference());
|
|
}
|
|
|
|
private Node createNewValueNode(Object value, ValueType valueType, long reference) throws IOException {
|
|
ByteBuffer buffer = null;
|
|
switch (valueType) {
|
|
case STRING:
|
|
buffer = StandardCharsets.UTF_8.encode((String) value);
|
|
break;
|
|
case NUMBER:
|
|
buffer = ByteBuffer.allocate(Double.BYTES);
|
|
buffer.putDouble((Double) value);
|
|
buffer.flip();
|
|
break;
|
|
case BOOLEAN:
|
|
buffer = ByteBuffer.allocate(Byte.BYTES);
|
|
buffer.put(((boolean) value) ? (byte) 1 : 0);
|
|
buffer.flip();
|
|
break;
|
|
default:
|
|
throw new RuntimeException();
|
|
}
|
|
long dataReference = -1;
|
|
if (buffer != null) {
|
|
dataReference = referencesIO.allocateReference(buffer.limit(), buffer);
|
|
}
|
|
return new ValueNode(reference, dataReference, valueType);
|
|
}
|
|
|
|
private Node createNewNodeNode() throws IOException {
|
|
return createNewNodeNode(referencesIO.allocateReference());
|
|
}
|
|
|
|
private Node createNewNodeNode(long reference) {
|
|
return createNewNodeNode(new NodeProperty[0], reference);
|
|
}
|
|
|
|
private Node createNewNodeNode(NodeProperty[] properties) throws IOException {
|
|
return createNewNodeNode(properties, referencesIO.allocateReference());
|
|
}
|
|
|
|
private Node createNewNodeNode(NodeProperty[] properties, long reference) {
|
|
return new ClassNode(reference, properties);
|
|
}
|
|
|
|
private void setNode(Node node) throws IOException {
|
|
ByteBuffer buffer;
|
|
switch (node.getType()) {
|
|
case ARRAY:
|
|
ArrayNode arrayNode = ((ArrayNode) node);
|
|
buffer = ByteBuffer.allocate(Integer.BYTES + arrayNode.countItems() * Long.BYTES);
|
|
buffer.putInt((arrayNode.countItems() & arrayCountMask) | isArrayMask);
|
|
for (long ref : arrayNode.getItems()) {
|
|
buffer.putLong(ref);
|
|
}
|
|
buffer.flip();
|
|
break;
|
|
case CLASS:
|
|
ClassNode classNode = ((ClassNode) node);
|
|
ByteArrayDataOutput dataOutput = ByteStreams.newDataOutput();
|
|
dataOutput.writeInt(classNode.countProperties() & nodeCountMask);
|
|
for (NodeProperty ref : classNode.getProperties()) {
|
|
dataOutput.write(ref.getName().length);
|
|
dataOutput.write(ref.getName());
|
|
dataOutput.writeLong(ref.getReference());
|
|
}
|
|
buffer = ByteBuffer.wrap(dataOutput.toByteArray());
|
|
break;
|
|
case VALUE:
|
|
ValueNode valueNode = (ValueNode) node;
|
|
buffer = ByteBuffer.allocate(Integer.BYTES + Long.BYTES);
|
|
switch (valueNode.getValueType()) {
|
|
case STRING:
|
|
buffer.putInt(isValueMask | isValueStringMask);
|
|
break;
|
|
case NUMBER:
|
|
buffer.putInt(isValueMask | isValueNumberMask);
|
|
break;
|
|
case BOOLEAN:
|
|
buffer.putInt(isValueMask | isValueBooleanMask);
|
|
break;
|
|
default:
|
|
throw new RuntimeException();
|
|
}
|
|
buffer.putLong(valueNode.getValueReference());
|
|
buffer.flip();
|
|
break;
|
|
default:
|
|
throw new RuntimeException();
|
|
}
|
|
referencesIO.writeToReference(node.getReference(), buffer.limit(), buffer);
|
|
}
|
|
|
|
private Node loadNode(long reference) throws IOException {
|
|
Node node;
|
|
ByteBuffer nodeData = referencesIO.readFromReference(reference);
|
|
int propertiesCount = nodeData.getInt();
|
|
boolean isArray = (propertiesCount & isArrayMask) != 0;
|
|
if (isArray) {
|
|
int arrayElementsCount = propertiesCount & arrayCountMask;
|
|
long[] arrayElementsReferences = new long[arrayElementsCount];
|
|
for (int i = 0; i < arrayElementsCount; i++) {
|
|
arrayElementsReferences[i] = nodeData.getLong();
|
|
}
|
|
node = new ArrayNode(reference, arrayElementsReferences);
|
|
} else {
|
|
boolean isValue = (propertiesCount & isValueMask) != 0;
|
|
if (isValue) {
|
|
long dataReference = nodeData.getLong();
|
|
if ((propertiesCount & isValueStringMask) != 0) {
|
|
node = new ValueNode(reference, dataReference, ValueType.STRING);
|
|
} else if ((propertiesCount & isValueNumberMask) != 0) {
|
|
node = new ValueNode(reference, dataReference, ValueType.NUMBER);
|
|
} else if ((propertiesCount & isValueBooleanMask) != 0) {
|
|
node = new ValueNode(reference, dataReference, ValueType.BOOLEAN);
|
|
} else {
|
|
throw new IOException();
|
|
}
|
|
} else {
|
|
NodeProperty[] nodeProperties = new NodeProperty[propertiesCount & nodeCountMask];
|
|
for (int i = 0; i < propertiesCount; i++) {
|
|
byte nameLength = nodeData.get();
|
|
byte[] name = new byte[nameLength];
|
|
nodeData.get(name);
|
|
long nodeReference = nodeData.getLong();
|
|
nodeProperties[i] = new NodeProperty(name, nodeReference);
|
|
}
|
|
node = new ClassNode(reference, nodeProperties);
|
|
}
|
|
}
|
|
return node;
|
|
}
|
|
|
|
public static boolean nameEquals(byte[] name1, byte[] name2) {
|
|
if (name1 == null || name2 == null || name1.length == 0 || name2.length == 0) {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
if (name1.length != name2.length) {
|
|
return false;
|
|
}
|
|
for (int i = 0; i < name1.length; i++) {
|
|
if (name1[i] != name2[i])
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|