All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerGraph Maven / Gradle / Ivy

There is a newer version: 3.3.4.23
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.tinkerpop.gremlin.tinkergraph.structure;

import gnu.trove.map.hash.THashMap;
import gnu.trove.set.hash.THashSet;
import org.apache.commons.configuration.BaseConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.tinkerpop.gremlin.process.computer.GraphComputer;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalStrategies;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.Transaction;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.tinkerpop.gremlin.structure.io.Io;
import org.apache.tinkerpop.gremlin.structure.io.IoCore;
import org.apache.tinkerpop.gremlin.structure.io.graphson.GraphSONVersion;
import org.apache.tinkerpop.gremlin.structure.io.gryo.GryoVersion;
import org.apache.tinkerpop.gremlin.structure.util.ElementHelper;
import org.apache.tinkerpop.gremlin.structure.util.GraphFactory;
import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
import org.apache.tinkerpop.gremlin.tinkergraph.process.computer.TinkerGraphComputer;
import org.apache.tinkerpop.gremlin.tinkergraph.process.computer.TinkerGraphComputerView;
import org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.strategy.optimization.TinkerGraphCountStrategy;
import org.apache.tinkerpop.gremlin.tinkergraph.process.traversal.strategy.optimization.TinkerGraphStepStrategy;
import org.apache.tinkerpop.gremlin.tinkergraph.storage.EdgeDeserializer;
import org.apache.tinkerpop.gremlin.tinkergraph.storage.EdgeSerializer;
import org.apache.tinkerpop.gremlin.tinkergraph.storage.OndiskOverflow;
import org.apache.tinkerpop.gremlin.tinkergraph.storage.SerializationStats;
import org.apache.tinkerpop.gremlin.tinkergraph.storage.VertexDeserializer;
import org.apache.tinkerpop.gremlin.tinkergraph.storage.VertexSerializer;
import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
import org.apache.tinkerpop.gremlin.util.iterator.MultiIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Stream;

/**
 * A fork of the in-memory reference implementation TinkerGraph featuring:
 * - using ~70% less memory (depending on your domain)
 * - strict schema enforcement (optional)
 * - on-disk overflow, i.e. elements are serialized to disk if (and only if) they don't fit into memory (optional)
 *
 * TODO MP: remove complexity in implementation by removing option to use standard elements
 */
@Graph.OptIn(Graph.OptIn.SUITE_STRUCTURE_STANDARD)
@Graph.OptIn(Graph.OptIn.SUITE_STRUCTURE_INTEGRATE)
@Graph.OptIn(Graph.OptIn.SUITE_PROCESS_STANDARD)
public final class TinkerGraph implements Graph {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    static {
        TraversalStrategies.GlobalCache.registerStrategies(TinkerGraph.class, TraversalStrategies.GlobalCache.getStrategies(Graph.class).clone().addStrategies(
                TinkerGraphStepStrategy.instance(),
                TinkerGraphCountStrategy.instance()));
    }

    public static final Configuration EMPTY_CONFIGURATION() {
        return new BaseConfiguration() {{
            this.setProperty(Graph.GRAPH, TinkerGraph.class.getName());
            this.setProperty(GREMLIN_TINKERGRAPH_OVERFLOW_HEAP_PERCENTAGE_THRESHOLD, 80);
            this.setProperty(GREMLIN_TINKERGRAPH_ONDISK_OVERFLOW_ENABLED, true);
        }};
    }

