blob: 2f01dcf28adc450bf882a6bd0927d8b377c2655f [file] [log] [blame]
/*
* 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.process.computer;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.step.branch.UnionStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.filter.RangeGlobalStep;
import org.apache.tinkerpop.gremlin.process.traversal.step.map.VertexStep;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil;
import org.apache.tinkerpop.gremlin.structure.Direction;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import java.io.Serializable;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* GraphFilter is used by {@link GraphComputer} implementations to prune the source graph data being loaded into the OLAP system.
* There are two types of filters: a {@link Vertex} filter and an {@link Edge} filter.
* The vertex filter is a {@link Traversal} that can only check the id, label, and properties of the vertex.
* The edge filter is a {@link Traversal} that starts at the vertex are emits all legal incident edges.
* If no vertex filter is provided, then no vertices are filtered. If no edge filter is provided, then no edges are filtered.
* The use of a GraphFilter can greatly reduce the amount of data processed by the {@link GraphComputer}.
* For instance, for {@code g.V().count()}, there is no reason to load edges, and thus, the edge filter can be {@code bothE().limit(0)}.
*
* @author Marko A. Rodriguez (http://markorodriguez.com)
*/
public final class GraphFilter implements Cloneable, Serializable {
/**
* A enum denoting whether a particular result will be allowed or not.
* {@link Legal#YES} means that the specified element set will definitely not be removed by {@link GraphFilter}.
* {@link Legal#MAYBE} means that the element set may or may not be filtered out by the {@link GraphFilter}.
* {@link Legal#NO} means that the specified element set will definitely be removed by {@link GraphFilter}.
*/
public enum Legal {
YES, MAYBE, NO;
/**
* The enum is either {@link Legal#YES} or {@link Legal#MAYBE}.
*
* @return true if potentially legal.
*/
public boolean positive() {
return this != NO;
}
/**
* The enum is {@link Legal#NO}.
*
* @return true if definitely not legal.
*/
public boolean negative() {
return this == NO;
}
}
private Traversal.Admin<Vertex, Vertex> vertexFilter = null;
private Traversal.Admin<Vertex, Edge> edgeFilter = null;
private Map<Direction, Map<String, Legal>> edgeLegality = new EnumMap<>(Direction.class);
private boolean allowNoEdges = false;
public GraphFilter() {
// no args constructor
}
public GraphFilter(final Computer computer) {
if (null != computer.getVertices())
this.setVertexFilter(computer.getVertices());
if (null != computer.getEdges())
this.setEdgeFilter(computer.getEdges());
}
/**
* Set the filter for selecting vertices from the source graph.
* The vertex filter can only access the vertex, its properties, and its properties properties.
* Vertex filters can not access the incident edges of the vertex.
*
* @param vertexFilter The {@link Traversal} that will either let the vertex pass or not.
*/
public void setVertexFilter(final Traversal<Vertex, Vertex> vertexFilter) {
if (!TraversalHelper.isLocalProperties(vertexFilter.asAdmin()))
throw GraphComputer.Exceptions.vertexFilterAccessesIncidentEdges(vertexFilter);
this.vertexFilter = vertexFilter.asAdmin().clone();
}
/**
* Set the filter for selecting edges from the source graph.
* The edge filter can only access the local star graph (not adjacent vertices).
*
* @param edgeFilter The {@link Traversal} that will generate the legal incident edges of the vertex.
*/
public void setEdgeFilter(final Traversal<Vertex, Edge> edgeFilter) {
if (!TraversalHelper.isLocalStarGraph(edgeFilter.asAdmin()))
throw GraphComputer.Exceptions.edgeFilterAccessesAdjacentVertices(edgeFilter);
this.edgeFilter = edgeFilter.asAdmin().clone();
////
this.edgeLegality = new EnumMap<>(Direction.class);
this.edgeLegality.put(Direction.OUT, new HashMap<>());
this.edgeLegality.put(Direction.IN, new HashMap<>());
this.edgeLegality.put(Direction.BOTH, new HashMap<>());
if (this.edgeFilter.getEndStep() instanceof RangeGlobalStep && 0 == ((RangeGlobalStep) this.edgeFilter.getEndStep()).getHighRange())
this.allowNoEdges = true;
////
if (this.edgeFilter.getStartStep() instanceof VertexStep) {
final VertexStep step = (VertexStep) this.edgeFilter.getStartStep();
final Map<String, Legal> map = this.edgeLegality.get(step.getDirection());
if (step.returnsEdge()) {
if (step.getEdgeLabels().length == 0)
map.put(null, 1 == this.edgeFilter.getSteps().size() ? Legal.YES : Legal.MAYBE);
else {
for (final String label : step.getEdgeLabels()) {
map.put(label, 1 == this.edgeFilter.getSteps().size() ? Legal.YES : Legal.MAYBE);
}
}
}
} else if (this.edgeFilter.getStartStep() instanceof UnionStep) {
final UnionStep<?, ?> step = (UnionStep) this.edgeFilter.getStartStep();
for (final Traversal.Admin<?, ?> union : step.getGlobalChildren()) {
if (union.getStartStep() instanceof VertexStep) {
final VertexStep vertexStep = (VertexStep) union.getStartStep();
final Map<String, Legal> map = this.edgeLegality.get(vertexStep.getDirection());
if (vertexStep.returnsEdge()) {
if (vertexStep.getEdgeLabels().length == 0)
map.put(null, 2 == union.getSteps().size() ? Legal.YES : Legal.MAYBE);
else {
for (final String label : vertexStep.getEdgeLabels()) {
map.put(label, 2 == union.getSteps().size() ? Legal.YES : Legal.MAYBE);
}
}
}
}
}
}
final Map<String, Legal> outMap = this.edgeLegality.get(Direction.OUT);
final Map<String, Legal> inMap = this.edgeLegality.get(Direction.IN);
final Map<String, Legal> bothMap = this.edgeLegality.get(Direction.BOTH);
for (final Map.Entry<String, Legal> entry : bothMap.entrySet()) {
final Legal legal = inMap.get(entry.getKey());
if (null == legal || legal.compareTo(entry.getValue()) > 0)
inMap.put(entry.getKey(), entry.getValue());
}
for (final Map.Entry<String, Legal> entry : bothMap.entrySet()) {
final Legal legal = outMap.get(entry.getKey());
if (null == legal || legal.compareTo(entry.getValue()) > 0)
outMap.put(entry.getKey(), entry.getValue());
}
for (final Map.Entry<String, Legal> entry : outMap.entrySet()) {
final Legal legal = inMap.get(entry.getKey());
if (null != legal)
bothMap.put(entry.getKey(), legal.compareTo(entry.getValue()) > 0 ? legal : entry.getValue());
}
if (outMap.isEmpty() && inMap.isEmpty() && bothMap.isEmpty()) { // the edge filter could not be reasoned on
outMap.put(null, Legal.MAYBE);
inMap.put(null, Legal.MAYBE);
bothMap.put(null, Legal.MAYBE);
}
}
/**
* Returns true if the provided vertex meets the vertex-filter criteria.
* If no vertex filter is provided, then the vertex is considered legal.
*
* @param vertex the vertex to test for legality
* @return whether the vertex is {@link Legal#YES}.
*/
public boolean legalVertex(final Vertex vertex) {
return null == this.vertexFilter || TraversalUtil.test(vertex, this.vertexFilter);
}
/**
* Returns an iterator of legal edges incident to the provided vertex.
* If no edge filter is provided, then all incident edges are returned.
*
* @param vertex the vertex whose legal edges are to be access.
* @return an iterator of edges that are {@link Legal#YES}.
*/
public Iterator<Edge> legalEdges(final Vertex vertex) {
return null == this.edgeFilter ?
vertex.edges(Direction.BOTH) :
TraversalUtil.applyAll(vertex, this.edgeFilter);
}
/**
* Get the vertex filter associated with this graph filter.
*
* @return the vertex filter or null if no vertex filter was provided.
*/
public final Traversal.Admin<Vertex, Vertex> getVertexFilter() {
return this.vertexFilter;
}
/**
* Get the edge filter associated with this graph filter.
*
* @return the edge filter or null if no edge filter was provided.
*/
public final Traversal.Admin<Vertex, Edge> getEdgeFilter() {
return this.edgeFilter;
}
/**
* Whether filters have been defined.
*
* @return true if either a vertex or edge filter has been provided.
*/
public boolean hasFilter() {
return this.vertexFilter != null || this.edgeFilter != null;
}
/**
* Whether an edge filter has been defined.
*
* @return true if an edge filter was provided.
*/
public boolean hasEdgeFilter() {
return this.edgeFilter != null;
}
/**
* Whether a vertex filter has been defined.
*
* @return true if a vertex filter was provided.
*/
public boolean hasVertexFilter() {
return this.vertexFilter != null;
}
/**
* For a particular edge directionality, get all the {@link Legal#YES} or {@link Legal#MAYBE} edge labels.
* If the label set contains {@code null}, then all edge labels for that direction are positively legal.
*
* @param direction the direction to get the positively legal edge labels for.
* @return the set of positively legal edge labels for the direction.
*/
public Set<String> getLegallyPositiveEdgeLabels(final Direction direction) {
if (null == this.edgeFilter)
return Collections.singleton(null);
else if (this.allowNoEdges)
return Collections.emptySet();
else
return this.edgeLegality.get(direction).containsKey(null) ?
Collections.singleton(null) :
this.edgeLegality.get(direction).entrySet()
.stream()
.filter(entry -> entry.getValue().positive())
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
/**
* Get the legality of a particular edge direction and label.
*
* @param direction the direction of the edge.
* @param label the label of the edge.
* @return the {@link Legal} of the arguments.
*/
public Legal checkEdgeLegality(final Direction direction, final String label) {
if (null == this.edgeFilter)
return Legal.YES;
if (this.checkEdgeLegality(direction).negative())
return Legal.NO;
final Map<String, Legal> legalMap = this.edgeLegality.get(direction);
if (legalMap.containsKey(label))
return legalMap.get(label);
else if (legalMap.containsKey(null))
return legalMap.get(null);
else
return Legal.NO;
}
/**
* Get the legality of a particular edge direction.
*
* @param direction the direction of the edge.
* @return the {@link Legal} of the edge direction.
*/
public Legal checkEdgeLegality(final Direction direction) {
if (null == this.edgeFilter)
return Legal.YES;
else if (this.allowNoEdges)
return Legal.NO;
return this.edgeLegality.get(direction).values()
.stream()
.reduce(Legal.NO, (a, b) -> a.compareTo(b) < 0 ? a : b);
}
@Override
public int hashCode() {
return (null == this.edgeFilter ? 111 : this.edgeFilter.hashCode()) ^ (null == this.vertexFilter ? 222 : this.vertexFilter.hashCode());
}
@Override
public boolean equals(final Object object) {
if (!(object instanceof GraphFilter))
return false;
else if (((GraphFilter) object).hasVertexFilter() && !((GraphFilter) object).vertexFilter.equals(this.vertexFilter))
return false;
else if (((GraphFilter) object).hasEdgeFilter() && !((GraphFilter) object).edgeFilter.equals(this.edgeFilter))
return false;
else
return true;
}
@Override
public GraphFilter clone() {
try {
final GraphFilter clone = (GraphFilter) super.clone();
if (null != this.vertexFilter)
clone.vertexFilter = this.vertexFilter.clone();
if (null != this.edgeFilter)
clone.edgeFilter = this.edgeFilter.clone();
return clone;
} catch (final CloneNotSupportedException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
@Override
public String toString() {
if (!this.hasFilter())
return "graphfilter[none]";
else if (this.hasVertexFilter() && this.hasEdgeFilter())
return "graphfilter[" + this.vertexFilter + "," + this.edgeFilter + "]";
else if (this.hasVertexFilter())
return "graphfilter[" + this.vertexFilter + "]";
else
return "graphfilter[" + this.edgeFilter + "]";
}
}