strangedb-server/src/main/java/it/cavallium/strangedb/server/DatabaseNodesIO.java

453 lines
14 KiB
Java
Raw Normal View History

2019-03-09 18:53:30 +01:00
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.*;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
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 isNumberMask = 0b11000000000000000000000000000000;
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 {
Node foundNode = getNode(path);
return toJson(foundNode);
}
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(org.json.JSONObject.quote(loadValue(valueNode.getValueReference())));
break;
}
case NUMBER: {
NumberNode valueNode = (NumberNode) foundNode;
sb.append(loadNumber(valueNode.getNumberReference()));
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(long reference) throws IOException {
ByteBuffer buffer = referencesIO.readFromReference(reference);
return StandardCharsets.UTF_8.decode(buffer).toString();
}
private void setNumber(long reference, double value) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(Double.BYTES);
buffer.putDouble(value);
buffer.flip();
referencesIO.writeToReference(reference, buffer.limit(), buffer);
}
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 NUMBER:
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 '"':
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);
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 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;
}
Node node = createNewNodeNode(properties, reference);
setNode(node);
return node.getReference();
} else if (o instanceof String) {
Node node = createNewValueNode((String) o, 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 if (o instanceof Double) {
number = (Double) o;
} else {
throw new RuntimeException();
}
Node node = createNewNumberNode(number+0, 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);
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;
}
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() throws IOException {
long reference = referencesIO.allocateReference();
return new ValueNode(reference, referencesIO.allocateReference());
}
private Node createNewValueNode(String value) throws IOException {
return createNewValueNode(value, referencesIO.allocateReference());
}
private Node createNewValueNode(String value, long reference) throws IOException {
byte[] string = value.getBytes(StandardCharsets.UTF_8);
long dataReference = referencesIO.allocateReference(string.length, ByteBuffer.wrap(string));
return new ValueNode(reference, dataReference);
}
private Node createNewNumberNode(double value, long reference) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(Double.BYTES);
buffer.putDouble(value);
buffer.flip();
long dataReference = referencesIO.allocateReference(buffer.limit(), buffer);
return new NumberNode(reference, dataReference);
}
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:
buffer = ByteBuffer.allocate(Integer.BYTES + Long.BYTES);
buffer.putInt(isValueMask);
buffer.putLong(((ValueNode) node).getValueReference());
buffer.flip();
break;
case NUMBER:
buffer = ByteBuffer.allocate(Integer.BYTES + Long.BYTES);
buffer.putInt(isNumberMask);
buffer.putLong(((NumberNode) node).getNumberReference());
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 isNumber = (propertiesCount & isNumberMask) == isNumberMask;
boolean isArray = !isNumber && (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 = !isNumber && (propertiesCount & isValueMask) != 0;
if (isValue) {
long valueReference = nodeData.getLong();
node = new ValueNode(reference, valueReference);
} else if (isNumber) {
long numberReference = nodeData.getLong();
node = new NumberNode(reference, numberReference);
} 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;
}
}