    public static final String GREMLIN_TINKERGRAPH_VERTEX_ID_MANAGER = "gremlin.tinkergraph.vertexIdManager";
    public static final String GREMLIN_TINKERGRAPH_EDGE_ID_MANAGER = "gremlin.tinkergraph.edgeIdManager";
    public static final String GREMLIN_TINKERGRAPH_VERTEX_PROPERTY_ID_MANAGER = "gremlin.tinkergraph.vertexPropertyIdManager";
    public static final String GREMLIN_TINKERGRAPH_DEFAULT_VERTEX_PROPERTY_CARDINALITY = "gremlin.tinkergraph.defaultVertexPropertyCardinality";
    public static final String GREMLIN_TINKERGRAPH_GRAPH_LOCATION = "gremlin.tinkergraph.graphLocation";
    public static final String GREMLIN_TINKERGRAPH_GRAPH_FORMAT = "gremlin.tinkergraph.graphFormat";
    public static final String GREMLIN_TINKERGRAPH_ONDISK_OVERFLOW_ENABLED = "gremlin.tinkergraph.ondiskOverflow.enabled";

    /** when heap (after GC run) is above this threshold (e.g. 80 for 80%), @see ReferenceManager will start to clear some references, i.e. write them to storage and set them to `null` */
    public static final String GREMLIN_TINKERGRAPH_OVERFLOW_HEAP_PERCENTAGE_THRESHOLD = "gremlin.tinkergraph.ondiskOverflow.heapPercentageThreshold";

    public static final String GRAPH_FORMAT_MVSTORE = "overflowdb.graphFormat.mvstore";

    private final TinkerGraphFeatures features = new TinkerGraphFeatures();

    protected AtomicLong currentId = new AtomicLong(-1L);
    // TODO: replace with the more memory efficient `TLongHashMap`
    // note: if on-disk overflow enabled, these [Vertex|Edge] values are [VertexRef|ElementRef]
    protected Map vertices;
    protected Map edges;
    protected THashMap> verticesByLabel;
    protected THashMap> edgesByLabel;

    protected TinkerGraphVariables variables = null;
    protected TinkerGraphComputerView graphComputerView = null;
    protected TinkerIndex vertexIndex = null;
    protected TinkerIndex edgeIndex = null;

    protected final IdManager vertexIdManager;
    protected final IdManager edgeIdManager;
    protected final IdManager vertexPropertyIdManager;
    protected final VertexProperty.Cardinality defaultVertexPropertyCardinality;

    protected final boolean usesSpecializedElements;
    protected final Map specializedVertexFactoryByLabel;
    protected final Map specializedEdgeFactoryByLabel;

    private final Configuration configuration;
    private final String graphLocation;
    private final String graphFormat;
    private boolean closed = false;

    /* overflow to disk: elements are serialized on eviction from on-heap cache - off by default */
    // TODO: also allow using for generic elements
    public final boolean ondiskOverflowEnabled;
    protected OndiskOverflow ondiskOverflow;
    protected ReferenceManager referenceManager;

