/*
* Copyright (c) 2002-2017 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product may include a number of subcomponents with
* separate copyright notices and license terms. Your use of the source
* code for these subcomponents is subject to the terms and
* conditions of the subcomponent's license, as noted in the LICENSE file.
*/
package org.neo4j.ogm.testutil;
import static org.neo4j.graphdb.Direction.*;
import static org.neo4j.helpers.collection.Iterables.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import org.junit.Assert;
import org.neo4j.graphdb.*;
import org.neo4j.test.TestGraphDatabaseFactory;
import org.parboiled.common.StringUtils;
/**
* Utility methods used to facilitate testing against a real Neo4j database.
*
* @author Michal Bachman
* @author Mark Angrish
*/
public final class GraphTestUtils {
private static GraphDatabaseService otherDatabase = new TestGraphDatabaseFactory().newImpermanentDatabase();
private GraphTestUtils() {
// this class cannot be instantiated
}
/**
* Checks that the graph in the specified {@link GraphDatabaseService} is the same as the graph that the given cypher
* produces.
*
* @param graphDatabase The {@link GraphDatabaseService} to check
* @param sameGraphCypher The Cypher create statement, which communicates the desired state of the database
* @throws AssertionError if the cypher doesn't produce a graph that matches the state of the given database
*/
public static synchronized void assertSameGraph(GraphDatabaseService graphDatabase, String sameGraphCypher) {
otherDatabase.execute(sameGraphCypher);
try {
try (Transaction tx = graphDatabase.beginTx()) {
try (Transaction tx2 = otherDatabase.beginTx()) {
doAssertSubgraph(graphDatabase, otherDatabase, "existing database");
doAssertSubgraph(otherDatabase, graphDatabase, "Cypher-created database");
tx2.failure();
}
tx.failure();
}
} finally {
otherDatabase.execute("MATCH (n) OPTIONAL MATCH (n) DETACH DELETE n");
}
}
private static void doAssertSubgraph(GraphDatabaseService database, GraphDatabaseService otherDatabase, String firstDatabaseName) {
Map<Long, Long[]> sameNodesMap = buildSameNodesMap(database, otherDatabase, firstDatabaseName);
Set<Map<Long, Long>> nodeMappings = buildNodeMappingPermutations(sameNodesMap, otherDatabase);
if (nodeMappings.size() == 1) {
assertRelationshipsMappingExistsForSingleNodeMapping(database, otherDatabase, nodeMappings.iterator().next(), firstDatabaseName);
return;
}
for (Map<Long, Long> nodeMapping : nodeMappings) {
if (relationshipsMappingExists(database, otherDatabase, nodeMapping)) {
return;
}
}
Assert.fail("There is no corresponding relationship mapping for any of the possible node mappings");
}
private static Set<Map<Long, Long>> buildNodeMappingPermutations(Map<Long, Long[]> sameNodesMap, GraphDatabaseService otherDatabase) {
Set<Map<Long, Long>> result = new HashSet<>();
result.add(new HashMap<>());
for (Map.Entry<Long, Long[]> entry : sameNodesMap.entrySet()) {
Set<Map<Long, Long>> newResult = new HashSet<>();
for (Long target : entry.getValue()) {
for (Map<Long, Long> mapping : result) {
if (!mapping.values().contains(target)) {
Map<Long, Long> newMapping = new HashMap<>(mapping);
newMapping.put(entry.getKey(), target);
newResult.add(newMapping);
}
}
}
if (newResult.isEmpty()) {
Assert.fail("Could not find a node corresponding to: " + print(otherDatabase.getNodeById(entry.getKey()))
+ ". There are most likely more nodes with the same characteristics (labels, properties) in your "
+ "cypher CREATE statement but fewer in the database.");
}
result = newResult;
}
return result;
}
public static Iterable<Node> allNodes(GraphDatabaseService graphDatabaseService) {
try {
Method allNodes = GraphDatabaseService.class.getMethod("getAllNodes");
return (Iterable<Node>) allNodes.invoke(graphDatabaseService);
} catch (NoSuchMethodException nsme) {
try {
Class clazz = Class.forName("org.neo4j.tooling.GlobalGraphOperations");
try {
Method at = clazz.getMethod("at", GraphDatabaseService.class);
Object instance = at.invoke(null, graphDatabaseService);
Method allNodes = instance.getClass().getMethod("getAllNodes");
return (Iterable<Node>) allNodes.invoke(instance);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException nsme2) {
throw new RuntimeException(nsme2);
}
} catch (ClassNotFoundException cnfe) {
throw new RuntimeException(cnfe);
}
} catch (InvocationTargetException | IllegalAccessException ite) {
throw new RuntimeException(ite);
}
}
public static Iterable<Relationship> allRelationships(GraphDatabaseService graphDatabaseService) {
try {
Method allRelationships = GraphDatabaseService.class.getMethod("getAllRelationships");
return (Iterable<Relationship>) allRelationships.invoke(graphDatabaseService);
} catch (NoSuchMethodException nsme) {
try {
Class clazz = Class.forName("org.neo4j.tooling.GlobalGraphOperations");
try {
Method at = clazz.getMethod("at", GraphDatabaseService.class);
Object instance = at.invoke(null, graphDatabaseService);
Method allRelationships = instance.getClass().getMethod("getAllRelationships");
return (Iterable<Relationship>) allRelationships.invoke(instance);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException nsme2) {
throw new RuntimeException(nsme2);
}
} catch (ClassNotFoundException cnfe) {
throw new RuntimeException(cnfe);
}
} catch (InvocationTargetException | IllegalAccessException ite) {
throw new RuntimeException(ite);
}
}
private static Map<Long, Long[]> buildSameNodesMap(GraphDatabaseService database, GraphDatabaseService otherDatabase,
String firstDatabaseName) {
Map<Long, Long[]> sameNodesMap = new HashMap<>(); //map of nodeID and IDs of nodes that match
for (Node node : allNodes(otherDatabase)) {
Iterable<Node> sameNodes = findSameNodes(database, node); //List of all nodes that match this
//fail fast
if (!sameNodes.iterator().hasNext()) {
Assert.fail("There is no corresponding node to " + print(node) + " in " + firstDatabaseName);
}
Set<Long> sameNodeIds = new HashSet<>();
for (Node sameNode : sameNodes) {
sameNodeIds.add(sameNode.getId());
}
sameNodesMap.put(node.getId(), sameNodeIds.toArray(new Long[sameNodeIds.size()]));
}
return sameNodesMap;
}
public static Iterable<Node> findSameNodes(GraphDatabaseService database, Node node) {
Iterator<Label> labels = node.getLabels().iterator();
if (labels.hasNext()) {
return findSameNodesByLabel(database, node, labels.next());
}
return findSameNodesWithoutLabel(database, node);
}
public static Iterable<Node> findSameNodesWithoutLabel(GraphDatabaseService database, Node node) {
Set<Node> result = new HashSet<>();
for (Node candidate : allNodes(database)) {
if (areSame(node, candidate)) {
result.add(candidate);
}
}
return result;
}
public static Iterable<Node> nodesWithLabel(GraphDatabaseService database, Label label) {
Set<Node> result = new HashSet<>();
for (Node node : allNodes(database)) {
if (node.hasLabel(label)) {
result.add(node);
}
}
return result;
}
public static Iterable<Node> findSameNodesByLabel(GraphDatabaseService database, Node node, Label label) {
Set<Node> result = new HashSet<>();
for (Node candidate : nodesWithLabel(database, label)) {
if (areSame(node, candidate)) {
result.add(candidate);
}
}
return result;
}
private static void assertRelationshipsMappingExistsForSingleNodeMapping(GraphDatabaseService database,
GraphDatabaseService otherDatabase, Map<Long, Long> mapping, String firstDatabaseName) {
Set<Long> usedRelationships = new HashSet<>();
for (Relationship relationship : allRelationships(otherDatabase)) {
if (!relationshipMappingExists(database, relationship, mapping, usedRelationships)) {
Assert.fail("No corresponding relationship found to " + print(relationship) + " in " + firstDatabaseName);
}
}
}
private static boolean relationshipsMappingExists(GraphDatabaseService database, GraphDatabaseService otherDatabase,
Map<Long, Long> mapping) {
Set<Long> usedRelationships = new HashSet<>();
for (Relationship relationship : allRelationships(otherDatabase)) {
if (!relationshipMappingExists(database, relationship, mapping, usedRelationships)) {
return false;
}
}
return true;
}
private static boolean relationshipMappingExists(GraphDatabaseService database, Relationship relationship, Map<Long, Long> nodeMapping,
Set<Long> usedRelationships) {
for (Relationship candidate : database.getNodeById(nodeMapping.get(relationship.getStartNode().getId())).getRelationships(OUTGOING)) {
if (nodeMapping.get(relationship.getEndNode().getId()).equals(candidate.getEndNode().getId())) {
if (areSame(candidate, relationship) && !usedRelationships.contains(candidate.getId())) {
usedRelationships.add(candidate.getId());
return true;
}
}
}
return false;
}
public static boolean areSame(Node node1, Node node2) {
return haveSameLabels(node1, node2) && haveSameProperties(node1, node2);
}
public static boolean areSame(Relationship relationship1, Relationship relationship2) {
return haveSameType(relationship1, relationship2) && haveSameProperties(relationship1, relationship2);
}
public static boolean haveSameLabels(Node node1, Node node2) {
if (count(node1.getLabels()) != count(node2.getLabels())) {
return false;
}
for (Label label : node1.getLabels()) {
if (!node2.hasLabel(label)) {
return false;
}
}
return true;
}
public static boolean haveSameType(Relationship relationship1, Relationship relationship2) {
return relationship1.isType(relationship2.getType());
}
public static boolean haveSameProperties(PropertyContainer pc1, PropertyContainer pc2) {
int pc1KeyCount = 0, pc2KeyCount = 0;
for (String key : pc1.getPropertyKeys()) {
pc1KeyCount++;
if (!pc2.hasProperty(key)) {
return false;
}
if (!stringRepresentationsMatch(pc1.getProperty(key), pc2.getProperty(key))) {
return false;
}
}
for (Iterator<String> it = pc2.getPropertyKeys().iterator(); it.hasNext(); it.next()) {
pc2KeyCount++;
}
return pc1KeyCount == pc2KeyCount;
}
private static String print(Node node) {
StringBuilder string = new StringBuilder("(");
List<String> labelNames = new LinkedList<>();
for (Label label : node.getLabels()) {
labelNames.add(label.name());
}
Collections.sort(labelNames);
for (String labelName : labelNames) {
string.append(":").append(labelName);
}
String props = propertiesToString(node);
if (StringUtils.isNotEmpty(props) && !labelNames.isEmpty()) {
string.append(" ");
}
string.append(props);
string.append(")");
return string.toString();
}
private static String print(Relationship relationship) {
StringBuilder string = new StringBuilder();
string.append(print(relationship.getStartNode()));
string.append("-[:").append(relationship.getType().name());
String props = propertiesToString(relationship);
if (StringUtils.isNotEmpty(props)) {
string.append(" ");
}
string.append(props);
string.append("]->");
string.append(print(relationship.getEndNode()));
return string.toString();
}
private static String propertiesToString(PropertyContainer propertyContainer) {
if (!propertyContainer.getPropertyKeys().iterator().hasNext()) {
return "";
}
StringBuilder string = new StringBuilder("{");
List<String> propertyKeys = new LinkedList<>();
for (String key : propertyContainer.getPropertyKeys()) {
propertyKeys.add(key);
}
Collections.sort(propertyKeys);
for (String key : propertyKeys) {
string.append(key).append(": ").append(propertyValueToString(propertyContainer.getProperty(key))).append(", ");
}
string.setLength(string.length() - 2);
string.append("}");
return string.toString();
}
private static boolean stringRepresentationsMatch(Object one, Object other) {
String oneString = propertyValueToString(one);
String otherString = propertyValueToString(other);
return oneString.equals(otherString);
}
private static String propertyValueToString(Object o) {
if (o instanceof byte[]) {
return Arrays.toString((byte[]) o);
}
if (o instanceof char[]) {
return Arrays.toString((char[]) o);
}
if (o instanceof boolean[]) {
return Arrays.toString((boolean[]) o);
}
if (o instanceof long[]) {
return Arrays.toString((long[]) o);
}
if (o instanceof double[]) {
return Arrays.toString((double[]) o);
}
if (o instanceof int[]) {
return Arrays.toString((int[]) o);
}
if (o instanceof short[]) {
return Arrays.toString((short[]) o);
}
if (o instanceof float[]) {
return Arrays.toString((float[]) o);
}
if (o instanceof String[]) {
return Arrays.toString((String[]) o);
}
return String.valueOf(o);
}
}