diff --git a/java/server/src/main/java/org/softwareheritage/graph/AllowedEdges.java b/java/server/src/main/java/org/softwareheritage/graph/AllowedEdges.java index 8c74758..407324c 100644 --- a/java/server/src/main/java/org/softwareheritage/graph/AllowedEdges.java +++ b/java/server/src/main/java/org/softwareheritage/graph/AllowedEdges.java @@ -1,90 +1,88 @@ package org.softwareheritage.graph; import java.util.ArrayList; import org.softwareheritage.graph.Graph; import org.softwareheritage.graph.Node; /** * Edge restriction based on node types, used when visiting the graph. * * @author Thibault Allançon * @version 1.0 * @since 1.0 */ public class AllowedEdges { /** Graph on which edge restriction is performed */ Graph graph; /** * 2D boolean matrix storing access rights for all combination of src/dst node types (first - * dimension is source, second dimension is destination) + * dimension is source, second dimension is destination), when edge restriction is not enforced + * this array is set to null for early bypass. */ - boolean[][] allowed; + public boolean[][] restrictedTo; /** * Constructor. * * @param graph the graph on which to perform edge restriction * @param edgesFmt a formatted string describing allowed edges (TODO: link API doc) */ public AllowedEdges(Graph graph, String edgesFmt) { this.graph = graph; int nbNodeTypes = Node.Type.values().length; - this.allowed = new boolean[nbNodeTypes][nbNodeTypes]; + this.restrictedTo = new boolean[nbNodeTypes][nbNodeTypes]; // Special values (null, empty, "*") if (edgesFmt == null || edgesFmt.isEmpty()) { return; } if (edgesFmt.equals("*")) { - for (int i = 0; i < nbNodeTypes; i++) { - for (int j = 0; j < nbNodeTypes; j++) { - allowed[i][j] = true; - } - } + // Allows for quick bypass (with simple null check) when no edge restriction + restrictedTo = null; return; } // Format: "src1:dst1,src2:dst2,[...]" // TODO: link API doc String[] edgeTypes = edgesFmt.split(","); for (String edgeType : edgeTypes) { String[] nodeTypes = edgeType.split(":"); if (nodeTypes.length != 2) { throw new IllegalArgumentException("Cannot parse edge type: " + edgeType); } ArrayList srcTypes = Node.Type.parse(nodeTypes[0]); ArrayList dstTypes = Node.Type.parse(nodeTypes[1]); for (Node.Type srcType : srcTypes) { for (Node.Type dstType : dstTypes) { - allowed[srcType.ordinal()][dstType.ordinal()] = true; + restrictedTo[srcType.ordinal()][dstType.ordinal()] = true; } } } } /** * Checks if a given edge can be followed during graph traversal. * * @param srcNodeId edge source node * @param dstNodeId edge destination node * @return true if allowed and false otherwise */ public boolean isAllowed(long srcNodeId, long dstNodeId) { Node.Type srcType = graph.getNodeType(srcNodeId); Node.Type dstType = graph.getNodeType(dstNodeId); - return isAllowed(srcType, dstType); + return restrictedTo[srcType.ordinal()][dstType.ordinal()]; } /** * Works like {@link AllowedEdges#isAllowed(long, long)} but with node types directly instead of * node ids. * * @see AllowedEdges#isAllowed(long, long) */ public boolean isAllowed(Node.Type srcType, Node.Type dstType) { - return allowed[srcType.ordinal()][dstType.ordinal()]; + return restrictedTo[srcType.ordinal()][dstType.ordinal()]; } } diff --git a/java/server/src/main/java/org/softwareheritage/graph/Neighbors.java b/java/server/src/main/java/org/softwareheritage/graph/Neighbors.java index f2d08e3..9840350 100644 --- a/java/server/src/main/java/org/softwareheritage/graph/Neighbors.java +++ b/java/server/src/main/java/org/softwareheritage/graph/Neighbors.java @@ -1,84 +1,94 @@ package org.softwareheritage.graph; import java.util.Iterator; import it.unimi.dsi.fastutil.longs.LongBigArrays; import org.softwareheritage.graph.AllowedEdges; import org.softwareheritage.graph.Graph; /** * Iterator class to go over a node neighbors in the graph. * * @author Thibault Allançon * @version 1.0 * @since 1.0 */ public class Neighbors implements Iterable { /** Graph used to explore neighbors */ Graph graph; /** Boolean to specify the use of the transposed graph */ boolean useTransposed; /** Graph edge restriction */ AllowedEdges edges; /** Source node from which neighbors will be listed */ long srcNodeId; /** * Constructor. * * @param graph graph used to explore neighbors * @param useTransposed boolean value to use transposed graph * @param edges edges allowed to be used in the graph * @param srcNodeId source node from where to list neighbors */ public Neighbors(Graph graph, boolean useTransposed, AllowedEdges edges, long srcNodeId) { this.graph = graph; this.useTransposed = useTransposed; this.edges = edges; this.srcNodeId = srcNodeId; } @Override public Iterator iterator() { return new NeighborsIterator(); } /** * Inner class for {@link Neighbors} iterator. * * @author Thibault Allançon * @version 1.0 * @since 1.0 */ public class NeighborsIterator implements Iterator { long nextNeighborIdx; long nbNeighbors; long[][] neighbors; public NeighborsIterator() { this.nextNeighborIdx = -1; this.nbNeighbors = graph.degree(srcNodeId, useTransposed); this.neighbors = graph.neighbors(srcNodeId, useTransposed); } - // Look ahead because with edge restriction not all neighbors are considered public boolean hasNext() { + // No edge restriction case: bypass type checks and skip to next neighbor + if (edges.restrictedTo == null) { + if (nextNeighborIdx + 1 < nbNeighbors) { + nextNeighborIdx++; + return true; + } else { + return false; + } + } + + // Edge restriction case: look ahead for next neighbor for (long lookAheadIdx = nextNeighborIdx + 1; lookAheadIdx < nbNeighbors; lookAheadIdx++) { long nextNodeId = LongBigArrays.get(neighbors, lookAheadIdx); if (edges.isAllowed(srcNodeId, nextNodeId)) { nextNeighborIdx = lookAheadIdx; return true; } } return false; } public Long next() { long nextNodeId = LongBigArrays.get(neighbors, nextNeighborIdx); return nextNodeId; } } } diff --git a/java/server/src/test/java/org/softwareheritage/graph/AllowedEdgesTest.java b/java/server/src/test/java/org/softwareheritage/graph/AllowedEdgesTest.java index 02e0642..66c633e 100644 --- a/java/server/src/test/java/org/softwareheritage/graph/AllowedEdgesTest.java +++ b/java/server/src/test/java/org/softwareheritage/graph/AllowedEdgesTest.java @@ -1,119 +1,121 @@ package org.softwareheritage.graph; import java.util.ArrayList; import org.junit.Test; -import static org.junit.Assert.assertTrue; +import org.junit.Assert; import org.softwareheritage.graph.AllowedEdges; import org.softwareheritage.graph.GraphTest; import org.softwareheritage.graph.Node; public class AllowedEdgesTest extends GraphTest { class EdgeType { Node.Type src; Node.Type dst; public EdgeType(Node.Type src, Node.Type dst) { this.src = src; this.dst = dst; } @Override public boolean equals(Object otherObj) { if (otherObj == this) return true; if (!(otherObj instanceof EdgeType)) return false; EdgeType other = (EdgeType) otherObj; return src == other.src && dst == other.dst; } } void assertEdgeRestriction(AllowedEdges edges, ArrayList expectedAllowed) { Node.Type[] nodeTypes = Node.Type.values(); for (Node.Type src : nodeTypes) { for (Node.Type dst : nodeTypes) { EdgeType edge = new EdgeType(src, dst); boolean isAllowed = edges.isAllowed(src, dst); boolean isExpected = false; for (EdgeType expected : expectedAllowed) { if (expected.equals(edge)) { isExpected = true; break; } } - assertTrue("Edge type: " + src + " -> " + dst, isAllowed == isExpected); + Assert.assertTrue("Edge type: " + src + " -> " + dst, isAllowed == isExpected); } } } @Test public void dirToDirDirToCntEdges() { Graph graph = getGraph(); AllowedEdges edges = new AllowedEdges(graph, "dir:dir,dir:cnt"); ArrayList expected = new ArrayList<>(); expected.add(new EdgeType(Node.Type.DIR, Node.Type.DIR)); expected.add(new EdgeType(Node.Type.DIR, Node.Type.CNT)); assertEdgeRestriction(edges, expected); } @Test public void relToRevRevToRevRevToDirEdges() { Graph graph = getGraph(); AllowedEdges edges = new AllowedEdges(graph, "rel:rev,rev:rev,rev:dir"); ArrayList expected = new ArrayList<>(); expected.add(new EdgeType(Node.Type.REL, Node.Type.REV)); expected.add(new EdgeType(Node.Type.REV, Node.Type.REV)); expected.add(new EdgeType(Node.Type.REV, Node.Type.DIR)); assertEdgeRestriction(edges, expected); } @Test public void revToAllDirToDirEdges() { Graph graph = getGraph(); AllowedEdges edges = new AllowedEdges(graph, "rev:*,dir:dir"); ArrayList expected = new ArrayList<>(); for (Node.Type dst : Node.Type.values()) { expected.add(new EdgeType(Node.Type.REV, dst)); } expected.add(new EdgeType(Node.Type.DIR, Node.Type.DIR)); assertEdgeRestriction(edges, expected); } @Test public void allToCntEdges() { Graph graph = getGraph(); AllowedEdges edges = new AllowedEdges(graph, "*:cnt"); ArrayList expected = new ArrayList<>(); for (Node.Type src : Node.Type.values()) { expected.add(new EdgeType(src, Node.Type.CNT)); } assertEdgeRestriction(edges, expected); } @Test public void allEdges() { Graph graph = getGraph(); AllowedEdges edges = new AllowedEdges(graph, "*:*"); - AllowedEdges edges2 = new AllowedEdges(graph, "*"); ArrayList expected = new ArrayList<>(); for (Node.Type src : Node.Type.values()) { for (Node.Type dst : Node.Type.values()) { expected.add(new EdgeType(src, dst)); } } assertEdgeRestriction(edges, expected); - assertEdgeRestriction(edges2, expected); + + // Special null value used to quickly bypass edge check when no restriction + AllowedEdges edges2 = new AllowedEdges(graph, "*"); + Assert.assertNull(edges2.restrictedTo); } @Test public void noEdges() { Graph graph = getGraph(); AllowedEdges edges = new AllowedEdges(graph, ""); AllowedEdges edges2 = new AllowedEdges(graph, null); ArrayList expected = new ArrayList<>(); assertEdgeRestriction(edges, expected); assertEdgeRestriction(edges2, expected); } }