strangedb-server/src/main/java/it/cavallium/strangedb/server/DatabaseNodesIO.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;
}
}