    private TinkerGraph(final Configuration configuration, boolean usesSpecializedElements,
                        Map specializedVertexFactoryByLabel,
                        Map specializedEdgeFactoryByLabel) {
        this.configuration = configuration;
        this.usesSpecializedElements = usesSpecializedElements;
        this.specializedVertexFactoryByLabel = specializedVertexFactoryByLabel;
        this.specializedEdgeFactoryByLabel = specializedEdgeFactoryByLabel;
        vertexIdManager = selectIdManager(configuration, GREMLIN_TINKERGRAPH_VERTEX_ID_MANAGER, Vertex.class);
        edgeIdManager = selectIdManager(configuration, GREMLIN_TINKERGRAPH_EDGE_ID_MANAGER, Edge.class);
        vertexPropertyIdManager = selectIdManager(configuration, GREMLIN_TINKERGRAPH_VERTEX_PROPERTY_ID_MANAGER, VertexProperty.class);
        defaultVertexPropertyCardinality = VertexProperty.Cardinality.valueOf(
          configuration.getString(GREMLIN_TINKERGRAPH_DEFAULT_VERTEX_PROPERTY_CARDINALITY, VertexProperty.Cardinality.single.name()));

        graphLocation = configuration.getString(GREMLIN_TINKERGRAPH_GRAPH_LOCATION, null);
        ondiskOverflowEnabled = configuration.getBoolean(GREMLIN_TINKERGRAPH_ONDISK_OVERFLOW_ENABLED, true);
        if (ondiskOverflowEnabled) {
            graphFormat = GRAPH_FORMAT_MVSTORE;
            referenceManager = new ReferenceManagerImpl(configuration.getInt(GREMLIN_TINKERGRAPH_OVERFLOW_HEAP_PERCENTAGE_THRESHOLD));
            VertexDeserializer vertexDeserializer = new VertexDeserializer(this, specializedVertexFactoryByLabel);
            EdgeDeserializer edgeDeserializer = new EdgeDeserializer(this, specializedEdgeFactoryByLabel);
            if (graphLocation == null) {
                ondiskOverflow = OndiskOverflow.createWithTempFile(vertexDeserializer, edgeDeserializer);
                initEmptyElementCollections();
            } else {
                ondiskOverflow = OndiskOverflow.createWithSpecificLocation(vertexDeserializer, edgeDeserializer, new File(graphLocation));
                initElementCollections(ondiskOverflow);
            }
        } else {
            graphFormat = configuration.getString(GREMLIN_TINKERGRAPH_GRAPH_FORMAT, null);
            if ((graphLocation != null && null == graphFormat) || (null == graphLocation && graphFormat != null))
                throw new IllegalStateException(String.format("The %s and %s must both be specified if either is present",
                    GREMLIN_TINKERGRAPH_GRAPH_LOCATION, GREMLIN_TINKERGRAPH_GRAPH_FORMAT));
            initEmptyElementCollections();
            referenceManager = new NoOpReferenceManager();
            if (graphLocation != null) loadGraph();
        }
    }

    private void initEmptyElementCollections() {
        vertices = new THashMap<>();
        edges = new THashMap<>();
        verticesByLabel = new THashMap<>(100);
        edgesByLabel = new THashMap<>(100);
    }

    /** implementation note: must start with vertices, because the edges require the vertexRefs to be already present! */
    private void initElementCollections(OndiskOverflow ondiskOverflow) {
        long start = System.currentTimeMillis();
        final Set> serializedVertices = ondiskOverflow.allVertices();
        final Set> serializedEdges = ondiskOverflow.allEdges();
        int elementCount = serializedVertices.size() + serializedEdges.size();
        logger.info("initializing " + elementCount + " elements from existing storage - this may take some time");
        int importCount = 0;

        vertices = new THashMap<>(serializedVertices.size());
        verticesByLabel = new THashMap<>(serializedVertices.size());
        final Iterator> serializedVertexIter = serializedVertices.iterator();
        while (serializedVertexIter.hasNext()) {
            final Map.Entry entry = serializedVertexIter.next();
            try {
                final VertexRef vertexRef = (VertexRef) ondiskOverflow.getVertexDeserializer().get().deserializeRef(entry.getValue());
                vertices.put(vertexRef.id, vertexRef);
                getElementsByLabel(verticesByLabel, vertexRef.label).add(vertexRef);
                importCount++;
                if (importCount % 100000 == 0) {
                    logger.debug("imported " + importCount + " elements - still running...");
                }
            } catch (IOException e) {
                throw new RuntimeException("error while initializing vertex from storage: id=" + entry.getKey(), e);
            }
        }

        edges = new THashMap<>(serializedEdges.size());
        edgesByLabel = new THashMap<>(serializedEdges.size());
        final Iterator> serializedEdgeIter = serializedEdges.iterator();
        while (serializedEdgeIter.hasNext()) {
            final Map.Entry entry = serializedEdgeIter.next();
            try {
                final EdgeRef edgeRef = (EdgeRef) ondiskOverflow.getEdgeDeserializer().get().deserializeRef(entry.getValue());
                edges.put(edgeRef.id, edgeRef);
                getElementsByLabel(edgesByLabel, edgeRef.label).add(edgeRef);
                importCount++;
                if (importCount % 100000 == 0) {
                    logger.debug("imported " + importCount + " elements - still running...");
                }
            } catch (IOException e) {
                throw new RuntimeException("error while initializing edge from storage: id=" + entry.getKey(), e);
            }
        }

        long elapsedMillis = System.currentTimeMillis() - start;
        logger.info("initialized " + this.toString() + " from existing storage in " + elapsedMillis + "ms");
    }

