Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes 1640: Support options for apoc.export.json for jsonarray/json/jsonlines #1676

Merged
merged 1 commit into from
Nov 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion core/src/main/java/apoc/export/json/ExportJson.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ private Stream<ProgressInfo> exportJson(String fileName, String source, Object d
if (StringUtils.isNotBlank(fileName)) apocConfig.checkWriteAllowed(exportConfig);
final String format = "json";
ProgressReporter reporter = new ProgressReporter(null, null, new ProgressInfo(fileName, source, format));
JsonFormat exporter = new JsonFormat(db);
JsonFormat exporter = new JsonFormat(db, getJsonFormat(config));
ExportFileManager cypherFileManager = FileManagerFactory.createFileManager(fileName, false);
if (exportConfig.streamStatements()) {
return ExportUtils.getProgressInfoStream(db, pools.getDefaultExecutorService() ,terminationGuard, format, exportConfig, reporter, cypherFileManager,
Expand All @@ -99,6 +99,16 @@ private Stream<ProgressInfo> exportJson(String fileName, String source, Object d
}
}

private JsonFormat.Format getJsonFormat(Map<String, Object> config) {
if (config == null) {
return JsonFormat.Format.JSON_LINES;
}
final String jsonFormat = config.getOrDefault("jsonFormat", JsonFormat.Format.JSON_LINES.toString())
.toString()
.toUpperCase();
return JsonFormat.Format.valueOf(jsonFormat);
}

private void dump(Object data, ExportConfig c, ProgressReporter reporter, JsonFormat exporter, ExportFileManager cypherFileManager) {
try {
if (data instanceof SubGraph)
Expand Down
95 changes: 94 additions & 1 deletion core/src/main/java/apoc/export/json/JsonFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,15 @@
import java.util.function.Consumer;

public class JsonFormat implements Format {
enum Format {JSON_LINES, ARRAY_JSON, JSON, JSON_ID_AS_KEYS}
private final GraphDatabaseService db;
private final Format format;

public JsonFormat(GraphDatabaseService db) {
private boolean isExportSubGraph = false;

public JsonFormat(GraphDatabaseService db, Format format) {
this.db = db;
this.format = format;
}

@Override
Expand All @@ -53,26 +58,101 @@ private ProgressInfo dump(Writer writer, Reporter reporter, Consumer<JsonGenerat

@Override
public ProgressInfo dump(SubGraph graph, ExportFileManager writer, Reporter reporter, ExportConfig config) throws Exception {
isExportSubGraph = true;
Consumer<JsonGenerator> consumer = (jsonGenerator) -> {
try {
writeJsonContainerStart(jsonGenerator);
writeJsonNodeContainerStart(jsonGenerator);
writeNodes(graph.getNodes(), reporter, jsonGenerator, config);
writeJsonNodeContainerEnd(jsonGenerator);
writeJsonRelationshipContainerStart(jsonGenerator);
writeRels(graph.getRelationships(), reporter, jsonGenerator, config);
writeJsonRelationshipContainerEnd(jsonGenerator);
writeJsonContainerEnd(jsonGenerator);
} catch (IOException e) {
throw new RuntimeException(e);
}
};
return dump(writer.getPrintWriter("json"), reporter, consumer);
}

private void writeJsonRelationshipContainerEnd(JsonGenerator jsonGenerator) throws IOException {
switch (format) {
case JSON:
jsonGenerator.writeEndArray();
break;
case JSON_ID_AS_KEYS:
jsonGenerator.writeEndObject();
break;
}
}

private void writeJsonRelationshipContainerStart(JsonGenerator jsonGenerator) throws IOException {
switch (format) {
case JSON:
jsonGenerator.writeFieldName("rels");
jsonGenerator.writeStartArray();
break;
case JSON_ID_AS_KEYS:
jsonGenerator.writeFieldName("rels");
jsonGenerator.writeStartObject();
break;
}
}

private void writeJsonNodeContainerEnd(JsonGenerator jsonGenerator) throws IOException {
switch (format) {
case JSON:
jsonGenerator.writeEndArray();
break;
case JSON_ID_AS_KEYS:
jsonGenerator.writeEndObject();
break;
}
}

private void writeJsonNodeContainerStart(JsonGenerator jsonGenerator) throws IOException {
switch (format) {
case JSON:
jsonGenerator.writeStartObject();
jsonGenerator.writeFieldName("nodes");
jsonGenerator.writeStartArray();
break;
case JSON_ID_AS_KEYS:
jsonGenerator.writeStartObject();
jsonGenerator.writeFieldName("nodes");
jsonGenerator.writeStartObject();
break;
}
}

private void writeJsonContainerEnd(JsonGenerator jsonGenerator) throws IOException {
switch (format) {
case ARRAY_JSON:
jsonGenerator.writeEndArray();
break;
}
}

private void writeJsonContainerStart(JsonGenerator jsonGenerator) throws IOException {
switch (format) {
case ARRAY_JSON:
jsonGenerator.writeStartArray();
break;
}
}

public ProgressInfo dump(Result result, ExportFileManager writer, Reporter reporter, ExportConfig config) throws Exception {
Consumer<JsonGenerator> consumer = (jsonGenerator) -> {
try {
writeJsonContainerStart(jsonGenerator);
String[] header = result.columns().toArray(new String[result.columns().size()]);
result.accept((row) -> {
writeJsonResult(reporter, header, jsonGenerator, row, config);
reporter.nextRow();
return true;
});
writeJsonContainerEnd(jsonGenerator);
} catch (IOException e) {
throw new RuntimeException(e);
}
Expand All @@ -96,10 +176,22 @@ private void writeNodes(Iterable<Node> nodes, Reporter reporter, JsonGenerator j

private void writeNode(Reporter reporter, JsonGenerator jsonGenerator, Node node, ExportConfig config) throws IOException {
Map<String, Object> allProperties = node.getAllProperties();
writeJsonIdKeyStart(jsonGenerator, node.getId());
JsonFormatSerializer.DEFAULT.writeNode(jsonGenerator, node, config);
reporter.update(1, 0, allProperties.size());
}

private void writeJsonIdKeyStart(JsonGenerator jsonGenerator, long id) throws IOException {
if (!isExportSubGraph) {
return;
}
switch (format) {
case JSON_ID_AS_KEYS:
writeFieldName(jsonGenerator, String.valueOf(id), true);
break;
}
}

private void writeRels(Iterable<Relationship> rels, Reporter reporter, JsonGenerator jsonGenerator, ExportConfig config) throws IOException {
for (Relationship rel : rels) {
writeRel(reporter, jsonGenerator, rel, config);
Expand All @@ -108,6 +200,7 @@ private void writeRels(Iterable<Relationship> rels, Reporter reporter, JsonGener

private void writeRel(Reporter reporter, JsonGenerator jsonGenerator, Relationship rel, ExportConfig config) throws IOException {
Map<String, Object> allProperties = rel.getAllProperties();
writeJsonIdKeyStart(jsonGenerator, rel.getId());
JsonFormatSerializer.DEFAULT.writeRelationship(jsonGenerator, rel, config);
reporter.update(0, 1, allProperties.size());
}
Expand Down
27 changes: 27 additions & 0 deletions core/src/test/java/apoc/export/json/ExportJsonTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,33 @@ public void testExportAllJson() throws Exception {
assertFileEquals(filename);
}

@Test
public void testExportAllJsonArray() {
String filename = "all_array.json";
TestUtil.testCall(db, "CALL apoc.export.json.all($file, {jsonFormat: 'ARRAY_JSON'})",
map("file", filename),
(r) -> assertResults(filename, r, "database"));
assertFileEquals(filename);
}

@Test
public void testExportAllJsonFields() {
String filename = "all_fields.json";
TestUtil.testCall(db, "CALL apoc.export.json.all($file, {jsonFormat: 'JSON'})",
map("file", filename),
(r) -> assertResults(filename, r, "database"));
assertFileEquals(filename);
}

@Test
public void testExportAllJsonIdAsKeys() {
String filename = "all_id_as_keys.json";
TestUtil.testCall(db, "CALL apoc.export.json.all($file, {jsonFormat: 'JSON_ID_AS_KEYS'})",
map("file", filename),
(r) -> assertResults(filename, r, "database"));
assertFileEquals(filename);
}

@Test
public void testExportAllJsonStream() throws Exception {
String filename = "all.json";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"type":"node","id":"0","labels":["User"],"properties":{"born":"2015-07-04T19:32:24","name":"Adam","place":{"crs":"wgs-84","latitude":33.46789,"longitude":13.1,"height":null},"male":true,"age":42,"kids":["Sam","Anna","Grace"]}},{"type":"node","id":"1","labels":["User"],"properties":{"name":"Jim","age":42}},{"type":"node","id":"2","labels":["User"],"properties":{"age":12}},{"id":"0","type":"relationship","label":"KNOWS","properties":{"bffSince":"P5M1DT12H","since":1993},"start":{"id":"0","labels":["User"]},"end":{"id":"1","labels":["User"]}}]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"nodes":[{"type":"node","id":"0","labels":["User"],"properties":{"born":"2015-07-04T19:32:24","name":"Adam","place":{"crs":"wgs-84","latitude":33.46789,"longitude":13.1,"height":null},"male":true,"age":42,"kids":["Sam","Anna","Grace"]}},{"type":"node","id":"1","labels":["User"],"properties":{"name":"Jim","age":42}},{"type":"node","id":"2","labels":["User"],"properties":{"age":12}}],"rels":[{"id":"0","type":"relationship","label":"KNOWS","properties":{"bffSince":"P5M1DT12H","since":1993},"start":{"id":"0","labels":["User"]},"end":{"id":"1","labels":["User"]}}]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"nodes":{"0":{"type":"node","id":"0","labels":["User"],"properties":{"born":"2015-07-04T19:32:24","name":"Adam","place":{"crs":"wgs-84","latitude":33.46789,"longitude":13.1,"height":null},"male":true,"age":42,"kids":["Sam","Anna","Grace"]}},"1":{"type":"node","id":"1","labels":["User"],"properties":{"name":"Jim","age":42}},"2":{"type":"node","id":"2","labels":["User"],"properties":{"age":12}}},"rels":{"0":{"id":"0","type":"relationship","label":"KNOWS","properties":{"bffSince":"P5M1DT12H","since":1993},"start":{"id":"0","labels":["User"]},"end":{"id":"1","labels":["User"]}}}}
15 changes: 15 additions & 0 deletions docs/asciidoc/modules/ROOT/pages/export/json.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ include::example$generated-documentation/apoc.export.json.csv[]
| name | type | default | description
| writeNodeProperties | boolean | false | if true export properties too.
| stream | boolean | false | stream the json directly to the client into the `data` field
| jsonFormat | enum[JSON_LINES, ARRAY_JSON, JSON, JSON_ID_AS_KEYS] | JSON_LINES | the format of the exported json
|===

.jsonFormat types
[opts=header]
|===
| name | description
| JSON_LINES | the data will be exported as https://jsonlines.org/[JSON lines]
| ARRAY_JSON | the data will be exported as array of json:
`[{"type":"node","id":"2","labels":["User"],"properties":{"age":12}},...]`
| JSON | the data will be exported as json with two (array) fields `nodes` and `rels`:
`{"nodes":[{"type":"node","id":"2","labels":["User"],"properties":{...}},...],"rels":[{"id":"0","type":"relationship","label":"KNOWS","properties":{"since":1993},"start":{"id":"0","labels":["User"]},"end":{"id":"1","labels":["User"]}},...]}`
| JSON_ID_AS_KEYS | the data will be exported as json with two (map) fields `nodes` and `rels`
where the key is the neo4j internal id and the value is the graph entity value:
`{"nodes":{"0":{"type":"node","id":"0","labels":["User"],"properties":{...},"1":{...},"2":{...},"rels":{"0":{"id":"0","type":"relationship","label":"KNOWS","properties":{...},"start":{"id":"0","labels":["User"]},"end":{"id":"1","labels":["User"]}}}}`
|===

[NOTE]
Expand Down