Skip to content

Commit

Permalink
feat (core): Graph Commons JSON Converter
Browse files Browse the repository at this point in the history
  • Loading branch information
vorburger committed Dec 14, 2024
1 parent 35cdf0b commit d635fef
Show file tree
Hide file tree
Showing 19 changed files with 344 additions and 16 deletions.
9 changes: 9 additions & 0 deletions docs/use/rosetta/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ $ ./enola rosetta --no-file-loader --in test/picasso.ttl --out "docs/BUILT/picas

![Smaller Graph of Painters](../../BUILT/picasso-small.gv.svg)

### Graph Commons

```bash cd ../.././..
$ ./enola rosetta --in enola:TikaMediaTypes --out /tmp/TikaMediaTypes.graphcommons.json
...
```

produces a JSON which can be imported into [GraphCommons.com](https://graphcommons.com/).

### GEXF

```bash cd ../.././..
Expand Down
7 changes: 7 additions & 0 deletions java/dev/enola/cli/CommandWithIRI.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import dev.enola.core.view.EnolaMessages;
import dev.enola.rdf.io.RdfWriterConverter;
import dev.enola.rdf.proto.ProtoThingRdfConverter;
import dev.enola.thing.gen.graphcommons.GraphCommonsJsonGenerator;
import dev.enola.thing.gen.graphviz.GraphvizGenerator;
import dev.enola.thing.message.ProtoThings;
import dev.enola.thing.metadata.ThingMetadataProvider;
Expand Down Expand Up @@ -108,6 +109,12 @@ protected void write(Message thing) throws IOException {
return;
}

if (Format.GraphCommons.equals(format) && thing instanceof Things protoThings) {
var javaThings = ProtoThings.proto2java(protoThings.getThingsList());
new GraphCommonsJsonGenerator().convertIntoOrThrow(javaThings, resource);
return;
}

// Otherwise
new ProtoIO(typeRegistryWrapper.get()).write(thing, resource);
}
Expand Down
2 changes: 2 additions & 0 deletions java/dev/enola/cli/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import dev.enola.rdf.io.RdfMediaTypeYamlLd;
import dev.enola.rdf.io.RdfMediaTypes;
import dev.enola.thing.gen.gexf.GexfMediaType;
import dev.enola.thing.gen.graphcommons.GraphCommonsMediaType;
import dev.enola.thing.gen.graphviz.GraphvizMediaType;
import dev.enola.thing.io.ThingMediaTypes;

Expand All @@ -46,6 +47,7 @@ class Configuration {
new MarkdownMediaTypes(),
new GraphvizMediaType(),
new GexfMediaType(),
new GraphCommonsMediaType(),
new DatalogMediaTypes(),
new StandardMediaTypes(),
new YamlMediaType(),
Expand Down
4 changes: 4 additions & 0 deletions java/dev/enola/cli/Format.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import dev.enola.common.protobuf.ProtobufMediaTypes;
import dev.enola.rdf.io.RdfMediaTypes;
import dev.enola.thing.gen.graphcommons.GraphCommonsMediaType;
import dev.enola.thing.gen.graphviz.GraphvizMediaType;

public enum Format {
Expand All @@ -30,6 +31,8 @@ public enum Format {

Graphviz,

GraphCommons,

TextProto,

ProtoYAML,
Expand All @@ -43,6 +46,7 @@ MediaType toMediaType() {
case Turtle -> RdfMediaTypes.TURTLE;
case JSONLD -> RdfMediaTypes.JSON_LD;
case Graphviz -> GraphvizMediaType.GV;
case GraphCommons -> GraphCommonsMediaType.GCJSON;

case TextProto -> ProtobufMediaTypes.PROTOBUF_TEXTPROTO_UTF_8;
case ProtoYAML -> ProtobufMediaTypes.PROTOBUF_YAML_UTF_8;
Expand Down
4 changes: 3 additions & 1 deletion java/dev/enola/common/context/Singleton.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ public Singleton<T> set(T value) {

@Override
public T get() {
if (value == null) throw new IllegalStateException();
if (value == null)
throw new IllegalStateException(
getClass() + " was never set(); use SingletonRule in tests");
else return value;
}

Expand Down
7 changes: 6 additions & 1 deletion java/dev/enola/common/io/resource/MediaTypeDetector.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ class MediaTypeDetector {
private static final Set<MediaType> TRY_FIXING =
ImmutableSet.of(
// raw.githubusercontent.com returns "text/plain" e.g. for *.yaml
MediaType.parse("text/plain"));
MediaType.parse("text/plain"),
// URLConnection assumes "application/json" for all *.json but we want "longest
// match" e.g. for ".graphcommons.json"
MediaType.parse("application/json"),
// URLConnection assumes "application/xml" instead of GexfMediaType
MediaType.parse("application/xml"));

private static boolean isSpecial(MediaType mediaType) {
var mediaTypeWithoutParameters = mediaType.withoutParameters();
Expand Down
21 changes: 15 additions & 6 deletions java/dev/enola/common/io/resource/MemoryResourceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import dev.enola.common.context.testlib.SingletonRule;
import dev.enola.common.io.mediatype.MediaTypeProviders;
import dev.enola.common.io.mediatype.YamlMediaType;

import org.junit.Rule;
import org.junit.Test;
Expand All @@ -35,14 +36,15 @@

public class MemoryResourceTest {

public @Rule SingletonRule r = $(MediaTypeProviders.set());
// The new YamlMediaType() is required for non-regression of the funkyYamlURL below
public @Rule SingletonRule r = $(MediaTypeProviders.set(new YamlMediaType()));

private static final byte[] BYTES = new byte[] {1, 2, 3};
private static final String TEXT = "hello, world";

@Test
public void testBinaryMemoryResource() throws IOException {
MemoryResource resource = new MemoryResource(OCTET_STREAM);
var resource = new MemoryResource(OCTET_STREAM);
resource.byteSink().write(BYTES);
assertThat(resource.byteSource().read()).isEqualTo(BYTES);

Expand All @@ -52,16 +54,23 @@ public void testBinaryMemoryResource() throws IOException {

@Test
public void testTextMemoryResource() throws IOException {
MemoryResource resource = new MemoryResource(PLAIN_TEXT_UTF_8);
var resource = new MemoryResource(PLAIN_TEXT_UTF_8);
resource.charSink().write(TEXT);
assertThat(resource.charSource().read()).isEqualTo(TEXT);
}

@Test
public void testMediaTypePrecedence() throws IOException {
// This does not work for PLAIN_TEXT_UTF_8, because that's "special"
public void testMediaTypePrecedenceHTML_GZIP() {
// TODO Fix to also make this work for PLAIN_TEXT_UTF_8, which is "special"
// (It's one of a few MediaTypes which MediaTypeDetector always overrides)
MemoryResource resource = new MemoryResource(URI.create("test.html"), GZIP);
var resource = new MemoryResource(URI.create("test.html"), GZIP);
assertThat(resource.mediaType()).isEqualTo(GZIP);
}

@Test
public void testMediaTypePrecedenceYAML_JSON() {
var funkyYamlURL = "classpath:/picasso.yaml?context=classpath:/picasso-context.jsonld";
var resource = new MemoryResource(URI.create(funkyYamlURL), JSON_UTF_8);
assertThat(resource.mediaType()).isEqualTo(JSON_UTF_8);
}
}
2 changes: 2 additions & 0 deletions java/dev/enola/common/io/resource/ResourceProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
*/
public interface ResourceProvider extends ProviderFromIRI<Resource> {

// TODO Rename all parameters from iri or uri to url - because that's what these are!

// TODO Change all @Nullable Resource to Optional<Resource>... or, better, throw exception for
// unknown schema

Expand Down
2 changes: 2 additions & 0 deletions java/dev/enola/core/rosetta/Rosetta.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import dev.enola.rdf.io.RdfResourceConverter;
import dev.enola.thing.gen.gexf.GexfGenerator;
import dev.enola.thing.gen.gexf.GexfResourceConverter;
import dev.enola.thing.gen.graphcommons.GraphCommonsResourceConverter;
import dev.enola.thing.gen.graphviz.GraphvizGenerator;
import dev.enola.thing.gen.graphviz.GraphvizResourceConverter;
import dev.enola.thing.io.Loader;
Expand Down Expand Up @@ -103,6 +104,7 @@ public Rosetta(ResourceProvider rp, Loader loader) {
new YamlJsonResourceConverter(),
new GraphvizResourceConverter(loader, new GraphvizGenerator(tmp)),
new GexfResourceConverter(loader, new GexfGenerator(tmp)),
new GraphCommonsResourceConverter(loader),
new XmlResourceConverter(rp),
new CharResourceConverter()));
// NOT new IdempotentCopyingResourceNonConverter()
Expand Down
24 changes: 19 additions & 5 deletions java/dev/enola/core/rosetta/RosettaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,15 @@
import dev.enola.common.io.iri.namespace.NamespaceRepositoryEnolaDefaults;
import dev.enola.common.io.mediatype.MediaTypeProviders;
import dev.enola.common.io.mediatype.YamlMediaType;
import dev.enola.common.io.resource.ClasspathResource;
import dev.enola.common.io.resource.MemoryResource;
import dev.enola.common.io.resource.ResourceProvider;
import dev.enola.common.io.resource.StringResource;
import dev.enola.common.io.resource.*;
import dev.enola.common.xml.XmlMediaType;
import dev.enola.common.yamljson.JSON;
import dev.enola.common.yamljson.YAML;
import dev.enola.rdf.io.RdfLoader;
import dev.enola.rdf.io.RdfMediaTypes;
import dev.enola.thing.Thing;
import dev.enola.thing.gen.gexf.GexfMediaType;
import dev.enola.thing.gen.graphcommons.GraphCommonsMediaType;
import dev.enola.thing.gen.graphviz.GraphvizMediaType;
import dev.enola.thing.impl.ImmutableThing;
import dev.enola.thing.io.ThingMediaTypes;
Expand All @@ -57,6 +55,7 @@
import org.junit.Test;
import org.junit.rules.TestRule;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class RosettaTest {
Expand All @@ -72,6 +71,7 @@ public class RosettaTest {
MediaTypeProviders.set(
new RdfMediaTypes(),
new GraphvizMediaType(),
new GraphCommonsMediaType(),
new GexfMediaType(),
new YamlMediaType(),
new XmlMediaType()));
Expand Down Expand Up @@ -186,7 +186,7 @@ public void testXMLToTurtle() throws Exception {
}

@Test
public void testGraphvizAndGexf() throws Exception {
public void testGexfAndGraphvizAndGraphCommons() throws Exception {
var in = rp.get("classpath:/graph.ttl");
try (var ctx = TLC.open()) {
// This tests that StackedThingProvider in GraphvizGenerator works;
Expand All @@ -200,18 +200,32 @@ public void testGraphvizAndGexf() throws Exception {
var namespaceConverter = new NamespaceConverterWithRepository(namespaceRepo);
ctx.push(NamespaceConverter.class, namespaceConverter);

// TODO GexfMediaTypeTest: checkRosettaConvert(in, "classpath:/graph.expected.gexf");
var gexf = new MemoryResource(GexfMediaType.GEXF);
rosetta.convertInto(in, gexf);
assertThat(gexf)
.hasCharsEqualTo(rp.get("classpath:/graph.expected.gexf?charset=UTF-8"));

// TODO checkRosettaConvert(in, "classpath:/graph.expected-full.gv?" +
// OUT_URI_QUERY_PARAMETER_FULL + "=true");
var gv = new MemoryResource(GV, OUT_URI_QUERY_PARAMETER_FULL + "=true");
rosetta.convertInto(in, gv);
assertThat(gv).hasCharsEqualTo(rp.get("classpath:/graph.expected-full.gv"));

// TODO checkRosettaConvert(in, "classpath:/graph.expected-full.gv?"
// + OUT_URI_QUERY_PARAMETER_FULL + "=false");
gv = new MemoryResource(GV, OUT_URI_QUERY_PARAMETER_FULL + "=false");
rosetta.convertInto(in, gv);
assertThat(gv).hasCharsEqualTo(rp.get("classpath:/graph.expected-short.gv"));

checkRosettaConvert(in, "classpath:/graph.expected.graphcommons.json");
}
}

void checkRosettaConvert(ReadableResource in, String expectedURL) throws IOException {
var expected = rp.get(expectedURL);
var actual = new MemoryResource(expected.mediaType());
rosetta.convertInto(in, actual);
assertThat(actual).hasCharsEqualTo(expected);
}
}
2 changes: 2 additions & 0 deletions java/dev/enola/thing/gen/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ java_library(
"@maven//:com_google_auto_service_auto_service_annotations",
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_errorprone_error_prone_type_annotations",
"@maven//:com_google_code_gson_gson",
"@maven//:com_google_guava_guava",
"@maven//:org_jspecify_jspecify",
"@maven//:org_slf4j_slf4j_api",
Expand All @@ -70,6 +71,7 @@ junit_tests(
"//java/dev/enola/common/context/testlib",
"//java/dev/enola/common/io",
"//java/dev/enola/common/io/testlib",
"//java/dev/enola/common/xml",
"//java/dev/enola/datatype",
"//java/dev/enola/model",
"//java/dev/enola/rdf/io",
Expand Down
47 changes: 47 additions & 0 deletions java/dev/enola/thing/gen/gexf/GexfMediaTypeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2024 The Enola <https://enola.dev> Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.enola.thing.gen.gexf;

import static com.google.common.truth.Truth.assertThat;

import static dev.enola.common.context.testlib.SingletonRule.$;

import dev.enola.common.context.testlib.SingletonRule;
import dev.enola.common.io.mediatype.MediaTypeProviders;
import dev.enola.common.io.resource.ClasspathResource;
import dev.enola.common.xml.XmlMediaType;

import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;

public class GexfMediaTypeTest {

@Rule
public SingletonRule r = $(MediaTypeProviders.set(new GexfMediaType(), new XmlMediaType()));

@Test
@Ignore // TODO Figure out why this doesn't work (but only IFF XmlMediaType is present)
public void gexfMediaType() {
var r = new ClasspathResource.Provider().get("classpath:/graph.expected.gexf");
assertThat(r.mediaType()).isEqualTo(GexfMediaType.GEXF);
}

@Test
public void TODO_empty() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2024 The Enola <https://enola.dev> Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.enola.thing.gen.graphcommons;

import static com.google.gson.FormattingStyle.PRETTY;
import static com.google.gson.Strictness.STRICT;

import com.google.common.io.CharStreams;
import com.google.gson.stream.JsonWriter;

import dev.enola.common.context.TLC;
import dev.enola.common.convert.ConversionException;
import dev.enola.thing.Thing;
import dev.enola.thing.gen.ThingsIntoAppendableConverter;
import dev.enola.thing.repo.StackedThingProvider;
import dev.enola.thing.repo.ThingProvider;

import java.io.IOException;

/** Generator of JSON Format used by <a href="https://graphcommons.com/>Graph Commons</a>. */
public class GraphCommonsJsonGenerator implements ThingsIntoAppendableConverter {

@Override
public boolean convertInto(Iterable<Thing> from, Appendable out)
throws ConversionException, IOException {
var writer = CharStreams.asWriter(out);
var jsonWriter = new JsonWriter(writer);
jsonWriter.setStrictness(STRICT);
jsonWriter.setFormattingStyle(PRETTY); // TODO FormattingStyle.COMPACT, if !pretty
jsonWriter.setSerializeNulls(false);
jsonWriter.setHtmlSafe(true); // TODO ?
jsonWriter.beginObject();
try (var ctx = TLC.open()) {
ctx.push(ThingProvider.class, new StackedThingProvider(from));
jsonWriter.name("nodes").beginArray();
for (Thing thing : from) printThingNode(thing, jsonWriter);
jsonWriter.endArray().name("edges").beginArray();
for (Thing thing : from) printThingEdges(thing, jsonWriter);
jsonWriter.endArray().name("nodeTypes").beginArray();
// TODO printThingNodeTypes()
jsonWriter.endArray().name("edgeTypes").beginArray();
// TODO printThingEdgeTypes()
jsonWriter.endArray().name("name").value("Enola.dev");
}
jsonWriter.endObject();
jsonWriter.flush();
writer.close();
return true;
}

private void printThingNode(Thing thing, JsonWriter jsonWriter) throws IOException {
jsonWriter.beginObject();
jsonWriter.name("id").value(thing.iri());
jsonWriter.endObject();
}

private void printThingEdges(Thing thing, JsonWriter jsonWriter) {}
}
Loading

0 comments on commit d635fef

Please sign in to comment.