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 jsonProperties = obj.toMap(); NodeProperty[] properties = new NodeProperty[jsonProperties.size()]; int i = 0; for (Map.Entry 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; } }