| /******************************************************************************* |
| * Copyright (c) 2018 Aston University. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * This Source Code may also be made available under the following Secondary |
| * Licenses when the conditions for such availability set forth in the Eclipse |
| * Public License, v. 2.0 are satisfied: GNU General Public License, version 3. |
| * |
| * SPDX-License-Identifier: EPL-2.0 OR GPL-3.0 |
| * |
| * Contributors: |
| * Antonio Garcia-Dominguez - initial API and implementation |
| ******************************************************************************/ |
| package org.eclipse.hawk.greycat; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.concurrent.CompletableFuture; |
| import java.util.concurrent.ExecutionException; |
| import java.util.function.Supplier; |
| import java.util.stream.IntStream; |
| import java.util.stream.Stream; |
| |
| import org.eclipse.hawk.core.graph.EmptyIGraphIterable; |
| import org.eclipse.hawk.core.graph.IGraphEdge; |
| import org.eclipse.hawk.core.graph.IGraphIterable; |
| import org.eclipse.hawk.core.graph.IGraphDatabase.Mode; |
| import org.eclipse.hawk.core.graph.timeaware.ITimeAwareGraphNode; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import com.google.common.collect.BiMap; |
| import com.google.common.collect.HashBiMap; |
| import com.google.common.collect.Iterators; |
| |
| import greycat.Constants; |
| import greycat.Graph; |
| import greycat.Node; |
| import greycat.Type; |
| import greycat.plugin.NodeState; |
| import greycat.plugin.Resolver; |
| import greycat.struct.DoubleArray; |
| import greycat.struct.IntArray; |
| import greycat.struct.LongArray; |
| import greycat.struct.Relation; |
| import greycat.struct.StringArray; |
| import greycat.struct.StringIntMap; |
| |
| public class GreycatNode implements ITimeAwareGraphNode { |
| |
| protected static class StreamIterable<T> implements Iterable<T> { |
| private Stream<T> stream; |
| |
| protected StreamIterable(Stream<T> stream) { |
| this.stream = stream; |
| } |
| |
| @Override |
| public Iterator<T> iterator() { |
| return stream.iterator(); |
| } |
| } |
| |
| private enum Direction { |
| IN { |
| @Override |
| public String getPrefix() { |
| return "in_"; |
| } |
| |
| public IGraphEdge convertToEdge(String type, GreycatNode current, GreycatNode other) { |
| if (GreycatHeavyEdge.NODETYPE.equals(other.getNodeLabel())) { |
| return new GreycatHeavyEdge(other, type); |
| } |
| return new GreycatLightEdge(other, current, type); |
| } |
| }, OUT { |
| @Override |
| public String getPrefix() { |
| return "out_"; |
| } |
| |
| @Override |
| public IGraphEdge convertToEdge(String type, GreycatNode current, GreycatNode other) { |
| if (GreycatHeavyEdge.NODETYPE.equals(other.getNodeLabel())) { |
| return new GreycatHeavyEdge(other, type); |
| } |
| return new GreycatLightEdge(current, other, type); |
| } |
| }; |
| |
| public abstract String getPrefix(); |
| public abstract IGraphEdge convertToEdge(String type, GreycatNode current, GreycatNode other); |
| } |
| |
| private final class NodeProvider { |
| private Graph _graph; |
| private Node _node; |
| |
| public NodeProvider(Graph graph, Node node) { |
| this._graph = graph; |
| this._node = node; |
| } |
| |
| public Node get() { |
| if (db.getGraph() == _graph) { |
| return _node; |
| } else { |
| // graph in DB changed: need to free and refetch the node |
| _node.free(); |
| |
| CompletableFuture<Node> result = new CompletableFuture<>(); |
| db.getGraph().lookup(world, time, id, node -> result.complete(node)); |
| try { |
| _node = result.get(); |
| _graph = db.getGraph(); |
| } catch (InterruptedException | ExecutionException e) { |
| LOGGER.error(String.format("Could not refetch node %d:%d:%d", world, time, id), e); |
| } |
| |
| return _node; |
| } |
| } |
| |
| public void markDirty() { |
| db.markDirty(GreycatNode.this); |
| } |
| |
| public void free() { |
| if (_node != null) { |
| _node.free(); |
| _node = null; |
| _graph = null; |
| } |
| } |
| } |
| |
| /** |
| * Encapsulates an access to the underlying node, with implicit free unless |
| * changed since the last save, and implicit saves upon full closing of all |
| * nodes open so far. Allows for nested calls: should be safe as long as we |
| * stick to try-with-resources. |
| */ |
| private int nestingLevel = 0; |
| protected final class NodeReader implements AutoCloseable, Supplier<Node> { |
| private NodeReader() { |
| ++nestingLevel; |
| db.markOpen(GreycatNode.this); |
| } |
| |
| public Node get() { |
| return nodeProvider.get(); |
| } |
| |
| public void markDirty() { |
| nodeProvider.markDirty(); |
| } |
| |
| @Override |
| public void close() { |
| if (--nestingLevel <= 0) { |
| db.markClosed(GreycatNode.this); |
| } |
| } |
| } |
| |
| /** Prefix for all attribute names. Prevents clashes with reserved names. */ |
| private static final String ATTRIBUTE_PREFIX = "a_"; |
| |
| /** |
| * Special property used to record actual Java types. Greycat V11 only stores |
| * int, long, double and String scalars and arrays. We have to keep the real |
| * type here and then convert back. |
| * |
| * Only problem is that we can't really modify arrays in place - we have to |
| * replace them entirely with {@link #setProperty(String, Object)}. |
| */ |
| private static final String JAVATYPE_PROPERTY = "h_javatypes"; |
| private static final BiMap<String, Integer> javaTypes = HashBiMap.create(); |
| static { |
| int counter = 0; |
| for (Class<?> klass : Arrays.asList(Float.class, |
| Double[].class, Float[].class, Long[].class, Integer[].class, Short[].class, Byte[].class, |
| double[].class, float[].class, long[].class, int[].class, short[].class, byte[].class)) { |
| javaTypes.put(klass.getSimpleName(), counter++); |
| } |
| } |
| |
| /** Greycat does not save empty strings into nodes, instead returning just null for them. We replace them with this placeholder value and map it back afterwards. */ |
| private static final String EMPTY_STRING_MARKER = "_@_h_empty_@_"; |
| |
| private static final Logger LOGGER = LoggerFactory.getLogger(GreycatNode.class); |
| |
| private final AbstractGreycatDatabase db; |
| private final long world, time, id; |
| private final NodeProvider nodeProvider; |
| |
| /** lazily initialized node label */ |
| private String nodeLabel; |
| |
| protected static int getValueType(Object value) { |
| if (value == null) { |
| return Type.STRING; |
| } |
| |
| switch (value.getClass().getSimpleName()) { |
| case "boolean": |
| case "Boolean": |
| return Type.BOOL; |
| case "Short": |
| case "Byte": |
| case "Integer": |
| return Type.INT; |
| case "Long": |
| return Type.LONG; |
| case "Float": |
| case "Double": |
| return Type.DOUBLE; |
| case "String": |
| return Type.STRING; |
| } |
| |
| LOGGER.warn("Unknown type: {}, returning Type.STRING", value.getClass().getSimpleName()); |
| return Type.STRING; |
| } |
| |
| /** |
| * For nodes that have been newly created and still need to be saved for the first time. |
| */ |
| public GreycatNode(AbstractGreycatDatabase db, long world, long time, long id, Node node) { |
| this.db = db; |
| this.world = world; |
| this.time = time; |
| this.id = id; |
| |
| this.nodeProvider = new NodeProvider(db.getGraph(), node); |
| } |
| |
| @Override |
| public Long getId() { |
| return id; |
| } |
| |
| public long getWorld() { |
| return world; |
| } |
| |
| @Override |
| public long getTime() { |
| return time; |
| } |
| |
| @Override |
| public void end() { |
| try (NodeReader rn = getNodeReader()) { |
| // 1. Unlink node from next endpoint |
| // 2. Unindex node from next endpoint |
| // 3. End node lifespan in Greycat |
| |
| /* |
| * end() means that the edges and node should still be available at *this* |
| * precise timepoint, but not from the next timepoint onwards. |
| */ |
| for (IGraphEdge out : travelInTime(time + 1).getOutgoing()) { |
| out.delete(); |
| } |
| for (IGraphEdge in : travelInTime(time + 1).getIncoming()) { |
| in.delete(); |
| } |
| |
| db.luceneIndexer.remove(travelInTime(time + 1)); |
| rn.get().end(); |
| } |
| } |
| |
| @Override |
| public GreycatNode travelInTime(long time) { |
| if (time == NO_SUCH_INSTANT) { |
| return null; |
| } |
| return db.lookup(world, time, id); |
| } |
| |
| public String getNodeLabel() { |
| if (nodeLabel == null) { |
| try (NodeReader rn = getNodeReader()) { |
| nodeLabel = rn.get().get(AbstractGreycatDatabase.NODE_LABEL_IDX).toString(); |
| } |
| } |
| |
| return nodeLabel; |
| } |
| |
| @Override |
| public Set<String> getPropertyKeys() { |
| try (NodeReader rn = getNodeReader()) { |
| final Node n = rn.get(); |
| final Resolver resolver = n.graph().resolver(); |
| NodeState state = resolver.resolveState(n); |
| |
| final Set<String> results = new HashSet<>(); |
| state.each((attributeKey, elemType, elem) -> { |
| final String resolveName = resolver.hashToString(attributeKey); |
| if (resolveName != null && resolveName.startsWith(ATTRIBUTE_PREFIX)) { |
| results.add(resolveName.substring(ATTRIBUTE_PREFIX.length())); |
| } |
| }); |
| |
| return results; |
| } |
| } |
| |
| protected NodeReader getNodeReader() { |
| return new NodeReader(); |
| } |
| |
| @Override |
| public boolean isAlive() { |
| try (NodeReader rn = getNodeReader()) { |
| return rn.get() != null; |
| } |
| } |
| |
| @Override |
| public Object getProperty(String name) { |
| Object rawValue; |
| try (NodeReader rn = getNodeReader()) { |
| rawValue = rn.get().get(ATTRIBUTE_PREFIX + name); |
| |
| if (rawValue instanceof StringArray) { |
| return ((StringArray) rawValue).extract(); |
| } else if (rawValue instanceof LongArray) { |
| final int javaTypeID = getJavaTypesMap(rn).getValue(name); |
| final String javaType = javaTypes.inverse().get(javaTypeID); |
| final LongArray lArray = (LongArray) rawValue; |
| |
| if ("Long[]".equals(javaType)) { |
| Long[] ret = new Long[lArray.size()]; |
| for (int i = 0; i < lArray.size(); i++) { |
| ret[i] = lArray.get(i); |
| } |
| return ret; |
| } |
| |
| return lArray.extract(); |
| } else if (rawValue instanceof DoubleArray) { |
| final int javaTypeID = getJavaTypesMap(rn).getValue(name); |
| final String javaType = javaTypes.inverse().get(javaTypeID); |
| final DoubleArray dArray = (DoubleArray) rawValue; |
| |
| switch (javaType) { |
| case "Double[]": { |
| Double[] ret = new Double[dArray.size()]; |
| for (int i = 0; i < dArray.size(); i++) { |
| ret[i] = dArray.get(i); |
| } |
| return ret; |
| } |
| case "Float[]": { |
| Float[] ret = new Float[dArray.size()]; |
| for (int i = 0; i < dArray.size(); i++) { |
| ret[i] = (float) dArray.get(i); |
| } |
| return ret; |
| } |
| case "float[]": { |
| float[] ret = new float[dArray.size()]; |
| for (int i = 0; i < dArray.size(); i++) { |
| ret[i] = (float) dArray.get(i); |
| } |
| return ret; |
| } |
| } |
| |
| return dArray.extract(); |
| } else if (rawValue instanceof IntArray) { |
| final int javaTypeID = getJavaTypesMap(rn).getValue(name); |
| final String javaType = javaTypes.inverse().get(javaTypeID); |
| final IntArray iArray = (IntArray) rawValue; |
| |
| switch (javaType) { |
| case "Integer[]": { |
| Integer[] ret = new Integer[iArray.size()]; |
| for (int i = 0; i < iArray.size(); i++) { |
| ret[i] = iArray.get(i); |
| } |
| return ret; |
| } |
| case "Short[]": { |
| Short[] ret = new Short[iArray.size()]; |
| for (int i = 0; i < iArray.size(); i++) { |
| ret[i] = (short) iArray.get(i); |
| } |
| return ret; |
| } |
| case "short[]": { |
| short[] ret = new short[iArray.size()]; |
| for (int i = 0; i < iArray.size(); i++) { |
| ret[i] = (short) iArray.get(i); |
| } |
| return ret; |
| } |
| case "Byte[]": { |
| Byte[] ret = new Byte[iArray.size()]; |
| for (int i = 0; i < iArray.size(); i++) { |
| ret[i] = (byte) iArray.get(i); |
| } |
| return ret; |
| } |
| case "byte[]": { |
| byte[] ret = new byte[iArray.size()]; |
| for (int i = 0; i < iArray.size(); i++) { |
| ret[i] = (byte) iArray.get(i); |
| } |
| return ret; |
| } |
| } |
| |
| return iArray.extract(); |
| } else if (rawValue instanceof Double) { |
| final int javaTypeID = getJavaTypesMap(rn).getValue(name); |
| final String javaType = javaTypes.inverse().get(javaTypeID); |
| |
| if ("Float".equals(javaType)) { |
| return ((Double) rawValue).floatValue(); |
| } |
| } else if (EMPTY_STRING_MARKER.equals(rawValue)) { |
| return ""; |
| } |
| |
| return rawValue; |
| } |
| } |
| |
| @Override |
| public void setProperty(String name, Object value) { |
| try (NodeReader rn = getNodeReader()) { |
| setPropertyRaw(rn, name, value); |
| } |
| } |
| |
| /** |
| * Allows for setting multiple properties at once, in a slightly more efficient way. |
| */ |
| public void setProperties(Map<String, Object> props) { |
| try (NodeReader rn = getNodeReader()) { |
| setPropertiesRaw(rn, props); |
| } |
| } |
| |
| protected void setPropertiesRaw(NodeReader rn, Map<String, Object> props) { |
| for (Entry<String, Object> entry : props.entrySet()) { |
| setPropertyRaw(rn, entry.getKey(), entry.getValue()); |
| } |
| } |
| |
| /** |
| * Saves the property, marking as dirty the node but without saving. |
| */ |
| protected void setPropertyRaw(NodeReader rn, String name, Object value) { |
| if (value != null && value.getClass().isArray()) { |
| setArrayPropertyRaw(rn, name, value); |
| } else if (value instanceof Float) { |
| getJavaTypesMap(rn).put(name, javaTypes.get(value.getClass().getSimpleName())); |
| rn.get().set(ATTRIBUTE_PREFIX + name, Type.DOUBLE, value); |
| } else { |
| getJavaTypesMap(rn).remove(name); |
| if ("".equals(value)) { |
| value = EMPTY_STRING_MARKER; |
| } |
| rn.get().set(ATTRIBUTE_PREFIX + name, getValueType(value), value); |
| } |
| rn.markDirty(); |
| } |
| |
| protected void setArrayPropertyRaw(NodeReader rn, String name, Object value) { |
| // Save real array type here, so we can convert back in getProperty() |
| final String javaType = value.getClass().getSimpleName(); |
| final Integer javaTypeID = javaTypes.get(javaType); |
| if (javaTypeID != null) { |
| final StringIntMap arrayTypes = getJavaTypesMap(rn); |
| arrayTypes.put(name, javaTypeID); |
| } |
| final Node n = rn.get(); |
| |
| switch (javaType) { |
| case "Double[]": |
| case "double[]": |
| case "Float[]": |
| case "float[]": |
| DoubleArray dArray = (DoubleArray) n.getOrCreate(ATTRIBUTE_PREFIX + name, Type.DOUBLE_ARRAY); |
| dArray.clear(); |
| |
| switch (javaType) { |
| case "Double[]": |
| for (Double d : (Double[]) value) { |
| if (d != null) { |
| dArray.addElement(d); |
| } |
| } |
| break; |
| case "double[]": |
| for (double d : (double[]) value) { |
| dArray.addElement(d); |
| } |
| break; |
| case "Float[]": |
| for (Float f : (Float[]) value) { |
| if (f != null) { |
| dArray.addElement(f); |
| } |
| } |
| break; |
| case "float[]": |
| for (float f : (float[]) value) { |
| dArray.addElement(f); |
| } |
| break; |
| } |
| |
| break; |
| case "Long[]": |
| case "long[]": |
| LongArray lArray = (LongArray) n.getOrCreate(ATTRIBUTE_PREFIX + name, Type.LONG_ARRAY); |
| lArray.clear(); |
| |
| if ("Long".equals(javaType)) { |
| for (long l : (Long[]) value) { |
| lArray.addElement(l); |
| } |
| } else { |
| for (long l : (long[]) value) { |
| lArray.addElement(l); |
| } |
| } |
| |
| break; |
| case "Integer[]": |
| case "int[]": |
| case "Short[]": |
| case "short[]": |
| case "Byte[]": |
| case "byte[]": |
| IntArray iArray = (IntArray) n.getOrCreate(ATTRIBUTE_PREFIX + name, Type.INT_ARRAY); |
| iArray.clear(); |
| |
| switch (javaType) { |
| case "Integer[]": |
| for (Integer i : (Integer[])value) { |
| if (i != null) { |
| iArray.addElement(i); |
| } |
| } |
| break; |
| case "int[]": |
| for (int i : (int[])value) { |
| iArray.addElement(i); |
| } |
| break; |
| case "Short[]": |
| for (Short i : (Short[])value) { |
| if (i != null) { |
| iArray.addElement(i); |
| } |
| } |
| break; |
| case "short[]": |
| for (short i : (short[])value) { |
| iArray.addElement(i); |
| } |
| break; |
| case "Byte[]": |
| for (Byte i : (Byte[])value) { |
| if (i != null) { |
| iArray.addElement(i); |
| } |
| } |
| break; |
| case "byte[]": |
| for (byte i : (byte[])value) { |
| iArray.addElement(i); |
| } |
| break; |
| } |
| |
| break; |
| case "String[]": { |
| StringArray sArray = (StringArray) n.getOrCreate(ATTRIBUTE_PREFIX + name, Type.STRING_ARRAY); |
| sArray.clear(); |
| sArray.addAll((String[]) value); |
| break; |
| } |
| } |
| } |
| |
| protected StringIntMap getJavaTypesMap(NodeReader rn) { |
| StringIntMap value = (StringIntMap) rn.get().get(JAVATYPE_PROPERTY); |
| if (value == null) { |
| value = (StringIntMap) rn.get().getOrCreate(JAVATYPE_PROPERTY, Type.STRING_TO_INT_MAP); |
| rn.markDirty(); |
| } |
| return value; |
| } |
| |
| @Override |
| public Iterable<IGraphEdge> getEdges() { |
| try (NodeReader rn = getNodeReader()) { |
| return getAllEdges(rn, getAllEdges(rn, new ArrayList<>(), Direction.OUT), Direction.IN); |
| } |
| } |
| |
| @Override |
| public IGraphIterable<IGraphEdge> getEdgesWithType(String type) { |
| try (NodeReader rn = getNodeReader()) { |
| final IGraphIterable<IGraphEdge> inEdges = getEdgesWithType(rn, Direction.IN, type); |
| final IGraphIterable<IGraphEdge> outEdges = getEdgesWithType(rn, Direction.OUT, type); |
| |
| return new IGraphIterable<IGraphEdge>() { |
| @Override |
| public Iterator<IGraphEdge> iterator() { |
| return Iterators.concat(inEdges.iterator(), outEdges.iterator()); |
| } |
| |
| @Override |
| public int size() { |
| return inEdges.size() + outEdges.size(); |
| } |
| |
| @Override |
| public IGraphEdge getSingle() { |
| if (inEdges.size() > 0) { |
| return inEdges.getSingle(); |
| } else { |
| return outEdges.getSingle(); |
| } |
| } |
| }; |
| } |
| } |
| |
| @Override |
| public IGraphIterable<IGraphEdge> getOutgoingWithType(String type) { |
| try (NodeReader rn = getNodeReader()) { |
| return getEdgesWithType(rn, Direction.OUT, type); |
| } |
| } |
| |
| @Override |
| public IGraphIterable<IGraphEdge> getIncomingWithType(String type) { |
| try (NodeReader rn = getNodeReader()) { |
| return getEdgesWithType(rn, Direction.IN, type); |
| } |
| } |
| |
| protected IGraphIterable<IGraphEdge> getEdgesWithType(final NodeReader rn, final Direction dir, String type) { |
| final int relationPosition = db.getGraph().resolver().stringToHash(dir.getPrefix() + type, false); |
| final Relation relation = (Relation) rn.get().getAt(relationPosition); |
| if (relation == null) { |
| return new EmptyIGraphIterable<>(); |
| } |
| |
| /* |
| * Do NOT preload all target nodes, unlike traverse - doing so balloons memory |
| * usage in some cases (e.g. going from the file node to the instance nodes). |
| * |
| * Streaming the results should reduce the impact of this call - we may need only |
| * the first few elements anyway. |
| */ |
| final int nEdges = relation.size(); |
| |
| return new IGraphIterable<IGraphEdge>() { |
| @Override |
| public Iterator<IGraphEdge> iterator() { |
| return Iterators.transform( |
| IntStream.range(0, nEdges).iterator(), |
| i -> { |
| final long nodeId = relation.get(i); |
| final GreycatNode target = db.lookup(world, time, nodeId); |
| return dir.convertToEdge(type, GreycatNode.this, target); |
| } |
| ); |
| } |
| |
| @Override |
| public int size() { |
| return relation.size(); |
| } |
| |
| @Override |
| public IGraphEdge getSingle() { |
| final long nodeId = relation.get(0); |
| final GreycatNode target = db.lookup(world, time, nodeId); |
| return dir.convertToEdge(type, GreycatNode.this, target); |
| } |
| }; |
| } |
| |
| @Override |
| public Iterable<IGraphEdge> getIncoming() { |
| try (NodeReader rn = getNodeReader()) { |
| return getAllEdges(rn, new ArrayList<>(), Direction.IN); |
| } |
| } |
| |
| @Override |
| public Iterable<IGraphEdge> getOutgoing() { |
| try (NodeReader rn = getNodeReader()) { |
| return getAllEdges(rn, new ArrayList<>(), Direction.OUT); |
| } |
| } |
| |
| protected List<IGraphEdge> getAllEdges(final NodeReader rn, final List<IGraphEdge> results, final Direction dir) { |
| final Node n = rn.get(); |
| final Resolver resolver = n.graph().resolver(); |
| final NodeState state = resolver.resolveState(n); |
| final String prefix = dir.getPrefix(); |
| |
| state.each((attributeKey, elemType, elem) -> { |
| if (elemType == Type.RELATION) { |
| final String resolveName = resolver.hashToString(attributeKey); |
| if (resolveName.startsWith(prefix)) { |
| final String edgeType = resolveName.substring(prefix.length()); |
| Relation castedRelArr = (Relation) elem; |
| for (int j = 0; j < castedRelArr.size(); j++) { |
| final GreycatNode targetNode = db.lookup(world, time, castedRelArr.get(j)); |
| results.add(dir.convertToEdge(edgeType, this, targetNode)); |
| } |
| } |
| } |
| }); |
| return results; |
| } |
| |
| @Override |
| public void delete() { |
| if (db.currentMode() == Mode.NO_TX_MODE) { |
| CompletableFuture<Boolean> cSaved = new CompletableFuture<>(); |
| db.hardDelete(this, dropped -> cSaved.complete(true)); |
| cSaved.join(); |
| } else { |
| db.softDelete(this); |
| } |
| } |
| |
| @Override |
| public AbstractGreycatDatabase getGraph() { |
| return db; |
| } |
| |
| @Override |
| public void removeProperty(String name) { |
| try (NodeReader rn = getNodeReader()) { |
| rn.get().remove(ATTRIBUTE_PREFIX + name); |
| rn.markDirty(); |
| } |
| } |
| |
| @Override |
| public void finalize() throws Throwable { |
| super.finalize(); |
| nodeProvider.free(); |
| } |
| |
| /** |
| * Returns <code>true</code> if this element has been soft deleted. |
| * If so, it should be ignored by any queries and iterables. |
| */ |
| protected boolean isSoftDeleted() { |
| try (NodeReader rn = getNodeReader()) { |
| final Boolean softDeleted = (Boolean) rn.get().get(AbstractGreycatDatabase.SOFT_DELETED_KEY); |
| return softDeleted == Boolean.TRUE; |
| } |
| } |
| |
| protected IGraphEdge addEdge(String type, GreycatNode end, Map<String, Object> props) { |
| if (props == null || props.isEmpty()) { |
| return GreycatLightEdge.create(type, this, end); |
| } else { |
| return GreycatHeavyEdge.create(type, this, end, props); |
| } |
| } |
| |
| protected static void addOutgoing(String type, final NodeReader rn, final NodeReader ro) { |
| rn.get().addToRelation(Direction.OUT.getPrefix() + type, ro.get()); |
| rn.markDirty(); |
| } |
| |
| protected static void addIncoming(String type, final NodeReader rn, final NodeReader ro) { |
| rn.get().addToRelation(Direction.IN.getPrefix() + type, ro.get()); |
| rn.markDirty(); |
| } |
| |
| protected static void removeOutgoing(String type, final NodeReader rn, final NodeReader ro) { |
| rn.get().removeFromRelation(Direction.OUT.getPrefix() + type, ro.get()); |
| rn.markDirty(); |
| } |
| |
| protected static void removeIncoming(String type, final NodeReader rn, final NodeReader ro) { |
| rn.get().removeFromRelation(Direction.IN.getPrefix() + type, ro.get()); |
| rn.markDirty(); |
| } |
| |
| @Override |
| public String toString() { |
| return "GreycatNode [world=" + world + ", time=" + time + ", id=" + id + "]"; |
| } |
| |
| @Override |
| public int hashCode() { |
| final int prime = 31; |
| int result = 1; |
| result = prime * result + (int) (id ^ (id >>> 32)); |
| result = prime * result + (int) (time ^ (time >>> 32)); |
| result = prime * result + (int) (world ^ (world >>> 32)); |
| return result; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) |
| return true; |
| if (obj == null) |
| return false; |
| if (getClass() != obj.getClass()) |
| return false; |
| GreycatNode other = (GreycatNode) obj; |
| if (id != other.id) |
| return false; |
| if (time != other.time) |
| return false; |
| if (world != other.world) |
| return false; |
| return true; |
| } |
| |
| @Override |
| public long getLatestInstant() throws Exception { |
| try (NodeReader rn = getNodeReader()) { |
| final Node n = rn.get(); |
| |
| CompletableFuture<long[]> result = new CompletableFuture<>(); |
| n.timepoints(Constants.BEGINNING_OF_TIME, Constants.END_OF_TIME, (value) -> { |
| result.complete(value); |
| }); |
| |
| long latest = Constants.BEGINNING_OF_TIME; |
| for (long timepoint : result.get()) { |
| latest = Math.max(latest, timepoint); |
| } |
| return latest; |
| } |
| } |
| |
| @Override |
| public List<Long> getInstantsBetween(long fromInclusive, long toInclusive) { |
| try (NodeReader rn = getNodeReader()) { |
| final Node n = rn.get(); |
| |
| CompletableFuture<long[]> result = new CompletableFuture<>(); |
| |
| /* |
| * FIXME Greycat says that in timepoints(...), both ends are inclusive, but our |
| * unit tests say otherwise. This is a temporal fix, with some caution around |
| * overflow. |
| */ |
| n.timepoints(fromInclusive, toInclusive < Constants.END_OF_TIME ? toInclusive + 1 : toInclusive, (value) -> { |
| result.complete(value); |
| }); |
| |
| List<Long> instants = new ArrayList<>(); |
| for (long instant : result.get()) { |
| instants.add(instant); |
| } |
| return instants; |
| } catch (InterruptedException e) { |
| LOGGER.error(e.getMessage(), e); |
| Thread.currentThread().interrupt(); |
| return Collections.emptyList(); |
| } catch (ExecutionException e) { |
| LOGGER.error(e.getMessage(), e); |
| return Collections.emptyList(); |
| } |
| } |
| |
| @Override |
| public long getEarliestInstant() throws Exception { |
| try (NodeReader rn = getNodeReader()) { |
| final Node n = rn.get(); |
| |
| CompletableFuture<long[]> result = new CompletableFuture<>(); |
| n.timepoints(Constants.BEGINNING_OF_TIME, Constants.END_OF_TIME, (value) -> { |
| result.complete(value); |
| }); |
| |
| long earliest = Constants.END_OF_TIME; |
| for (long timepoint : result.get()) { |
| earliest = Math.min(earliest, timepoint); |
| } |
| return earliest; |
| } |
| } |
| |
| @Override |
| public long getPreviousInstant() throws Exception { |
| try (NodeReader rn = getNodeReader()) { |
| final Node n = rn.get(); |
| |
| CompletableFuture<long[]> result = new CompletableFuture<>(); |
| |
| /* |
| * While the GreyCat javadocs say that both ends of the timepoints range are |
| * inclusive, the TimeAwareBackendTest#nextPrev method showed that the end of |
| * the range is exclusive. |
| */ |
| n.timepoints(Constants.BEGINNING_OF_TIME, getTime(), |
| (value) -> result.complete(value)); |
| |
| /* |
| * We assume timepoints(...) produces elements from newest to oldest. The |
| * previous instant is the most recent moment before the current one. |
| */ |
| final long[] instants = result.get(); |
| if (instants.length == 0) { |
| return NO_SUCH_INSTANT; |
| } |
| return instants[0]; |
| } |
| } |
| |
| @Override |
| public long getNextInstant() throws Exception { |
| try (NodeReader rn = getNodeReader()) { |
| final Node n = rn.get(); |
| |
| CompletableFuture<long[]> result = new CompletableFuture<>(); |
| n.timepoints(getTime() + 1, Constants.END_OF_TIME, (value) -> { |
| result.complete(value); |
| }); |
| |
| // See getPreviousInstant() for our assumptions |
| final long[] instants = result.get(); |
| if (instants.length == 0) { |
| return NO_SUCH_INSTANT; |
| } |
| return instants[instants.length - 1]; |
| } |
| } |
| |
| @Override |
| public List<Long> getInstantsFrom(long fromInclusive) { |
| return getInstantsBetween(fromInclusive, Constants.END_OF_TIME); |
| } |
| |
| @Override |
| public List<Long> getInstantsUpTo(long toInclusive) { |
| return getInstantsBetween(Constants.BEGINNING_OF_TIME, toInclusive); |
| } |
| |
| @Override |
| public List<Long> getAllInstants() throws Exception { |
| return getInstantsBetween(Constants.BEGINNING_OF_TIME, Constants.END_OF_TIME); |
| } |
| |
| } |