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 240395f
Show file tree
Hide file tree
Showing 20 changed files with 343 additions and 18 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
3 changes: 1 addition & 2 deletions java/dev/enola/common/io/mediatype/MediaTypeProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ default MediaType detect(String uri, ByteSource byteSource, MediaType original)
// TODO It's kinda wrong that this uses MediaTypeProviders.SINGLETON; it would be clearer if
// it only ever used itself. But that requires moving normalize() from MediaTypeProviders
// to... where? Another ABC?! Urgh.
var normalized = MediaTypeProviders.SINGLETON.get().normalize(original);
if (!normalized.equals(original)) return normalized;
original = MediaTypeProviders.SINGLETON.get().normalize(original);

// NB: This looks inefficient, and you could be tempted to do this "the other way around"
// (instead of checking EACH map entry with uri.endsWith(), the URI extension should be
Expand Down
8 changes: 7 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 JSON for all *.json but we want "longest
// match" e.g. for ".graphcommons.json"
MediaType.parse("application/json"),
// URLConnection assumes XML for .gexf instead of GexfMediaType (with +xml)
MediaType.parse("application/xml"));

private static boolean isSpecial(MediaType mediaType) {
var mediaTypeWithoutParameters = mediaType.withoutParameters();
Expand Down Expand Up @@ -167,6 +172,7 @@ private MediaType detect(@Nullable String contentType, @Nullable String contentE
MediaType mediaType = null;
if (contentType != null) {
mediaType = MediaTypes.parse(contentType);
// TODO Use isSpecial() here? But it also includes DEFAULT...
if (TRY_FIXING.contains(mediaType.withoutParameters())
|| IGNORE.contains(mediaType.withoutParameters())) {
mediaType = null;
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
44 changes: 44 additions & 0 deletions java/dev/enola/thing/gen/gexf/GexfMediaTypeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.Rule;
import org.junit.Test;

public class GexfMediaTypeTest {

// XmlMediaType is required because this tests non-regression for a past bug

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

@Test
public void gexfMediaType() {
var r = new ClasspathResource.Provider().get("classpath:/graph.expected.gexf");
assertThat(r.mediaType()).isEqualTo(GexfMediaType.GEXF);
}
}
Loading

0 comments on commit 240395f

Please sign in to comment.