Skip to content

Commit

Permalink
Update graphml procedure to support tinkerpop format (#2856) (#2857)
Browse files Browse the repository at this point in the history
Co-authored-by: Nacho Cordón <[email protected]>
  • Loading branch information
neo4j-oss-build and ncordon authored May 10, 2022
1 parent 277d689 commit 7efdfba
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 7 deletions.
26 changes: 21 additions & 5 deletions core/src/main/java/apoc/export/graphml/XmlGraphMLWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ private void writeKey(XMLStreamWriter writer, SubGraph ops, ExportConfig config)
Map<String, Class> keyTypes = new HashMap<>();
for (Node node : ops.getNodes()) {
if (node.getLabels().iterator().hasNext()) {
keyTypes.put("labels", String.class);
if (config.getFormat() == ExportFormat.TINKERPOP) {
keyTypes.put("labelV", String.class);
} else {
keyTypes.put("labels", String.class);
}
}
updateKeyTypes(keyTypes, node);
}
Expand All @@ -55,7 +59,11 @@ private void writeKey(XMLStreamWriter writer, SubGraph ops, ExportConfig config)
writeKey(writer, keyTypes, "node", useTypes);
keyTypes.clear();
for (Relationship rel : ops.getRelationships()) {
keyTypes.put("label", String.class);
if (config.getFormat() == ExportFormat.TINKERPOP) {
keyTypes.put("labelE", String.class);
} else {
keyTypes.put("label", String.class);
}
updateKeyTypes(keyTypes, rel);
}
if (format == ExportFormat.GEPHI) {
Expand Down Expand Up @@ -88,7 +96,9 @@ private void writeKey(XMLStreamWriter writer, Map<String, Class> keyTypes, Strin
private int writeNode(XMLStreamWriter writer, Node node, ExportConfig config) throws XMLStreamException {
writer.writeStartElement("node");
writer.writeAttribute("id", id(node));
writeLabels(writer, node);
if (config.getFormat() != ExportFormat.TINKERPOP) {
writeLabels(writer, node);
}
writeLabelsAsData(writer, node, config);
int props = writeProps(writer, node);
endElement(writer);
Expand All @@ -111,6 +121,8 @@ private void writeLabelsAsData(XMLStreamWriter writer, Node node, ExportConfig c
if (config.getFormat() == ExportFormat.GEPHI) {
writeData(writer, "TYPE", delimiter + FormatUtils.joinLabels(node, delimiter));
writeData(writer, "label", getLabelsStringGephi(config, node));
} else if (config.getFormat() == ExportFormat.TINKERPOP){
writeData(writer, "labelV", FormatUtils.joinLabels(node, delimiter));
} else {
writeData(writer, "labels", labelsString);
}
Expand All @@ -121,8 +133,12 @@ private int writeRelationship(XMLStreamWriter writer, Relationship rel, ExportCo
writer.writeAttribute("id", id(rel));
writer.writeAttribute("source", id(rel.getStartNode()));
writer.writeAttribute("target", id(rel.getEndNode()));
writer.writeAttribute("label", rel.getType().name());
writeData(writer, "label", rel.getType().name());
if (config.getFormat() == ExportFormat.TINKERPOP) {
writeData(writer, "labelE", rel.getType().name());
} else {
writer.writeAttribute("label", rel.getType().name());
writeData(writer, "label", rel.getType().name());
}
if (config.getFormat() == ExportFormat.GEPHI) {
writeData(writer, "TYPE", rel.getType().name());
}
Expand Down
4 changes: 3 additions & 1 deletion core/src/main/java/apoc/export/util/ExportFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ public enum ExportFormat {

PLAIN_FORMAT("plain", "", "", "", ""),

GEPHI("gephi", "", "", "", "");
GEPHI("gephi", "", "", "", ""),

TINKERPOP("tinkerpop", "", "", "", "");


private final String format;
Expand Down
87 changes: 87 additions & 0 deletions core/src/test/java/apoc/export/graphml/ExportGraphMLTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ public class ExportGraphMLTest {
"<key id=\"place\" for=\"node\" attr.name=\"place\"/>%n" +
"<key id=\"age\" for=\"node\" attr.name=\"age\"/>%n" +
"<key id=\"label\" for=\"edge\" attr.name=\"label\"/>%n";
public static final String KEY_TYPES_FALSE_TINKER = "<key id=\"born\" for=\"node\" attr.name=\"born\"/>%n" +
"<key id=\"name\" for=\"node\" attr.name=\"name\"/>%n" +
"<key id=\"labelV\" for=\"node\" attr.name=\"labelV\"/>%n"+
"<key id=\"place\" for=\"node\" attr.name=\"place\"/>%n" +
"<key id=\"age\" for=\"node\" attr.name=\"age\"/>%n" +
"<key id=\"values\" for=\"node\" attr.name=\"values\"/>" +
"<key id=\"labelE\" for=\"edge\" attr.name=\"labelE\"/>%n";
public static final String KEY_TYPES_DATA = "<key id=\"name\" for=\"node\" attr.name=\"name\"/>\n" +
"<key id=\"labels\" for=\"node\" attr.name=\"labels\"/>";
public static final String KEY_TYPES = "<key id=\"born\" for=\"node\" attr.name=\"born\" attr.type=\"string\"/>%n" +
Expand All @@ -85,6 +92,12 @@ public class ExportGraphMLTest {
"<key id=\"age\" for=\"node\" attr.name=\"age\" attr.type=\"long\"/>%n" +
"<key id=\"label\" for=\"edge\" attr.name=\"label\" attr.type=\"string\"/>%n" +
"<key id=\"TYPE\" for=\"edge\" attr.name=\"TYPE\" attr.type=\"string\"/>%n";
public static final String KEY_TYPES_PATH_TINKERPOP = "<key id=\"born\" for=\"node\" attr.name=\"born\" attr.type=\"string\"/>%n" +
"<key id=\"name\" for=\"node\" attr.name=\"name\" attr.type=\"string\"/>%n" +
"<key id=\"labelV\" for=\"node\" attr.name=\"labelV\" attr.type=\"string\"/>%n"+
"<key id=\"place\" for=\"node\" attr.name=\"place\" attr.type=\"string\"/>%n" +
"<key id=\"age\" for=\"node\" attr.name=\"age\" attr.type=\"long\"/>%n" +
"<key id=\"labelE\" for=\"edge\" attr.name=\"labelE\" attr.type=\"string\"/>%n";
public static final String KEY_TYPES_CAMEL_CASE = "<key id=\"firstName\" for=\"node\" attr.name=\"firstName\" attr.type=\"string\"/>%n" +
"<key id=\"ageNow\" for=\"node\" attr.name=\"ageNow\" attr.type=\"long\"/>%n" +
"<key id=\"name\" for=\"node\" attr.name=\"name\" attr.type=\"string\"/>%n" +
Expand All @@ -98,6 +111,10 @@ public class ExportGraphMLTest {
"<node id=\"n1\" labels=\":Bar\"><data key=\"labels\">:Bar</data><data key=\"age\">42</data><data key=\"name\">bar</data><data key=\"place\">{\"crs\":\"wgs-84\",\"latitude\":12.78,\"longitude\":56.7,\"height\":null}</data></node>%n" +
"<node id=\"n2\" labels=\":Bar\"><data key=\"labels\">:Bar</data><data key=\"age\">12</data><data key=\"values\">[1,2,3]</data></node>%n" +
"<edge id=\"e0\" source=\"n0\" target=\"n1\" label=\"KNOWS\"><data key=\"label\">KNOWS</data></edge>%n";
public static final String DATA_TINKER = "<node id=\"n0\"><data key=\"labelV\">Foo:Foo0:Foo2</data><data key=\"place\">{\"crs\":\"wgs-84-3d\",\"latitude\":12.78,\"longitude\":56.7,\"height\":100.0}</data><data key=\"name\">foo</data><data key=\"born\">2018-10-10</data></node>%n" +
"<node id=\"n1\"><data key=\"labelV\">Bar</data><data key=\"age\">42</data><data key=\"name\">bar</data><data key=\"place\">{\"crs\":\"wgs-84\",\"latitude\":12.78,\"longitude\":56.7,\"height\":null}</data></node>%n" +
"<node id=\"n2\"><data key=\"labelV\">Bar</data><data key=\"age\">12</data><data key=\"values\">[1,2,3]</data></node>%n" +
"<edge id=\"e0\" source=\"n0\" target=\"n1\"><data key=\"labelE\">KNOWS</data></edge>%n";
public static final String DATA_CAMEL_CASE =
"<node id=\"n0\" labels=\":Foo:Foo0:Foo2\"><data key=\"TYPE\">:Foo:Foo0:Foo2</data><data key=\"label\">foo</data><data key=\"firstName\">foo</data></node>%n" +
"<node id=\"n1\" labels=\":Bar\"><data key=\"TYPE\">:Bar</data><data key=\"label\">bar</data><data key=\"name\">bar</data><data key=\"ageNow\">42</data></node>%n" +
Expand Down Expand Up @@ -129,6 +146,10 @@ public class ExportGraphMLTest {
public static final String DATA_PATH_CAPTION = "<node id=\"n0\" labels=\":Foo:Foo0:Foo2\"><data key=\"TYPE\">:Foo:Foo0:Foo2</data><data key=\"label\">foo</data><data key=\"place\">{\"crs\":\"wgs-84-3d\",\"latitude\":12.78,\"longitude\":56.7,\"height\":100.0}</data><data key=\"name\">foo</data><data key=\"born\">2018-10-10</data></node>%n" +
"<node id=\"n1\" labels=\":Bar\"><data key=\"TYPE\">:Bar</data><data key=\"label\">bar</data><data key=\"age\">42</data><data key=\"name\">bar</data><data key=\"place\">{\"crs\":\"wgs-84\",\"latitude\":12.78,\"longitude\":56.7,\"height\":null}</data></node>%n" +
"<edge id=\"e0\" source=\"n0\" target=\"n1\" label=\"KNOWS\"><data key=\"label\">KNOWS</data><data key=\"TYPE\">KNOWS</data></edge>%n";

public static final String DATA_PATH_CAPTION_TINKER = "<node id=\"n0\"><data key=\"labelV\">Foo:Foo0:Foo2</data><data key=\"place\">{\"crs\":\"wgs-84-3d\",\"latitude\":12.78,\"longitude\":56.7,\"height\":100.0}</data><data key=\"name\">foo</data><data key=\"born\">2018-10-10</data></node>%n" +
"<node id=\"n1\"><data key=\"labelV\">Bar</data><data key=\"age\">42</data><data key=\"name\">bar</data><data key=\"place\">{\"crs\":\"wgs-84\",\"latitude\":12.78,\"longitude\":56.7,\"height\":null}</data></node>%n" +
"<edge id=\"e0\" source=\"n0\" target=\"n1\"><data key=\"labelE\">KNOWS</data></edge>%n";

public static final String DATA_PATH_CAPTION_DEFAULT = "<node id=\"n0\" labels=\":Foo:Foo0:Foo2\"><data key=\"TYPE\">:Foo:Foo0:Foo2</data><data key=\"label\">foo</data><data key=\"place\">{\"crs\":\"wgs-84-3d\",\"latitude\":12.78,\"longitude\":56.7,\"height\":100.0}</data><data key=\"name\">foo</data><data key=\"born\">2018-10-10</data></node>%n" +
"<node id=\"n1\" labels=\":Bar\"><data key=\"TYPE\">:Bar</data><data key=\"label\">bar</data><data key=\"age\">42</data><data key=\"name\">bar</data><data key=\"place\">{\"crs\":\"wgs-84\",\"latitude\":12.78,\"longitude\":56.7,\"height\":null}</data></node>%n" +
Expand All @@ -139,10 +160,12 @@ public class ExportGraphMLTest {

private static final String EXPECTED_TYPES_PATH = String.format(HEADER + KEY_TYPES_PATH + GRAPH + DATA_PATH + FOOTER);
private static final String EXPECTED_TYPES_PATH_CAPTION = String.format(HEADER + KEY_TYPES_PATH + GRAPH + DATA_PATH_CAPTION + FOOTER);
private static final String EXPECTED_TYPES_PATH_CAPTION_TINKER = String.format(HEADER + KEY_TYPES_PATH_TINKERPOP + GRAPH + DATA_PATH_CAPTION_TINKER + FOOTER);
private static final String EXPECTED_TYPES_PATH_WRONG_CAPTION = String.format(HEADER + KEY_TYPES_PATH + GRAPH + DATA_PATH_CAPTION_DEFAULT + FOOTER);
private static final String EXPECTED_TYPES = String.format(HEADER + KEY_TYPES + GRAPH + DATA + FOOTER);
private static final String EXPECTED_TYPES_WITHOUT_CHAR_DATA_KEYS = String.format(HEADER + KEY_TYPES + GRAPH + DATA_WITHOUT_CHAR_DATA_KEYS + FOOTER);
private static final String EXPECTED_FALSE = String.format(HEADER + KEY_TYPES_FALSE + GRAPH + DATA + FOOTER);
private static final String EXPECTED_TINKER = String.format(HEADER + KEY_TYPES_FALSE_TINKER + GRAPH + DATA_TINKER + FOOTER);
private static final String EXPECTED_DATA = String.format(HEADER + KEY_TYPES_DATA + GRAPH + DATA_DATA + FOOTER);
private static final String EXPECTED_READ_NODE_EDGE = String.format(HEADER + GRAPH + DATA_NODE_EDGE + FOOTER);
private static final String EXPECTED_TYPES_PATH_CAMEL_CASE = String.format(HEADER + KEY_TYPES_CAMEL_CASE + GRAPH + DATA_CAMEL_CASE + FOOTER);
Expand Down Expand Up @@ -531,6 +554,70 @@ public void testExportGraphGraphMLQueryGephiWithArrayCaptionWrong() throws Excep
assertXMLEquals(output, EXPECTED_TYPES_PATH_WRONG_CAPTION);
}

@Test
public void testExportAllGraphMLTinker() throws Exception {
File output = new File(directory, "all.graphml");
TestUtil.testCall(db, "CALL apoc.export.graphml.all($file, {format:'tinkerpop'})", map("file", output.getAbsolutePath()),
(r) -> assertResults(output, r, "database"));
assertXMLEquals(output, EXPECTED_TINKER);
}

public void testExportGraphGraphMLQueryTinkerPop() throws Exception {
File output = new File(directory, "query.graphml");
TestUtil.testCall(db, "call apoc.export.graphml.query('MATCH p=()-[r]->() RETURN p limit 1000',$file,{useTypes:true, format: 'tinkerpop'}) ", map("file", output.getAbsolutePath()),
(r) -> {
assertEquals(2L, r.get("nodes"));
assertEquals(1L, r.get("relationships"));
assertEquals(6L, r.get("properties"));
assertEquals(output.getAbsolutePath(), r.get("file"));
if (r.get("source").toString().contains(":"))
assertEquals("statement" + ": nodes(2), rels(1)", r.get("source"));
else
assertEquals("file", r.get("source"));
assertEquals("graphml", r.get("format"));
assertTrue("Should get time greater than 0",((long) r.get("time")) > 0);
});
assertXMLEquals(output, EXPECTED_TYPES_PATH_CAPTION_TINKER);
}

@Test
public void testExportGraphGraphMLQueryTinkerPopWithArrayCaption() throws Exception {
File output = new File(directory, "query.graphml");
TestUtil.testCall(db, "call apoc.export.graphml.query('MATCH p=()-[r]->() RETURN p limit 1000',$file,{useTypes:true, format: 'tinkerpop', caption: ['bar','name','foo']}) ", map("file", output.getAbsolutePath()),
(r) -> {
assertEquals(2L, r.get("nodes"));
assertEquals(1L, r.get("relationships"));
assertEquals(6L, r.get("properties"));
assertEquals(output.getAbsolutePath(), r.get("file"));
if (r.get("source").toString().contains(":"))
assertEquals("statement" + ": nodes(2), rels(1)", r.get("source"));
else
assertEquals("file", r.get("source"));
assertEquals("graphml", r.get("format"));
assertTrue("Should get time greater than 0",((long) r.get("time")) > 0);
});
assertXMLEquals(output, EXPECTED_TYPES_PATH_CAPTION_TINKER);
}

@Test
public void testExportGraphGraphMLQueryTinkerPopWithArrayCaptionWrong() throws Exception {
File output = new File(directory, "query.graphml");
TestUtil.testCall(db, "call apoc.export.graphml.query('MATCH p=()-[r]->() RETURN p limit 1000',$file,{useTypes:true, format: 'tinkerpop', caption: ['c','d','e']}) ", map("file", output.getAbsolutePath()),
(r) -> {
assertEquals(2L, r.get("nodes"));
assertEquals(1L, r.get("relationships"));
assertEquals(6L, r.get("properties"));
assertEquals(output.getAbsolutePath(), r.get("file"));
if (r.get("source").toString().contains(":"))
assertEquals("statement" + ": nodes(2), rels(1)", r.get("source"));
else
assertEquals("file", r.get("source"));
assertEquals("graphml", r.get("format"));
assertTrue("Should get time greater than 0",((long) r.get("time")) > 0);
});
assertXMLEquals(output, EXPECTED_TYPES_PATH_CAPTION_TINKER);
}

@Test(expected = QueryExecutionException.class)
public void testExportGraphGraphMLQueryGephiWithStringCaption() throws Exception {
File output = new File(directory, "query.graphml");
Expand Down
2 changes: 1 addition & 1 deletion docs/asciidoc/modules/ROOT/pages/export/graphml.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ The procedures support the following config parameters:
[options=header]
|===
| param | default | description
| format | gephi | In export to Graphml script define the export format. Possible value is: "gephi"
| format | gephi | In export to Graphml script define the export format. Possible value are: "gephi" and "tinkerpop"
| caption | | It's an array of string (i.e. ['name','title']) that define an ordered set of properties eligible as value for the `Label` value, if no match is found the there is a fallback to the node label, if the node label is missing the then the ID is used
| useTypes | false | Write the attribute type information to the graphml output
| batchSize | 20000 | define the batch size
Expand Down

0 comments on commit 7efdfba

Please sign in to comment.