apoc.refactor.GraphRefactoring Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of apoc-core Show documentation
Show all versions of apoc-core Show documentation
Core package for Neo4j Procedures
package apoc.refactor;
import apoc.Pools;
import apoc.algo.Cover;
import apoc.refactor.util.PropertiesManager;
import apoc.refactor.util.RefactorConfig;
import apoc.result.GraphResult;
import apoc.result.NodeResult;
import apoc.result.RelationshipResult;
import apoc.util.Util;
import apoc.util.collection.Iterables;
import org.apache.commons.collections4.IterableUtils;
import org.neo4j.graphdb.*;
import org.neo4j.graphdb.schema.ConstraintType;
import org.neo4j.logging.Log;
import org.neo4j.procedure.*;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static apoc.refactor.util.PropertiesManager.mergeProperties;
import static apoc.refactor.util.RefactorConfig.RelationshipSelectionStrategy.MERGE;
import static apoc.refactor.util.RefactorUtil.*;
import static apoc.util.Util.withTransactionAndRebind;
public class GraphRefactoring {
@Context
public Transaction tx;
@Context
public GraphDatabaseService db;
@Context
public Log log;
@Context
public Pools pools;
private Stream doCloneNodes(@Name("nodes") List nodes, @Name("withRelationships") boolean withRelationships, List skipProperties) {
if (nodes == null) return Stream.empty();
return nodes.stream().map(node -> Util.rebind(tx, node)).map(node -> {
NodeRefactorResult result = new NodeRefactorResult(node.getId());
try {
Node copy = withTransactionAndRebind(db, tx, transaction -> {
Node newNode = copyLabels(node, transaction.createNode());
Map properties = node.getAllProperties();
if (skipProperties != null && !skipProperties.isEmpty())
for (String skip : skipProperties) properties.remove(skip);
newNode = copyProperties(properties, newNode);
copyLabels(node, newNode);
return newNode;
});
if (withRelationships) {
copyRelationships(node, copy, false, true);
}
return result.withOther(copy);
} catch (Exception e) {
return result.withError(e);
}
});
}
@Procedure(name = "apoc.refactor.extractNode", mode = Mode.WRITE)
@Description("Expands the given relationships into intermediate nodes.\n" +
"The intermediate nodes are connected by the given 'OUT' and 'IN' types.")
public Stream extractNode(@Name("rels") Object rels, @Name("labels") List labels, @Name("outType") String outType, @Name("inType") String inType) {
return Util.relsStream(tx, rels).map((rel) -> {
NodeRefactorResult result = new NodeRefactorResult(rel.getId());
try {
Node copy = withTransactionAndRebind(db, tx, transaction -> {
Node copyNode = copyProperties(rel, transaction.createNode(Util.labels(labels)));
copyNode.createRelationshipTo(rel.getEndNode(), RelationshipType.withName(outType));
return copyNode;
});
rel.getStartNode().createRelationshipTo(copy, RelationshipType.withName(inType));
rel.delete();
return result.withOther(copy);
} catch (Exception e) {
return result.withError(e);
}
});
}
@Procedure(name = "apoc.refactor.collapseNode", mode = Mode.WRITE)
@Description("Collapses the given node and replaces it with a relationship of the given type.")
public Stream collapseNode(@Name("nodes") Object nodes, @Name("relType") String type) {
return Util.nodeStream(tx, nodes).map((node) -> {
RelationshipRefactorResult result = new RelationshipRefactorResult(node.getId());
try {
Iterable outRels = node.getRelationships(Direction.OUTGOING);
Iterable inRels = node.getRelationships(Direction.INCOMING);
if (node.getDegree(Direction.OUTGOING) == 1 && node.getDegree(Direction.INCOMING) == 1) {
Relationship outRel = outRels.iterator().next();
Relationship inRel = inRels.iterator().next();
Relationship newRel = inRel.getStartNode().createRelationshipTo(outRel.getEndNode(), RelationshipType.withName(type));
newRel = copyProperties(node, copyProperties(inRel, copyProperties(outRel, newRel)));
for (Relationship r : inRels) r.delete();
for (Relationship r : outRels) r.delete();
node.delete();
return result.withOther(newRel);
} else {
return result.withError(String.format("Node %d has more that 1 outgoing %d or incoming %d relationships", node.getId(), node.getDegree(Direction.OUTGOING), node.getDegree(Direction.INCOMING)));
}
} catch (Exception e) {
return result.withError(e);
}
});
}
/**
* this procedure takes a list of nodes and clones them with their labels and properties
*/
@Procedure(name = "apoc.refactor.cloneNodes", mode = Mode.WRITE)
@Description("Clones the given nodes with their labels and properties.\n" +
"It is possible to skip any node properties using skipProperties (note: this only skips properties on nodes and not their relationships).")
public Stream cloneNodes(@Name("nodes") List nodes,
@Name(value = "withRelationships", defaultValue = "false") boolean withRelationships,
@Name(value = "skipProperties", defaultValue = "[]") List skipProperties) {
return doCloneNodes(nodes, withRelationships, skipProperties);
}
/**
* this procedure clones a subgraph defined by a list of nodes and relationships. The resulting clone is a disconnected subgraph,
* with no relationships connecting with the original nodes, nor with any other node outside the subgraph clone.
* This can be overridden by supplying a list of node pairings in the `standinNodes` config property, so any relationships that went to the old node, when cloned, will instead be redirected to the standin node.
* This is useful when instead of cloning a certain node or set of nodes, you want to instead redirect relationships in the resulting clone
* such that they point to some existing node in the graph.
*
* For example, this could be used to clone a branch from a tree structure (with none of the new relationships going
* to the original nodes) and to redirect any relationships from an old root node (which will not be cloned) to a different existing root node, which acts as the standin.
*
*/
@Procedure(name = "apoc.refactor.cloneSubgraphFromPaths", mode = Mode.WRITE)
@Description("Clones a sub-graph defined by the given list of paths.\n" +
"It is possible to skip any node properties using the skipProperties list via the config map.")
public Stream cloneSubgraphFromPaths(@Name("paths") List paths,
@Name(value="config", defaultValue = "{}") Map config) {
if (paths == null || paths.isEmpty()) return Stream.empty();
Set nodes = new HashSet<>();
Set rels = new HashSet<>();
for (Path path : paths) {
for (Relationship rel : path.relationships()) {
rels.add(rel);
}
for (Node node : path.nodes()) {
nodes.add(node);
}
}
List nodesList = new ArrayList<>(nodes);
List relsList = new ArrayList<>(rels);
return cloneSubgraph(nodesList, relsList, config);
}
/**
* this procedure clones a subgraph defined by a list of nodes and relationships. The resulting clone is a disconnected subgraph,
* with no relationships connecting with the original nodes, nor with any other node outside the subgraph clone.
* This can be overridden by supplying a list of node pairings in the `standinNodes` config property, so any relationships that went to the old node, when cloned, will instead be redirected to the standin node.
* This is useful when instead of cloning a certain node or set of nodes, you want to instead redirect relationships in the resulting clone
* such that they point to some existing node in the graph.
*
* For example, this could be used to clone a branch from a tree structure (with none of the new relationships going
* to the original nodes) and to redirect any relationships from an old root node (which will not be cloned) to a different existing root node, which acts as the standin.
*
*/
@Procedure(name = "apoc.refactor.cloneSubgraph", mode = Mode.WRITE)
@Description("Clones the given nodes with their labels and properties (optionally skipping any properties in the skipProperties list via the config map), and clones the given relationships.\n" +
"If no relationships are provided, all existing relationships between the given nodes will be cloned.")
public Stream cloneSubgraph(@Name("nodes") List nodes,
@Name(value="rels", defaultValue = "[]") List rels,
@Name(value="config", defaultValue = "{}") Map config) {
if (nodes == null || nodes.isEmpty()) return Stream.empty();
// empty or missing rels list means get all rels between nodes
if (rels == null || rels.isEmpty()) {
rels = Cover.coverNodes(nodes).collect(Collectors.toList());
}
Map copyMap = new HashMap<>(nodes.size());
List resultStream = new ArrayList<>();
Map standinMap = generateStandinMap((List>) config.getOrDefault("standinNodes", Collections.emptyList()));
List skipProperties = (List) config.getOrDefault("skipProperties", Collections.emptyList());
// clone nodes and populate copy map
for (Node node : nodes) {
if (node == null || standinMap.containsKey(node)) continue;
// standinNodes will NOT be cloned
NodeRefactorResult result = new NodeRefactorResult(node.getId());
try {
Node copy = withTransactionAndRebind(db, tx, transaction -> {
Node copyTemp = transaction.createNode();
Map properties = node.getAllProperties();
if (skipProperties != null && !skipProperties.isEmpty()) {
for (String skip : skipProperties) properties.remove(skip);
}
copyProperties(properties, copyTemp);
copyLabels(node, copyTemp);
return copyTemp;
});
resultStream.add(result.withOther(copy));
copyMap.put(node, copy);
} catch (Exception e) {
resultStream.add(result.withError(e));
}
}
// clone relationships, will be between cloned nodes and/or standins
for (Relationship rel : rels) {
if (rel == null) continue;
Node oldStart = rel.getStartNode();
Node newStart = standinMap.getOrDefault(oldStart, copyMap.get(oldStart));
Node oldEnd = rel.getEndNode();
Node newEnd = standinMap.getOrDefault(oldEnd, copyMap.get(oldEnd));
if (newStart != null && newEnd != null) {
Relationship newrel = newStart.createRelationshipTo(newEnd, rel.getType());
Map properties = rel.getAllProperties();
if (skipProperties != null && !skipProperties.isEmpty()) {
for (String skip : skipProperties) properties.remove(skip);
}
copyProperties(properties, newrel); }
}
return resultStream.stream();
}
private Map generateStandinMap(List> standins) {
Map standinMap = standins.isEmpty() ? Collections.emptyMap() : new HashMap<>(standins.size());
for (List pairing : standins) {
if (pairing == null) continue;
if (pairing.size() != 2) {
throw new IllegalArgumentException("\'standinNodes\' must be a list of node pairs");
}
Node from = pairing.get(0);
Node to = pairing.get(1);
if (from == null || to == null) {
throw new IllegalArgumentException("\'standinNodes\' must be a list of node pairs");
}
standinMap.put(from, to);
}
return standinMap;
}
/**
* Merges the nodes onto the first node.
* The other nodes are deleted and their relationships moved onto that first node.
*/
@Procedure(name = "apoc.refactor.mergeNodes", mode = Mode.WRITE,eager = true)
@Description("Merges the given list of nodes onto the first node in the list.\n" +
"All relationships are merged onto that node as well.")
public Stream mergeNodes(@Name("nodes") List nodes, @Name(value = "config", defaultValue = "{}") Map config) {
if (nodes == null || nodes.isEmpty()) return Stream.empty();
RefactorConfig conf = new RefactorConfig(config);
Set nodesSet = new LinkedHashSet<>(nodes);
// grab write locks upfront consistently ordered
nodesSet.stream().sorted(Comparator.comparingLong(Node::getId)).forEach(tx::acquireWriteLock);
final Node first = nodes.get(0);
final List existingSelfRelIds = conf.isPreservingExistingSelfRels()
? StreamSupport.stream(first.getRelationships().spliterator(), false).filter(Util::isSelfRel)
.map(Entity::getElementId)
.collect(Collectors.toList())
: Collections.emptyList();
nodesSet.stream().skip(1).forEach(node -> mergeNodes(node, first, conf, existingSelfRelIds));
return Stream.of(new NodeResult(first));
}
/**
* Merges the relationships onto the first relationship and delete them.
* All relationships must have the same starting node and ending node.
*/
@Procedure(name = "apoc.refactor.mergeRelationships", mode = Mode.WRITE)
@Description("Merges the given list of relationships onto the first relationship in the list.")
public Stream mergeRelationships(@Name("rels") List relationships, @Name(value = "config", defaultValue = "{}") Map config) {
if (relationships == null || relationships.isEmpty()) return Stream.empty();
Set relationshipsSet = new LinkedHashSet<>(relationships);
RefactorConfig conf = new RefactorConfig(config);
Iterator it = relationshipsSet.iterator();
Relationship first = it.next();
while (it.hasNext()) {
Relationship other = it.next();
if (first.getStartNode().equals(other.getStartNode()) && first.getEndNode().equals(other.getEndNode()))
mergeRels(other, first, true, conf);
else
throw new RuntimeException("All Relationships must have the same start and end nodes.");
}
return Stream.of(new RelationshipResult(first));
}
/**
* Changes the relationship-type of a relationship by creating a new one between the two nodes
* and deleting the old.
*/
@Procedure(name = "apoc.refactor.setType", mode = Mode.WRITE)
@Description("Changes the type of the given relationship.")
public Stream setType(@Name("rel") Relationship rel, @Name("newType") String newType) {
if (rel == null) return Stream.empty();
RelationshipRefactorResult result = new RelationshipRefactorResult(rel.getId());
try {
Relationship newRel = rel.getStartNode().createRelationshipTo(rel.getEndNode(), RelationshipType.withName(newType));
copyProperties(rel, newRel);
rel.delete();
return Stream.of(result.withOther(newRel));
} catch (Exception e) {
return Stream.of(result.withError(e));
}
}
/**
* Redirects a relationships to a new target node.
*/
@Procedure(name = "apoc.refactor.to", mode = Mode.WRITE,eager = true)
@Description("Redirects the given relationship to the given end node.")
public Stream to(@Name("rel") Relationship rel, @Name("endNode") Node newNode) {
if (rel == null || newNode == null) return Stream.empty();
RelationshipRefactorResult result = new RelationshipRefactorResult(rel.getId());
try {
Relationship newRel = rel.getStartNode().createRelationshipTo(newNode, rel.getType());
copyProperties(rel, newRel);
rel.delete();
return Stream.of(result.withOther(newRel));
} catch (Exception e) {
return Stream.of(result.withError(e));
}
}
@Procedure(name = "apoc.refactor.invert", mode = Mode.WRITE,eager = true)
@Description("Inverts the direction of the given relationship.")
public Stream invert(@Name("rel") Relationship rel) {
if (rel == null) return Stream.empty();
RelationshipRefactorResult result = new RelationshipRefactorResult(rel.getId());
try {
Relationship newRel = rel.getEndNode().createRelationshipTo(rel.getStartNode(), rel.getType());
copyProperties(rel, newRel);
rel.delete();
return Stream.of(result.withOther(newRel));
} catch (Exception e) {
return Stream.of(result.withError(e));
}
}
/**
* Redirects a relationships to a new target node.
*/
@Procedure(name = "apoc.refactor.from", mode = Mode.WRITE, eager = true)
@Description("Redirects the given relationship to the given start node.")
public Stream from(@Name("rel") Relationship rel, @Name("newNode") Node newNode) {
if (rel == null || newNode == null) return Stream.empty();
RelationshipRefactorResult result = new RelationshipRefactorResult(rel.getId());
try {
Relationship newRel = newNode.createRelationshipTo(rel.getEndNode(), rel.getType());
copyProperties(rel, newRel);
rel.delete();
return Stream.of(result.withOther(newRel));
} catch (Exception e) {
return Stream.of(result.withError(e));
}
}
/**
* Make properties boolean
*/
@Procedure(name = "apoc.refactor.normalizeAsBoolean", mode = Mode.WRITE)
@Description("Refactors the given property to a boolean.")
public void normalizeAsBoolean(
@Name("entity") Object entity,
@Name("propertyKey") String propertyKey,
@Name("trueValues") List