    /**
     * Open a new {@link TinkerGraph} instance.
     * 

* Reference Implementation Help: If a {@link Graph} implementation does not require a {@code Configuration} * (or perhaps has a default configuration) it can choose to implement a zero argument * {@code open()} method. This is an optional constructor method for TinkerGraph. It is not enforced by the Gremlin * Test Suite. */ public static TinkerGraph open() { return open(EMPTY_CONFIGURATION()); } /** * Open a new {@code TinkerGraph} instance. *

* Reference Implementation Help: This method is the one use by the {@link GraphFactory} to instantiate * {@link Graph} instances. This method must be overridden for the Structure Test Suite to pass. Implementers have * latitude in terms of how exceptions are handled within this method. Such exceptions will be considered * implementation specific by the test suite as all test generate graph instances by way of * {@link GraphFactory}. As such, the exceptions get generalized behind that facade and since * {@link GraphFactory} is the preferred method to opening graphs it will be consistent at that level. * * @param configuration the configuration for the instance * @return a newly opened {@link Graph} */ public static TinkerGraph open(final Configuration configuration) { return new TinkerGraph(configuration, false, new HashMap<>(), new HashMap<>()); } public static TinkerGraph open(List> vertexFactories, List> edgeFactories) { return open(EMPTY_CONFIGURATION(), vertexFactories, edgeFactories); } public static TinkerGraph open(final Configuration configuration, List> vertexFactories, List> edgeFactories) { boolean usesSpecializedElements = !vertexFactories.isEmpty() || !edgeFactories.isEmpty(); Map specializedVertexFactoryByLabel = new HashMap<>(); Map specializedEdgeFactoryByLabel = new HashMap<>(); vertexFactories.forEach(factory -> specializedVertexFactoryByLabel.put(factory.forLabel(), factory)); edgeFactories.forEach(factory -> specializedEdgeFactoryByLabel.put(factory.forLabel(), factory)); return new TinkerGraph(configuration, usesSpecializedElements, specializedVertexFactoryByLabel, specializedEdgeFactoryByLabel); } ////////////// STRUCTURE API METHODS ////////////////// @Override public Vertex addVertex(final Object... keyValues) { if (isClosed()) { throw new IllegalStateException("cannot add more elements, graph is closed"); } ElementHelper.legalPropertyKeyValueArray(keyValues); final String label = ElementHelper.getLabelValue(keyValues).orElse(Vertex.DEFAULT_LABEL); Long idValue = (Long) vertexIdManager.convert(ElementHelper.getIdValue(keyValues).orElse(null)); if (null != idValue) { if (vertices.containsKey(idValue)) throw Exceptions.vertexWithIdAlreadyExists(idValue); } else { idValue = (Long) vertexIdManager.getNextId(this); } currentId.set(Long.max(idValue, currentId.get())); final Vertex vertex = createVertex(idValue, label, keyValues); vertices.put(vertex.id(), vertex); getElementsByLabel(verticesByLabel, label).add(vertex); return vertex; } private Vertex createVertex(final long idValue, final String label, final Object... keyValues) { final Vertex vertex; if (specializedVertexFactoryByLabel.containsKey(label)) { final SpecializedElementFactory.ForVertex factory = specializedVertexFactoryByLabel.get(label); final SpecializedTinkerVertex underlying = factory.createVertex(idValue, this); vertex = factory.createVertexRef(underlying); } else { // vertex label not registered for a specialized factory, treating as generic vertex if (this.usesSpecializedElements) { throw new IllegalArgumentException( "this instance of TinkerGraph uses specialized elements, but doesn't have a factory for label " + label + ". Mixing specialized and generic elements is not (yet) supported"); } vertex = new TinkerVertex(idValue, label, this); } ElementHelper.attachProperties(vertex, VertexProperty.Cardinality.list, keyValues); return vertex; } @Override public C compute(final Class graphComputerClass) { if (!graphComputerClass.equals(TinkerGraphComputer.class)) throw Graph.Exceptions.graphDoesNotSupportProvidedGraphComputer(graphComputerClass); return (C) new TinkerGraphComputer(this); } @Override public GraphComputer compute() { return new TinkerGraphComputer(this); } @Override public Variables variables() { if (null == this.variables) this.variables = new TinkerGraphVariables(); return this.variables; } @Override public I io(final Io.Builder builder) { if (builder.requiresVersion(GryoVersion.V1_0) || builder.requiresVersion(GraphSONVersion.V1_0)) return (I) builder.graph(this).onMapper(mapper -> mapper.addRegistry(TinkerIoRegistryV1d0.instance())).create(); else if (builder.requiresVersion(GraphSONVersion.V2_0)) // there is no gryo v2 return (I) builder.graph(this).onMapper(mapper -> mapper.addRegistry(TinkerIoRegistryV2d0.instance())).create(); else return (I) builder.graph(this).onMapper(mapper -> mapper.addRegistry(TinkerIoRegistryV3d0.instance())).create(); } @Override public String toString() { return StringFactory.graphString(this, "vertices: " + vertices.size() + ", edges: " + edges.size()); } public SerializationStats getSerializationStats() { Map vertexSerializationGroupCount = new HashMap<>(); vertices.values().stream().filter(v -> v instanceof ElementRef).forEach(vertex -> { Integer elementSerializationCount = ((ElementRef) vertex).getSerializationCount(); Integer groupCountBefore = vertexSerializationGroupCount.getOrDefault(elementSerializationCount, 0); vertexSerializationGroupCount.put(elementSerializationCount, groupCountBefore + elementSerializationCount); }); Map edgeSerializationGroupCount = new HashMap<>(); edges.values().stream().filter(e -> e instanceof ElementRef).forEach(edge -> { Integer elementSerializationCount = ((ElementRef) edge).getSerializationCount(); Integer groupCountBefore = edgeSerializationGroupCount.getOrDefault(elementSerializationCount, 0); edgeSerializationGroupCount.put(elementSerializationCount, groupCountBefore + elementSerializationCount); }); return new SerializationStats(vertexSerializationGroupCount, edgeSerializationGroupCount); } /** * if the {@link #GREMLIN_TINKERGRAPH_GRAPH_LOCATION} is set, data in the graph is persisted to that location. */ @Override public void close() { this.closed = true; if (ondiskOverflowEnabled) { if (logger.isDebugEnabled()) logger.debug(getSerializationStats().toString()); referenceManager.clearAllReferences(); referenceManager.close(); ondiskOverflow.close(); } else { if (graphLocation != null) saveGraph(); } } @Override public Transaction tx() { throw Exceptions.transactionsNotSupported(); } @Override public Configuration configuration() { return configuration; } public Vertex vertex(final Long id) { return vertices.get(id); } @Override public Iterator vertices(final Object... ids) { return createElementIterator(Vertex.class, vertices, vertexIdManager, ids); } public Iterator verticesByLabel(final P labelPredicate) { return elementsByLabel(verticesByLabel, labelPredicate); } public Edge edge(final Long id) { return edges.get(id); } @Override public Iterator edges(final Object... ids) { return createElementIterator(Edge.class, edges, edgeIdManager, ids); } public Iterator edgesByLabel(final P labelPredicate) { return elementsByLabel(edgesByLabel, labelPredicate); } /** * retrieve the correct by-label map (and create it if it doesn't yet exist) */ protected Set getElementsByLabel(final THashMap> elementsByLabel, final String label) { if (!elementsByLabel.containsKey(label)) elementsByLabel.put(label, new THashSet<>(100000)); return elementsByLabel.get(label); } protected Iterator elementsByLabel(final THashMap> elementsByLabel, final P labelPredicate) { final MultiIterator multiIterator = new MultiIterator<>(); for (String label : elementsByLabel.keySet()) { if (labelPredicate.test(label)) { multiIterator.addIterator(elementsByLabel.get(label).iterator()); } } return multiIterator; } private void loadGraph() { final File f = new File(graphLocation); if (f.exists() && f.isFile()) { try { if (graphFormat.equals("graphml")) { io(IoCore.graphml()).readGraph(graphLocation); } else if (graphFormat.equals("graphson")) { io(IoCore.graphson()).readGraph(graphLocation); } else if (graphFormat.equals("gryo")) { io(IoCore.gryo()).readGraph(graphLocation); } else { io(IoCore.createIoBuilder(graphFormat)).readGraph(graphLocation); } } catch (Exception ex) { throw new RuntimeException(String.format("Could not load graph at %s with %s", graphLocation, graphFormat), ex); } } } private void saveGraph() { final File f = new File(graphLocation); if (f.exists()) { f.delete(); } else { final File parent = f.getParentFile(); // the parent would be null in the case of an relative path if the graphLocation was simply: "f.gryo" if (parent != null && !parent.exists()) { parent.mkdirs(); } } try { if (graphFormat.equals("graphml")) { io(IoCore.graphml()).writeGraph(graphLocation); } else if (graphFormat.equals("graphson")) { io(IoCore.graphson()).writeGraph(graphLocation); } else if (graphFormat.equals("gryo")) { io(IoCore.gryo()).writeGraph(graphLocation); } else { io(IoCore.createIoBuilder(graphFormat)).writeGraph(graphLocation); } } catch (Exception ex) { throw new RuntimeException(String.format("Could not save graph at %s with %s", graphLocation, graphFormat), ex); } } private Iterator createElementIterator(final Class clazz, final Map elements, final IdManager idManager, final Object... ids) { final Iterator iterator; if (0 == ids.length) { iterator = elements.values().iterator(); } else { final List idList = Arrays.asList(ids); validateHomogenousIds(idList); // if the type is of Element - have to look each up because it might be an Attachable instance or // other implementation. the assumption is that id conversion is not required for detached // stuff - doesn't seem likely someone would detach a Titan vertex then try to expect that // vertex to be findable in OrientDB return clazz.isAssignableFrom(ids[0].getClass()) ? IteratorUtils.filter(IteratorUtils.map(idList, id -> elements.get(clazz.cast(id).id())).iterator(), Objects::nonNull) : IteratorUtils.filter(IteratorUtils.map(idList, id -> elements.get(idManager.convert(id))).iterator(), Objects::nonNull); } return TinkerHelper.inComputerMode(this) ? (Iterator) (clazz.equals(Vertex.class) ? IteratorUtils.filter((Iterator) iterator, t -> this.graphComputerView.legalVertex(t)) : IteratorUtils.filter((Iterator) iterator, t -> this.graphComputerView.legalEdge(t.outVertex(), t))) : iterator; } /** * Return TinkerGraph feature set. *

* Reference Implementation Help: Implementers only need to implement features for which there are * negative or instance configured features. By default, all * {@link org.apache.tinkerpop.gremlin.structure.Graph.Features} return true. */ @Override public Features features() { return features; } private void validateHomogenousIds(final List ids) { final Iterator iterator = ids.iterator(); Object id = iterator.next(); if (id == null) throw Graph.Exceptions.idArgsMustBeEitherIdOrElement(); final Class firstClass = id.getClass(); while (iterator.hasNext()) { id = iterator.next(); if (id == null || !id.getClass().equals(firstClass)) throw Graph.Exceptions.idArgsMustBeEitherIdOrElement(); } } public boolean isClosed() { return closed; } public class TinkerGraphFeatures implements Features { private final TinkerGraphGraphFeatures graphFeatures = new TinkerGraphGraphFeatures(); private final TinkerGraphEdgeFeatures edgeFeatures = new TinkerGraphEdgeFeatures(); private final TinkerGraphVertexFeatures vertexFeatures = new TinkerGraphVertexFeatures(); private TinkerGraphFeatures() { } @Override public GraphFeatures graph() { return graphFeatures; } @Override public EdgeFeatures edge() { return edgeFeatures; } @Override public VertexFeatures vertex() { return vertexFeatures; } @Override public String toString() { return StringFactory.featureString(this); } } public class TinkerGraphVertexFeatures implements Features.VertexFeatures { private final TinkerGraphVertexPropertyFeatures vertexPropertyFeatures = new TinkerGraphVertexPropertyFeatures(); private TinkerGraphVertexFeatures() { } @Override public Features.VertexPropertyFeatures properties() { return vertexPropertyFeatures; } @Override public boolean supportsCustomIds() { return false; } @Override public boolean willAllowId(final Object id) { return vertexIdManager.allow(id); } @Override public VertexProperty.Cardinality getCardinality(final String key) { return defaultVertexPropertyCardinality; } } public class TinkerGraphEdgeFeatures implements Features.EdgeFeatures { private TinkerGraphEdgeFeatures() { } @Override public boolean supportsCustomIds() { return false; } @Override public boolean willAllowId(final Object id) { return edgeIdManager.allow(id); } } public class TinkerGraphGraphFeatures implements Features.GraphFeatures { private TinkerGraphGraphFeatures() { } @Override public boolean supportsConcurrentAccess() { return false; } @Override public boolean supportsTransactions() { return false; } @Override public boolean supportsThreadedTransactions() { return false; } } public class TinkerGraphVertexPropertyFeatures implements Features.VertexPropertyFeatures { private TinkerGraphVertexPropertyFeatures() { } @Override public boolean supportsCustomIds() { return false; } @Override public boolean willAllowId(final Object id) { return vertexIdManager.allow(id); } } ///////////// GRAPH SPECIFIC INDEXING METHODS /////////////// /** * Create an index for said element class ({@link Vertex} or {@link Edge}) and said property key. * Whenever an element has the specified key mutated, the index is updated. * When the index is created, all existing elements are indexed to ensure that they are captured by the index. * * @param key the property key to index * @param elementClass the element class to index * @param The type of the element class */ public void createIndex(final String key, final Class elementClass) { if (Vertex.class.isAssignableFrom(elementClass)) { if (null == this.vertexIndex) this.vertexIndex = new TinkerIndex<>(this, Vertex.class); this.vertexIndex.createKeyIndex(key); } else if (Edge.class.isAssignableFrom(elementClass)) { if (null == this.edgeIndex) this.edgeIndex = new TinkerIndex<>(this, Edge.class); this.edgeIndex.createKeyIndex(key); } else { throw new IllegalArgumentException("Class is not indexable: " + elementClass); } } /** * Drop the index for the specified element class ({@link Vertex} or {@link Edge}) and key. * * @param key the property key to stop indexing * @param elementClass the element class of the index to drop * @param The type of the element class */ public void dropIndex(final String key, final Class elementClass) { if (Vertex.class.isAssignableFrom(elementClass)) { if (null != this.vertexIndex) this.vertexIndex.dropKeyIndex(key); } else if (Edge.class.isAssignableFrom(elementClass)) { if (null != this.edgeIndex) this.edgeIndex.dropKeyIndex(key); } else { throw new IllegalArgumentException("Class is not indexable: " + elementClass); } } /** * Return all the keys currently being index for said element class ({@link Vertex} or {@link Edge}). * * @param elementClass the element class to get the indexed keys for * @param The type of the element class * @return the set of keys currently being indexed */ public Set getIndexedKeys(final Class elementClass) { if (Vertex.class.isAssignableFrom(elementClass)) { return null == this.vertexIndex ? Collections.emptySet() : this.vertexIndex.getIndexedKeys(); } else if (Edge.class.isAssignableFrom(elementClass)) { return null == this.edgeIndex ? Collections.emptySet() : this.edgeIndex.getIndexedKeys(); } else { throw new IllegalArgumentException("Class is not indexable: " + elementClass); } } /** * Construct an {@link TinkerGraph.IdManager} from the TinkerGraph {@code Configuration}. */ private static IdManager selectIdManager(final Configuration config, final String configKey, final Class clazz) { return DefaultIdManager.LONG; } /** * TinkerGraph will use an implementation of this interface to generate identifiers when a user does not supply * them and to handle identifier conversions when querying to provide better flexibility with respect to * handling different data types that mean the same thing. For example, the * {@link DefaultIdManager#LONG} implementation will allow {@code g.vertices(1l, 2l)} and * {@code g.vertices(1, 2)} to both return values. * * @param the id type */ public interface IdManager { /** * Generate an identifier which should be unique to the {@link TinkerGraph} instance. */ T getNextId(final TinkerGraph graph); /** * Convert an identifier to the type required by the manager. */ T convert(final Object id); /** * Determine if an identifier is allowed by this manager given its type. */ boolean allow(final Object id); } /** * A default set of {@link IdManager} implementations for common identifier types. */ public enum DefaultIdManager implements IdManager { /** * Manages identifiers of type {@code Long}. Will convert any class that extends from {@link Number} to a * {@link Long} and will also attempt to convert {@code String} values */ LONG { @Override public Long getNextId(final TinkerGraph graph) { return Stream.generate(() -> (graph.currentId.incrementAndGet())).findAny().get(); } @Override public Object convert(final Object id) { if (null == id) return null; else if (id instanceof Long) return id; else if (id instanceof Number) return ((Number) id).longValue(); else if (id instanceof String) return Long.parseLong((String) id); else throw new IllegalArgumentException(String.format("Expected an id that is convertible to Long but received %s", id.getClass())); } @Override public boolean allow(final Object id) { return id instanceof Number || id instanceof String; } }, /** * Manages identifiers of type {@code Integer}. Will convert any class that extends from {@link Number} to a * {@link Integer} and will also attempt to convert {@code String} values */ INTEGER { @Override public Integer getNextId(final TinkerGraph graph) { return Stream.generate(() -> (graph.currentId.incrementAndGet())).map(Long::intValue).findAny().get(); } @Override public Object convert(final Object id) { if (null == id) return null; else if (id instanceof Integer) return id; else if (id instanceof Number) return ((Number) id).intValue(); else if (id instanceof String) return Integer.parseInt((String) id); else throw new IllegalArgumentException(String.format("Expected an id that is convertible to Integer but received %s", id.getClass())); } @Override public boolean allow(final Object id) { return id instanceof Number || id instanceof String; } }, /** * Manages identifiers of type {@link java.util.UUID}. Will convert {@code String} values to * {@link java.util.UUID}. */ UUID { @Override public UUID getNextId(final TinkerGraph graph) { return java.util.UUID.randomUUID(); } @Override public Object convert(final Object id) { if (null == id) return null; else if (id instanceof java.util.UUID) return id; else if (id instanceof String) return java.util.UUID.fromString((String) id); else throw new IllegalArgumentException(String.format("Expected an id that is convertible to UUID but received %s", id.getClass())); } @Override public boolean allow(final Object id) { return id instanceof UUID || id instanceof String; } }, /** * Manages identifiers of any type. This represents the default way {@link TinkerGraph} has always worked. * In other words, there is no identifier conversion so if the identifier of a vertex is a {@code Long}, then * trying to request it with an {@code Integer} will have no effect. Also, like the original * {@link TinkerGraph}, it will generate {@link Long} values for identifiers. */ ANY { @Override public Long getNextId(final TinkerGraph graph) { return Stream.generate(() -> (graph.currentId.incrementAndGet())).findAny().get(); } @Override public Object convert(final Object id) { return id; } @Override public boolean allow(final Object id) { return true; } } } }