Skip to content

Commit

Permalink
fix #9 implement atag.chains.update (#10)
Browse files Browse the repository at this point in the history
* implement atag.chains.update

* configuration and docs
  • Loading branch information
sarmbruster authored May 8, 2024
1 parent 3ea2ad5 commit e935cf6
Show file tree
Hide file tree
Showing 5 changed files with 404 additions and 8 deletions.
115 changes: 107 additions & 8 deletions src/main/java/atag/chains/ChainsProcedure.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
package atag.chains;

import atag.util.EmptyPath;
import atag.util.ResultTypes;
import org.apache.commons.lang3.NotImplementedException;
import org.neo4j.graphalgo.impl.util.PathImpl;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.*;
import org.neo4j.graphdb.traversal.Evaluation;
import org.neo4j.graphdb.traversal.Traverser;
import org.neo4j.internal.helpers.collection.Iterables;
import org.neo4j.logging.Log;
import org.neo4j.procedure.Context;
import org.neo4j.procedure.Mode;
import org.neo4j.procedure.Name;
import org.neo4j.procedure.Procedure;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

public class ChainsProcedure {
Expand Down Expand Up @@ -125,8 +126,106 @@ private Path chainInternal(String text, String regex, String labelString, String
return build;
}

private static Stream<ResultTypes.PathResult> asPathResult(Path build) {
return Stream.of(new ResultTypes.PathResult(build));
@Procedure(mode = Mode.WRITE)
public Stream<ResultTypes.PathResult> update(
@Name("uuid of text node") String uuidText,
@Name("uuid of chain element before update") String uuidBefore,
@Name("uuid of chain element after update") String uuidAfter,
@Name("chain fragment to replace everything between uuidBefore and uuidAfter") List<Map<String, Object>> replacement,
@Name(value = "configuration", defaultValue = "{}") Map<String, String> config) {
String uuidProperty = "uuid";
Label textLabel = Label.label(config.getOrDefault("textLabel", "Text"));
Label characterLabel = Label.label(config.getOrDefault("elementLabel", "Character"));
RelationshipType relationshipType = RelationshipType.withName(config.getOrDefault("relationshipType", "NEXT_CHARACTER"));
Node textNode = tx.findNode(textLabel, uuidProperty, uuidText);
Node beforeNode = tx.findNode(characterLabel, uuidProperty, uuidBefore);
Node afterNode = tx.findNode(characterLabel, uuidProperty, uuidAfter);

Traverser traverser = tx.traversalDescription()
.expand(PathExpanders.forTypeAndDirection(relationshipType, Direction.OUTGOING))
.evaluator(path -> {
Node last = path.endNode();
if (last.equals(afterNode)) {
return Evaluation.INCLUDE_AND_PRUNE;
} else {
return Evaluation.EXCLUDE_AND_CONTINUE;
}
}).traverse(beforeNode);
Path path = Iterables.single(traverser);

Map<String, Node> existingNodes = new HashMap<>();
for (Node node: path.nodes()) {
String uuidValue = (String) node.getProperty(uuidProperty);
if (!(uuidValue.equals(uuidBefore) || (uuidValue.equals(uuidAfter)))) {
existingNodes.put(uuidValue, node);
}
}

if (beforeNode == null) {
throw new NotImplementedException("cannot yet deal with non existing before node");
}
if (afterNode == null) {
throw new NotImplementedException("cannot yet deal with non existing after node");
}

Node currentNode = beforeNode;
PathImpl.Builder builder = null;
boolean isFirst = true;

for (Map<String, Object> data: replacement) {
String uuid = data.get("uuid").toString();

Node existingNode = existingNodes.remove(uuid);
Relationship currentRelationship = null;
if (existingNode == null) {
Node newNode = tx.createNode(characterLabel);
currentRelationship = currentNode.createRelationshipTo(newNode, relationshipType);
currentNode = newNode;
} else {
Relationship existingRelationship = existingNode.getSingleRelationship(relationshipType, Direction.INCOMING);
Node existingPrevious = existingRelationship.getStartNode();
if (existingPrevious.equals(currentNode)) {
currentRelationship = existingRelationship;
} else {
existingRelationship.delete();
currentRelationship = currentNode.createRelationshipTo(existingNode, relationshipType);
}
currentNode = existingNode;
}
for (Map.Entry<String, Object> e: data.entrySet()) {
currentNode.setProperty(e.getKey(), e.getValue());
}

if (isFirst) {
builder = new PathImpl.Builder(currentNode);
isFirst = false;
} else {
builder = builder.push(currentRelationship);
}
}

// remove leftover nodes
for (Node n: existingNodes.values()) {
n.getRelationships().forEach(Relationship::delete);
n.delete();
}

// ensure last node is connected to afterNode
if (!currentNode.hasRelationship(Direction.OUTGOING, relationshipType)) {
Relationship r = afterNode.getSingleRelationship(relationshipType, Direction.INCOMING);
if (r!=null) {
r.delete();
}
currentNode.createRelationshipTo(afterNode, relationshipType);
}


return asPathResult(builder == null ? new EmptyPath() : builder.build());
}

private static Stream<ResultTypes.PathResult> asPathResult(Path path) {
return Stream.of(new ResultTypes.PathResult(path));
}


}
56 changes: 56 additions & 0 deletions src/main/java/atag/util/EmptyPath.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package atag.util;

import org.neo4j.graphdb.Entity;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.Relationship;
import org.neo4j.internal.helpers.collection.Iterables;

import java.util.Iterator;

public class EmptyPath implements Path {
@Override
public Node startNode() {
return null;
}

@Override
public Node endNode() {
return null;
}

@Override
public Relationship lastRelationship() {
return null;
}

@Override
public Iterable<Relationship> relationships() {
return Iterables.empty();
}

@Override
public Iterable<Relationship> reverseRelationships() {
return Iterables.empty();
}

@Override
public Iterable<Node> nodes() {
return Iterables.empty();
}

@Override
public Iterable<Node> reverseNodes() {
return Iterables.empty();
}

@Override
public int length() {
return 0;
}

@Override
public Iterator<Entity> iterator() {
return Iterables.<Entity>empty().iterator();
}
}
56 changes: 56 additions & 0 deletions src/site/markdown/atag.chains.update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# `atag.chains.tokenChain`

## Description

Partially updates a chain of tokens or characters

## Parameters

| name | type | description | default value |
|--------------|---------------|---------------------------------------------------------------------------------|---------------|
| uuidStart | String | uuid of the node located before the changeset | |
| uuidEnd | String | uuid of the node located after the changeset | |
| replacement | array of maps | a list of nodes described by their properties.<br/>each map must contain a uuid | |
| config | map | configuration settings, see table below for details | `{}` |
| return value | Path | a path representing the changeset, excluding uuidStart and uuidEnd nodes | |

## Configuration Settings

| name | description | default value |
|------------------|----------------------------------------------------------|------------------|
| textLabel | label to be used for entry point nodes, aka `Text` nodes | `Text` |
| elementLabel | label to be used for chain element nodes | `Character` |
| relationshipType | relationship type interconnection the element nodes | `NEXT_CHARACTER` |

## Examples

Assume this sample graph:

```cypher
CREATE (t:Text{uuid:$uuidText})
CREATE (s:Token{uuid:$uuidStart})
CREATE (m1:Token{uuid:$uuidMiddle1, tagName:'a'})
CREATE (m2:Token{uuid:$uuidMiddle2, tagName:'b'})
CREATE (e:Token{uuid:$uuidEnd})
CREATE (t)-[:NEXT_TOKEN]->(s)
CREATE (s)-[:NEXT_TOKEN]->(m1)
CREATE (m1)-[:NEXT_TOKEN]->(m2)
CREATE (m2)-[:NEXT_TOKEN]->(e)
```

```cypher
CALL atag.chains.update($uuidText, $uuidStart, $uuidEnd, [
{
uuid: $uuidMiddle1,
tagName: 'a'
},
{
uuid: $uuidMiddle2,
tagName: 'c'
}
], {
textLabel: "Text",
elementLabel: "Token",
relationshipType: "NEXT_TOKEN"
}) YIELD path RETURN path
```
1 change: 1 addition & 0 deletions src/site/site.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<item name="atag.chains.tokenChain" href="atag.chains.tokenChain.html"/>
<item name="atag.chains.fullChain" href="atag.chains.fullChain.html"/>
<item name="atag.chains.chain" href="atag.chains.chain.html"/>
<item name="atag.chains.update" href="atag.chains.update.html"/>
<item name="atag.text.load" href="atag.text.load.html"/>
<item name="atag.text.import.html" href="atag.text.import.html.html"/>
<item name="atag.text.import.xml" href="atag.text.import.xml.html"/>
Expand Down
Loading

0 comments on commit e935cf6

Please sign in to comment.