From 9b98efc416f2a4a79687fe863813c348cfd4a495 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Tue, 8 Oct 2024 21:26:11 +0200 Subject: [PATCH] feat (core): Graph Commons JSON Converter --- docs/use/docgen/index.md | 2 + docs/use/fetch/index.md | 21 +- docs/use/gen/index.md | 59 +++++ docs/use/help/index.md | 9 + docs/use/rosetta/index.md | 15 ++ java/dev/enola/BUILD | 1 + java/dev/enola/cli/CommandWithIRI.java | 8 + java/dev/enola/cli/Configuration.java | 2 + java/dev/enola/cli/Format.java | 4 + java/dev/enola/common/context/Singleton.java | 4 +- .../io/mediatype/MediaTypeProvider.java | 3 +- .../io/resource/FileDescriptorResource.java | 2 + .../common/io/resource/MediaTypeDetector.java | 10 +- .../io/resource/MemoryResourceTest.java | 21 +- .../common/io/resource/ResourceProvider.java | 2 + .../common/io/testlib/ResourceSubject.java | 2 +- java/dev/enola/core/rosetta/Rosetta.java | 4 + java/dev/enola/core/rosetta/RosettaTest.java | 24 +- .../enola/format/xml/XMLToThingHandler.java | 2 +- java/dev/enola/model/BUILD | 2 + java/dev/enola/model/enola/UnknownClass.java | 32 +++ java/dev/enola/thing/KIRI.java | 12 +- java/dev/enola/thing/PredicatesObjects.java | 7 +- java/dev/enola/thing/Thing.java | 4 +- java/dev/enola/thing/gen/BUILD | 2 + java/dev/enola/thing/gen/Colorizer.java | 42 ++++ java/dev/enola/thing/gen/Orphanage.java | 42 ++++ .../thing/gen/gexf/GexfMediaTypeTest.java | 44 ++++ .../GraphCommonsJsonGenerator.java | 226 ++++++++++++++++++ .../graphcommons/GraphCommonsMediaType.java | 38 +++ .../GraphCommonsMediaTypeTest.java | 39 +++ .../GraphCommonsResourceConverter.java | 47 ++++ .../thing/gen/graphviz/GraphvizGenerator.java | 20 +- java/dev/enola/thing/io/ThingMediaTypes.java | 2 + mkdocs.yaml | 1 + models/enola.dev/enola.ttl | 4 + test/graph.expected.graphcommons.json | 167 +++++++++++++ 37 files changed, 890 insertions(+), 36 deletions(-) create mode 100644 docs/use/gen/index.md create mode 100644 java/dev/enola/model/enola/UnknownClass.java create mode 100644 java/dev/enola/thing/gen/Colorizer.java create mode 100644 java/dev/enola/thing/gen/Orphanage.java create mode 100644 java/dev/enola/thing/gen/gexf/GexfMediaTypeTest.java create mode 100644 java/dev/enola/thing/gen/graphcommons/GraphCommonsJsonGenerator.java create mode 100644 java/dev/enola/thing/gen/graphcommons/GraphCommonsMediaType.java create mode 100644 java/dev/enola/thing/gen/graphcommons/GraphCommonsMediaTypeTest.java create mode 100644 java/dev/enola/thing/gen/graphcommons/GraphCommonsResourceConverter.java create mode 100644 test/graph.expected.graphcommons.json diff --git a/docs/use/docgen/index.md b/docs/use/docgen/index.md index 877e549fe..934f6d679 100644 --- a/docs/use/docgen/index.md +++ b/docs/use/docgen/index.md @@ -22,3 +22,5 @@ We can generate nice Markdown documentation as seen in [the tutorial](../../mode or for [our example Library model](../library/index.md), including a Graph in either Mermaid.JS or [Graphwiz](../rosetta/index.md#graphviz) or [GEXF](../rosetta/index.md#gexf) format, and [a Timeline](../../models/example.org/timeline.md). + +`docgen` (old) will later be integrated into (new) [generic `gen markdown`](../gen/index.md#markdown). diff --git a/docs/use/fetch/index.md b/docs/use/fetch/index.md index 86beb85c6..6cfe36434 100644 --- a/docs/use/fetch/index.md +++ b/docs/use/fetch/index.md @@ -73,9 +73,28 @@ $ ./enola fetch "data:application/json;charset=UTF-8,%7B%22key%22%3A+%22value%22 ... ``` +### File Descriptor + +`fd:` is a (non-standard) URL scheme in Enola for reading from or writing to [file descriptors](https://en.wikipedia.org/wiki/File_descriptor), for: + +* `fd:0` [STDIN](https://en.wikipedia.org/wiki/Stdin) +* `fd:1` [STDOUT](https://en.wikipedia.org/wiki/Stdout) +* `fd:2` [STDERR](https://en.wikipedia.org/wiki/Stderr) + +The _Media Type_ of this special resource will be `application/octet-stream` (**not** `application/binary`), +unless there is [a `?mediaType=` parameter](#media-type). + +The _Charset_ will be the default of the JVM, +unless there is (checked first) [a `?charset=` parameter](#charset) (e.g. `fd:0?charset=UTF-16BE`), +or the `?mediaType=` parameter includes a charset (e.g. `fd:1?mediaType=application/yaml;charset=utf-16be`). + + + + + ### Empty -`empty:` is a (non-standard) URL scheme in Enola for "no content" (as an alternative to `data:,`): +`empty:` is another (non-standard) URL scheme in Enola for "no content" (as an alternative to `data:,`): ```bash cd ../.././.. $ ./enola fetch empty:/ diff --git a/docs/use/gen/index.md b/docs/use/gen/index.md new file mode 100644 index 000000000..cd03cbf0a --- /dev/null +++ b/docs/use/gen/index.md @@ -0,0 +1,59 @@ + + +# Generate + +Generates [various outputs](../help/index.md#gen) from (loaded) _Things._ + +Many of these formats "mirror" the [respective support in Rosetta](../rosetta/index.md). +The difference is that Rosetta transforms one resource into another one, +whereas this generates (possibly several) output/s from possibly several input/s. + +## Sigma (TODO) + +We have plans to support https://www.sigmajs.org. + +## Graphviz + +Based on [Rosetta's Graphviz support](../rosetta/index.md#graphviz): + +```bash cd ../.././.. +$ ./enola gen graphviz --no-file-loader --load=enola:TikaMediaTypes --output /tmp/ +... +``` + +Produces a (rather huge...) `graphviz.gv` which can then be rendered to an (ugly!) SVG e.g. using: + + dot -Ksfdp -Tsvg -O /tmp/graphviz.gv + +## GEXF + +See [Rosetta's GEXF support](../rosetta/index.md#gexf). + + + +## Markdown (TODO) + +[The (old) `docgen`](../docgen/index.md) will later be integrated into a (new) generic `gen markdown`. diff --git a/docs/use/help/index.md b/docs/use/help/index.md index 2ded48eec..2f636e180 100644 --- a/docs/use/help/index.md +++ b/docs/use/help/index.md @@ -42,6 +42,15 @@ $ ./enola help docgen ... ``` +## Gen + +[Generation](../gen/index.md) has the following options: + +```bash cd ../.././.. +$ ./enola help gen +... +``` + ## Get [Get Entity](../get/index.md) has the following options: diff --git a/docs/use/rosetta/index.md b/docs/use/rosetta/index.md index 68d231ed9..f0935f4cd 100644 --- a/docs/use/rosetta/index.md +++ b/docs/use/rosetta/index.md @@ -36,6 +36,11 @@ e.g. between: Specifying the `--schema` flag is optional for YAML <=> JSON conversion, but required for TextProto. +Rosetta transforms a (single) input resource into one output resource of another format. +Alternatively, +[generate](../gen/index.md) can load (possibly several) resources +(or "logical IRIs") which contain _Things_ and transform them into (some of) these formats. + ## Graph Diagrams Enola can generate [Graph Diagrams like this](../../models/example.org/graph.md), through [DocGen](../docgen/index.md) (see @@ -63,6 +68,16 @@ $ ./enola rosetta --no-file-loader --in test/picasso.ttl --out "docs/BUILT/picas ![Smaller Graph of Painters](../../BUILT/picasso-small.gv.svg) + ### GEXF ```bash cd ../.././.. diff --git a/java/dev/enola/BUILD b/java/dev/enola/BUILD index 96ba97599..f20609d76 100644 --- a/java/dev/enola/BUILD +++ b/java/dev/enola/BUILD @@ -36,6 +36,7 @@ java_export( # TODO Fix JavaDoc generation which somehow breaks the build; see https://github.com/enola-dev/enola/issues/491 tags = ["no-javadocs"], visibility = ["//:__subpackages__"], + # TODO runtime_deps or exports = [ ?! runtime_deps = [ "//java/dev/enola/common", "//java/dev/enola/common/canonicalize", diff --git a/java/dev/enola/cli/CommandWithIRI.java b/java/dev/enola/cli/CommandWithIRI.java index af6b5f0d7..9a22478b4 100644 --- a/java/dev/enola/cli/CommandWithIRI.java +++ b/java/dev/enola/cli/CommandWithIRI.java @@ -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; @@ -108,6 +109,13 @@ 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(thingMetadataProvider) + .convertIntoOrThrow(javaThings, resource); + return; + } + // Otherwise new ProtoIO(typeRegistryWrapper.get()).write(thing, resource); } diff --git a/java/dev/enola/cli/Configuration.java b/java/dev/enola/cli/Configuration.java index 529832303..06e53cc30 100644 --- a/java/dev/enola/cli/Configuration.java +++ b/java/dev/enola/cli/Configuration.java @@ -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; @@ -46,6 +47,7 @@ class Configuration { new MarkdownMediaTypes(), new GraphvizMediaType(), new GexfMediaType(), + new GraphCommonsMediaType(), new DatalogMediaTypes(), new StandardMediaTypes(), new YamlMediaType(), diff --git a/java/dev/enola/cli/Format.java b/java/dev/enola/cli/Format.java index f7b523d25..3cb7e0a3a 100644 --- a/java/dev/enola/cli/Format.java +++ b/java/dev/enola/cli/Format.java @@ -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 { @@ -30,6 +31,8 @@ public enum Format { Graphviz, + GraphCommons, + TextProto, ProtoYAML, @@ -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; diff --git a/java/dev/enola/common/context/Singleton.java b/java/dev/enola/common/context/Singleton.java index 7310713ce..7a3c058ee 100644 --- a/java/dev/enola/common/context/Singleton.java +++ b/java/dev/enola/common/context/Singleton.java @@ -56,7 +56,9 @@ public Singleton 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; } diff --git a/java/dev/enola/common/io/mediatype/MediaTypeProvider.java b/java/dev/enola/common/io/mediatype/MediaTypeProvider.java index cffd4553d..f31eb9554 100644 --- a/java/dev/enola/common/io/mediatype/MediaTypeProvider.java +++ b/java/dev/enola/common/io/mediatype/MediaTypeProvider.java @@ -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 diff --git a/java/dev/enola/common/io/resource/FileDescriptorResource.java b/java/dev/enola/common/io/resource/FileDescriptorResource.java index 2602b61ab..16027a07d 100644 --- a/java/dev/enola/common/io/resource/FileDescriptorResource.java +++ b/java/dev/enola/common/io/resource/FileDescriptorResource.java @@ -45,6 +45,8 @@ */ public class FileDescriptorResource extends BaseResource implements Resource { + // NB: If updating ^^^ then also update docs/use/fetch/index.md + public static final String STDOUT = "fd:1?charset=UTF-8"; public static final URI STDOUT_URI = URI.create(STDOUT); diff --git a/java/dev/enola/common/io/resource/MediaTypeDetector.java b/java/dev/enola/common/io/resource/MediaTypeDetector.java index 14c1e5cbc..8075ad059 100644 --- a/java/dev/enola/common/io/resource/MediaTypeDetector.java +++ b/java/dev/enola/common/io/resource/MediaTypeDetector.java @@ -58,7 +58,12 @@ class MediaTypeDetector { private static final Set 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(); @@ -167,8 +172,7 @@ private MediaType detect(@Nullable String contentType, @Nullable String contentE MediaType mediaType = null; if (contentType != null) { mediaType = MediaTypes.parse(contentType); - if (TRY_FIXING.contains(mediaType.withoutParameters()) - || IGNORE.contains(mediaType.withoutParameters())) { + if (isSpecial(mediaType)) { mediaType = null; } } diff --git a/java/dev/enola/common/io/resource/MemoryResourceTest.java b/java/dev/enola/common/io/resource/MemoryResourceTest.java index 140fdd14f..cda55eb3a 100644 --- a/java/dev/enola/common/io/resource/MemoryResourceTest.java +++ b/java/dev/enola/common/io/resource/MemoryResourceTest.java @@ -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; @@ -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); @@ -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); + } } diff --git a/java/dev/enola/common/io/resource/ResourceProvider.java b/java/dev/enola/common/io/resource/ResourceProvider.java index 2610d77fb..1b5fe4d10 100644 --- a/java/dev/enola/common/io/resource/ResourceProvider.java +++ b/java/dev/enola/common/io/resource/ResourceProvider.java @@ -36,6 +36,8 @@ */ public interface ResourceProvider extends ProviderFromIRI { + // TODO Rename all parameters from iri or uri to url - because that's what these are! + // TODO Change all @Nullable Resource to Optional... or, better, throw exception for // unknown schema diff --git a/java/dev/enola/common/io/testlib/ResourceSubject.java b/java/dev/enola/common/io/testlib/ResourceSubject.java index 55d5b0464..34b84659c 100644 --- a/java/dev/enola/common/io/testlib/ResourceSubject.java +++ b/java/dev/enola/common/io/testlib/ResourceSubject.java @@ -73,7 +73,7 @@ private String canonicalize(@Nullable ReadableResource resource) throws IOExcept public void hasCharsEqualTo(@Nullable ReadableResource expected) throws IOException { var actualChars = canonicalize(actual); var expectedChars = canonicalize(expected); - check("charSource").that(actualChars).isEqualTo(expectedChars); + check(actualChars).that(actualChars).isEqualTo(expectedChars); } // TODO Improve confusing output for multiline diff diff --git a/java/dev/enola/core/rosetta/Rosetta.java b/java/dev/enola/core/rosetta/Rosetta.java index a65348181..65e7f7756 100644 --- a/java/dev/enola/core/rosetta/Rosetta.java +++ b/java/dev/enola/core/rosetta/Rosetta.java @@ -38,6 +38,8 @@ 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.GraphCommonsJsonGenerator; +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; @@ -103,6 +105,8 @@ public Rosetta(ResourceProvider rp, Loader loader) { new YamlJsonResourceConverter(), new GraphvizResourceConverter(loader, new GraphvizGenerator(tmp)), new GexfResourceConverter(loader, new GexfGenerator(tmp)), + new GraphCommonsResourceConverter( + loader, new GraphCommonsJsonGenerator(tmp)), new XmlResourceConverter(rp), new CharResourceConverter())); // NOT new IdempotentCopyingResourceNonConverter() diff --git a/java/dev/enola/core/rosetta/RosettaTest.java b/java/dev/enola/core/rosetta/RosettaTest.java index 6b7e9dc2f..86688f31a 100644 --- a/java/dev/enola/core/rosetta/RosettaTest.java +++ b/java/dev/enola/core/rosetta/RosettaTest.java @@ -35,10 +35,7 @@ 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; @@ -46,6 +43,7 @@ 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; @@ -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 { @@ -72,6 +71,7 @@ public class RosettaTest { MediaTypeProviders.set( new RdfMediaTypes(), new GraphvizMediaType(), + new GraphCommonsMediaType(), new GexfMediaType(), new YamlMediaType(), new XmlMediaType())); @@ -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; @@ -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); + } } diff --git a/java/dev/enola/format/xml/XMLToThingHandler.java b/java/dev/enola/format/xml/XMLToThingHandler.java index 5d7084493..a4b205b7a 100644 --- a/java/dev/enola/format/xml/XMLToThingHandler.java +++ b/java/dev/enola/format/xml/XMLToThingHandler.java @@ -142,7 +142,7 @@ public void endElement(String uri, String localName, String qName) { || !nested.predicateIRIs().iterator().next().equals(TEXT_PROPERTY_IRI)) thingBuilders.getLast().set(iri(uri, localName, qName), nested); else { - var text = nested.get(TEXT_PROPERTY_IRI, String.class); + var text = nested.getString(TEXT_PROPERTY_IRI); thingBuilders.getLast().set(iri(uri, localName, qName), text); } } diff --git a/java/dev/enola/model/BUILD b/java/dev/enola/model/BUILD index 862be4b2f..68a89aad7 100644 --- a/java/dev/enola/model/BUILD +++ b/java/dev/enola/model/BUILD @@ -26,6 +26,8 @@ java_library( plugins = ["//tools/bazel/java_plugin:autoservice"], visibility = ["//:__subpackages__"], deps = [ + # TODO Reduce model deps - this should only be the Thing API! + # All Converters should move from /model/ to an enola.connect package. "//java/dev/enola:api", "//java/dev/enola/common", "//java/dev/enola/common/context", diff --git a/java/dev/enola/model/enola/UnknownClass.java b/java/dev/enola/model/enola/UnknownClass.java new file mode 100644 index 000000000..b1c99278e --- /dev/null +++ b/java/dev/enola/model/enola/UnknownClass.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola 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.model.enola; + +import dev.enola.model.w3.rdf.HasType; + +import java.util.Set; + +public interface UnknownClass extends HasType { + + String IRI = "https://enola.dev/UnknownClass"; + + @Override + default Iterable typesIRI() { + return Set.of(IRI); + } +} diff --git a/java/dev/enola/thing/KIRI.java b/java/dev/enola/thing/KIRI.java index cea37a0ca..9bd2882be 100644 --- a/java/dev/enola/thing/KIRI.java +++ b/java/dev/enola/thing/KIRI.java @@ -74,6 +74,9 @@ public static final class E { public static final String IRI_TEMPLATE_DATATYPE = NS + "IRITemplate"; public static final String LABEL_PROPERTY = NS + "labelProperty"; + // Special + public static final String UNKNOWN_CLASS = NS + "UnknownClass"; // TODO UnknownClass.IRI + // Style-related stuff... public static final String COLOR = NS + "color"; public static final String TEXT_COLOR = NS + "text-color"; @@ -159,6 +162,8 @@ public static final class RDF { */ public static final String TYPE = NS + "type"; + public static final String PROPERTY = NS + "Property"; + /** 📃 */ public static final String HTML = NS + "HTML"; @@ -179,8 +184,11 @@ public static final class RDFS { /** https://www.w3.org/TR/rdf-schema/#ch_comment */ public static final String COMMENT = NS + "comment"; - /** http://www.w3.org/2000/01/rdf-schema#range */ - public static final String RANGE = "range"; + /** https://www.w3.org/TR/rdf-schema/#ch_domain */ + public static final String DOMAIN = NS + "domain"; + + /** https://www.w3.org/TR/rdf-schema/#ch_range */ + public static final String RANGE = NS + "range"; private RDFS() {} } diff --git a/java/dev/enola/thing/PredicatesObjects.java b/java/dev/enola/thing/PredicatesObjects.java index 2c8c6046f..5cee6ac80 100644 --- a/java/dev/enola/thing/PredicatesObjects.java +++ b/java/dev/enola/thing/PredicatesObjects.java @@ -39,7 +39,10 @@ * *

It is typically used for objects "contained inside" Things (or their nested sub-structs). */ -public interface PredicatesObjects { +public interface PredicatesObjects /*>*/ { + + // TODO Write default implementation of as() with ProxyTBF... + // default > U as(Class clazz) { /** * The Map's key is the IRI of a predicate, and the value is as would be returned by {@link @@ -50,6 +53,8 @@ public interface PredicatesObjects { /** IRIs of the Predicates of this Thing. */ // TODO Reconsider if this method is really required? Why not just #properties().keySet()? + // ^^^ it's useful to avoid loading a huge Thing entirely into memory... + // TODO Use Iterable<> instead Set<> Set predicateIRIs(); // TODO These is*() methods could be replaced with a Visitor - but how-to for nested Structs?! diff --git a/java/dev/enola/thing/Thing.java b/java/dev/enola/thing/Thing.java index b06426d72..639c55c39 100644 --- a/java/dev/enola/thing/Thing.java +++ b/java/dev/enola/thing/Thing.java @@ -32,7 +32,9 @@ * Data, such as also described by standards such has RDF and then used e.g. by SPARQL, or * JSON-LD, etc. */ -public interface Thing extends HasIRI, PredicatesObjects { +public interface Thing extends HasIRI, PredicatesObjects /**/ { + + // TODO @Override default > U as(Class clazz) { @Override String iri(); diff --git a/java/dev/enola/thing/gen/BUILD b/java/dev/enola/thing/gen/BUILD index 2bd7766c2..354929f86 100644 --- a/java/dev/enola/thing/gen/BUILD +++ b/java/dev/enola/thing/gen/BUILD @@ -49,6 +49,7 @@ java_library( "@enola_maven//:com_google_auto_service_auto_service_annotations", "@enola_maven//:com_google_errorprone_error_prone_annotations", "@enola_maven//:com_google_errorprone_error_prone_type_annotations", + "@enola_maven//:com_google_code_gson_gson", "@enola_maven//:com_google_guava_guava", "@enola_maven//:org_jspecify_jspecify", "@enola_maven//:org_slf4j_slf4j_api", @@ -66,6 +67,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", diff --git a/java/dev/enola/thing/gen/Colorizer.java b/java/dev/enola/thing/gen/Colorizer.java new file mode 100644 index 000000000..004d74d46 --- /dev/null +++ b/java/dev/enola/thing/gen/Colorizer.java @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola 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; + +import dev.enola.thing.KIRI; +import dev.enola.thing.Thing; + +public final class Colorizer { + + public static String hexColor(Thing thing) { + String color; + var colorProperty = thing.get(KIRI.E.COLOR, String.class); + if (colorProperty != null) return colorProperty; + return getColorCodeFromHash(thing.iri()); + } + + private static String getColorCodeFromHash(String inputString) { + int hashCode = inputString.hashCode(); + hashCode = Math.abs(hashCode); + int red = (hashCode & 0xFF0000) >> 16; + int green = (hashCode & 0x00FF00) >> 8; + int blue = hashCode & 0x0000FF; + return String.format("#%02X%02X%02X", red, green, blue); + } + + private Colorizer() {} +} diff --git a/java/dev/enola/thing/gen/Orphanage.java b/java/dev/enola/thing/gen/Orphanage.java new file mode 100644 index 000000000..01026d554 --- /dev/null +++ b/java/dev/enola/thing/gen/Orphanage.java @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola 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; + +import java.util.HashSet; +import java.util.Set; + +public class Orphanage { + private final Set orphanCandidates = new HashSet<>(); + private final Set nonOrphans = new HashSet<>(); + + public void candidate(String linkIRI) { + if (!nonOrphans.contains(linkIRI)) orphanCandidates.add(linkIRI); + } + + public void nonOrphan(String thingIRI) { + nonOrphans.add(thingIRI); + // TODO We could optimize and do orphanCandidates.remove(thingIRI); ? + } + + public Set orphans() { + // Remove links to all things which were processed after we processed them + // linkIRIs now contains things which were linked to but that have no properties + orphanCandidates.removeAll(nonOrphans); + return orphanCandidates; + } +} diff --git a/java/dev/enola/thing/gen/gexf/GexfMediaTypeTest.java b/java/dev/enola/thing/gen/gexf/GexfMediaTypeTest.java new file mode 100644 index 000000000..62272a01d --- /dev/null +++ b/java/dev/enola/thing/gen/gexf/GexfMediaTypeTest.java @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola 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); + } +} diff --git a/java/dev/enola/thing/gen/graphcommons/GraphCommonsJsonGenerator.java b/java/dev/enola/thing/gen/graphcommons/GraphCommonsJsonGenerator.java new file mode 100644 index 000000000..8d11b410f --- /dev/null +++ b/java/dev/enola/thing/gen/graphcommons/GraphCommonsJsonGenerator.java @@ -0,0 +1,226 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola 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 static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.hash.Hashing; +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.common.io.metadata.Metadata; +import dev.enola.thing.KIRI; +import dev.enola.thing.Thing; +import dev.enola.thing.gen.Colorizer; +import dev.enola.thing.gen.Orphanage; +import dev.enola.thing.gen.ThingsIntoAppendableConverter; +import dev.enola.thing.impl.OnlyIRIThing; +import dev.enola.thing.metadata.ThingMetadataProvider; +import dev.enola.thing.repo.StackedThingProvider; +import dev.enola.thing.repo.ThingProvider; + +import java.io.IOException; + +/** + * Generator of JSON Format used by Graph Commons. + * + *

@deprecated This does not actually work. + */ +@Deprecated +public class GraphCommonsJsonGenerator implements ThingsIntoAppendableConverter { + + // ============================================================================ + // This does not actually work. + // The reason is that https://graphcommons.com expects every edgeType + // to match to exactly one sourceNodeTypeId & targetNodeTypeId; but that's + // not the case in RDF. + // To make this work, you probably would have to add "fake" edgeTypes... + // ============================================================================ + + // TODO Handle links inside blank nodes; see the other Network Graph Generators + + private final ThingMetadataProvider metadataProvider; + + public GraphCommonsJsonGenerator(ThingMetadataProvider metadataProvider) { + this.metadataProvider = metadataProvider; + } + + @Override + public boolean convertInto(Iterable from, Appendable out) + throws ConversionException, IOException { + var nodeTypeOrphanage = new Orphanage(); + var edgeTypeOrphanage = new Orphanage(); + 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, nodeTypeOrphanage); + + jsonWriter.endArray().name("edges").beginArray(); + for (Thing thing : from) printThingEdge(thing, jsonWriter, edgeTypeOrphanage); + + jsonWriter.endArray().name("nodeTypes").beginArray(); + for (Thing thing : from) { + var types = thing.getLinks(KIRI.RDF.TYPE); + // TODO Include classes that are a subclass of rdfs:Class, such as schema:Class + if (!types.stream().map(Object::toString).toList().contains(KIRI.RDFS.CLASS)) + continue; + printThingNodeType(thing, jsonWriter, nodeTypeOrphanage); + } + for (String orphanIRI : nodeTypeOrphanage.orphans()) + printThingNodeType(new OnlyIRIThing(orphanIRI), jsonWriter, nodeTypeOrphanage); + + jsonWriter.endArray().name("edgeTypes").beginArray(); + for (Thing thing : from) { + var types = thing.getLinks(KIRI.RDF.TYPE); + // TODO Include properties that are subclass of rdf:Property (e.g. schema:Property) + if (!types.stream().map(Object::toString).toList().contains(KIRI.RDF.PROPERTY)) + continue; + printThingEdgeType(thing, jsonWriter, edgeTypeOrphanage); + } + for (String orphanIRI : edgeTypeOrphanage.orphans()) + printThingEdgeType(new OnlyIRIThing(orphanIRI), jsonWriter, edgeTypeOrphanage); + + jsonWriter.endArray().name("name").value("Enola.dev"); + } + jsonWriter.endObject(); + jsonWriter.flush(); + writer.close(); + return true; + } + + private void printThingNode(Thing thing, JsonWriter jsonWriter, Orphanage nodeTypeOrphanage) + throws IOException { + jsonWriter.beginObject(); + jsonWriter.name("id").value(thing.iri()); + + String typeId; + var types = thing.getLinks(KIRI.RDF.TYPE); + // NB: This randomly picks one of (arbitrarily the "first") type - there could be several! + if (!types.isEmpty()) typeId = types.iterator().next().toString(); + else typeId = KIRI.E.UNKNOWN_CLASS; + jsonWriter.name("typeId").value(typeId); + nodeTypeOrphanage.candidate(typeId); + + var meta = metadataProvider.get(thing.iri()); + printName(meta, jsonWriter); + printDescription(meta, jsonWriter); + printImage(meta, jsonWriter); + // TODO "reference" - what's that? + + jsonWriter.endObject(); + } + + private void printThingEdge(Thing thing, JsonWriter jsonWriter, Orphanage edgeTypeOrphanage) + throws IOException { + var sourceId = thing.iri(); + for (var linkPropertyIRI : thing.predicateIRIs()) { + if (linkPropertyIRI.equals(KIRI.RDF.TYPE)) continue; + var links = thing.getLinks(linkPropertyIRI); + if (!links.isEmpty()) edgeTypeOrphanage.candidate(linkPropertyIRI); + for (var link : links) { + var linkIRI = link.toString(); + jsonWriter.beginObject(); + jsonWriter.name("id").value(hash(sourceId, linkPropertyIRI, linkIRI)); + jsonWriter.name("typeId").value(linkPropertyIRI); + jsonWriter.name("sourceId").value(sourceId); + jsonWriter.name("targetId").value(linkIRI); + // TODO Weight? + jsonWriter.endObject(); + } + } + } + + private void printThingNodeType(Thing thing, JsonWriter jsonWriter, Orphanage nodeTypeOrphanage) + throws IOException { + jsonWriter.beginObject(); + jsonWriter.name("id").value(thing.iri()); + nodeTypeOrphanage.nonOrphan(thing.iri()); + var meta = metadataProvider.get(thing.iri()); + printName(meta, jsonWriter); + printDescription(meta, jsonWriter); + printColor(thing, jsonWriter); + // TODO "properties" ... + jsonWriter.endObject(); + } + + private void printThingEdgeType(Thing thing, JsonWriter jsonWriter, Orphanage edgeTypeOrphanage) + throws IOException { + jsonWriter.beginObject(); + jsonWriter.name("id").value(thing.iri()); + edgeTypeOrphanage.nonOrphan(thing.iri()); + var meta = metadataProvider.get(thing.iri()); + printName(meta, jsonWriter); + printDescription(meta, jsonWriter); + // TODO Handle bi-directional relationships + jsonWriter.name("directed").value(true); + printColor(thing, jsonWriter); + jsonWriter + .name("sourceNodeTypeId") + .value(printLinkPropertyOrUnknown(thing, KIRI.RDFS.DOMAIN)); + jsonWriter + .name("targetNodeTypeId") + .value(printLinkPropertyOrUnknown(thing, KIRI.RDFS.RANGE)); + // TODO "properties" ... + jsonWriter.endObject(); + } + + private String printLinkPropertyOrUnknown(Thing thing, String propertyIRI) { + // TODO What about if rdfs:domain or rdfs:range is multiple?! + var iri = thing.getString(propertyIRI); + if (iri != null) return iri; + else return KIRI.E.UNKNOWN_CLASS; + } + + private void printName(Metadata meta, JsonWriter jsonWriter) throws IOException { + jsonWriter.name("name").value(meta.label()); + } + + private void printDescription(Metadata meta, JsonWriter jsonWriter) throws IOException { + if (!meta.descriptionHTML().isEmpty()) + jsonWriter.name("description").value(meta.descriptionHTML()); + } + + private void printImage(Metadata meta, JsonWriter jsonWriter) throws IOException { + if (!meta.imageURL().isEmpty()) jsonWriter.name("image").value(meta.imageURL()); + } + + private void printColor(Thing thing, JsonWriter jsonWriter) throws IOException { + jsonWriter.name("color").value(Colorizer.hexColor(thing)); + } + + private String hash(String... strings) { + var hasher = Hashing.murmur3_128().newHasher(); + for (var string : strings) { + hasher.putString(string, UTF_8).putInt(1); + } + return hasher.hash().toString(); + } +} diff --git a/java/dev/enola/thing/gen/graphcommons/GraphCommonsMediaType.java b/java/dev/enola/thing/gen/graphcommons/GraphCommonsMediaType.java new file mode 100644 index 000000000..744708d71 --- /dev/null +++ b/java/dev/enola/thing/gen/graphcommons/GraphCommonsMediaType.java @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola 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 com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; +import com.google.common.net.MediaType; + +import dev.enola.common.io.mediatype.MediaTypeProvider; + +import java.nio.charset.StandardCharsets; + +public class GraphCommonsMediaType implements MediaTypeProvider { + + public static final MediaType GCJSON = + MediaType.create("text", "vnd.enola.graphcommons+json") + .withCharset(StandardCharsets.UTF_8); + + @Override + public Multimap extensionsToTypes() { + return ImmutableMultimap.of(".graphcommons.json", GCJSON); + } +} diff --git a/java/dev/enola/thing/gen/graphcommons/GraphCommonsMediaTypeTest.java b/java/dev/enola/thing/gen/graphcommons/GraphCommonsMediaTypeTest.java new file mode 100644 index 000000000..913ccade6 --- /dev/null +++ b/java/dev/enola/thing/gen/graphcommons/GraphCommonsMediaTypeTest.java @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola 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.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 org.junit.Rule; +import org.junit.Test; + +public class GraphCommonsMediaTypeTest { + @Rule public SingletonRule r = $(MediaTypeProviders.set(new GraphCommonsMediaType())); + + @Test + public void graphCommonsMediaType() { + var r = new ClasspathResource.Provider().get("classpath:/graph.expected.graphcommons.json"); + assertThat(r.mediaType()).isEqualTo(GraphCommonsMediaType.GCJSON); + } +} diff --git a/java/dev/enola/thing/gen/graphcommons/GraphCommonsResourceConverter.java b/java/dev/enola/thing/gen/graphcommons/GraphCommonsResourceConverter.java new file mode 100644 index 000000000..aaf2da1c9 --- /dev/null +++ b/java/dev/enola/thing/gen/graphcommons/GraphCommonsResourceConverter.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright 2024 The Enola 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 dev.enola.common.io.mediatype.MediaTypes; +import dev.enola.common.io.resource.ReadableResource; +import dev.enola.common.io.resource.WritableResource; +import dev.enola.common.io.resource.convert.CatchingResourceConverter; +import dev.enola.thing.io.Loader; + +public class GraphCommonsResourceConverter implements CatchingResourceConverter { + + private final GraphCommonsJsonGenerator graphCommonsGenerator; + private final Loader loader; + + public GraphCommonsResourceConverter( + Loader loader, GraphCommonsJsonGenerator graphCommonsGenerator) { + this.loader = loader; + this.graphCommonsGenerator = graphCommonsGenerator; + } + + @Override + public boolean convertIntoThrows(ReadableResource from, WritableResource into) + throws Exception { + if (!MediaTypes.normalizedNoParamsEquals(into.mediaType(), GraphCommonsMediaType.GCJSON)) + return false; + + var things = loader.loadAtLeastOneThing(from.uri()); + graphCommonsGenerator.convertIntoOrThrow(things, into); + return true; + } +} diff --git a/java/dev/enola/thing/gen/graphviz/GraphvizGenerator.java b/java/dev/enola/thing/gen/graphviz/GraphvizGenerator.java index ef788c7c3..255c52d12 100644 --- a/java/dev/enola/thing/gen/graphviz/GraphvizGenerator.java +++ b/java/dev/enola/thing/gen/graphviz/GraphvizGenerator.java @@ -26,6 +26,7 @@ import dev.enola.thing.KIRI; import dev.enola.thing.PredicatesObjects; import dev.enola.thing.Thing; +import dev.enola.thing.gen.Orphanage; import dev.enola.thing.gen.ThingsIntoAppendableConverter; import dev.enola.thing.impl.OnlyIRIThing; import dev.enola.thing.metadata.ThingMetadataProvider; @@ -81,29 +82,24 @@ public GraphvizGenerator(ThingMetadataProvider metadataProvider) { public boolean convertInto(Iterable from, Appendable out) throws ConversionException, IOException { Set thingIRIs = new HashSet<>(); - Set linkIRIs = new HashSet<>(); + var orphanage = new Orphanage(); out.append("digraph {\n"); try (var ctx = TLC.open()) { ctx.push(ThingProvider.class, new StackedThingProvider(from)); for (Thing thing : from) { - thingIRIs.add(thing.iri()); - printFullThing(thing, out, thingIRIs, linkIRIs); + orphanage.nonOrphan(thing.iri()); + printThing(thing, out, orphanage); } - // Remove links to all things which were processed after we processed them - linkIRIs.removeAll(thingIRIs); - // linkIRIs now contains things which were linked to but that have no properties - for (String orphanIRI : linkIRIs) { + for (String orphanIRI : orphanage.orphans()) { var orphanThing = new OnlyIRIThing(orphanIRI); - printFullThing(orphanThing, out, thingIRIs, linkIRIs); + printThing(orphanThing, out, orphanage); } } out.append("}\n"); return true; } - private void printFullThing( - Thing thing, Appendable out, Set thingIRIs, Set linkIRIs) - throws IOException { + private void printThing(Thing thing, Appendable out, Orphanage orphanage) throws IOException { boolean full = TLC.optional(Flags.FULL).orElse(false); out.append(" \""); @@ -148,7 +144,7 @@ private void printFullThing( out.append("\" label=\""); out.append(html(linkLabel)); out.append("\"]\n"); - if (!thingIRIs.contains(linkIRI)) linkIRIs.add(linkIRI); + orphanage.candidate(linkIRI); } } out.append('\n'); diff --git a/java/dev/enola/thing/io/ThingMediaTypes.java b/java/dev/enola/thing/io/ThingMediaTypes.java index 5b676a01e..3366685a8 100644 --- a/java/dev/enola/thing/io/ThingMediaTypes.java +++ b/java/dev/enola/thing/io/ThingMediaTypes.java @@ -34,6 +34,8 @@ public class ThingMediaTypes implements MediaTypeProvider { + // TODO Use vnd.enola.* like e.g. in GraphCommonsMediaType (for consistency) + public static final MediaType THING_TEXTPROTO_UTF_8 = ProtobufMediaTypes.setProtoMessageFQN( ProtobufMediaTypes.PROTOBUF_TEXTPROTO_UTF_8, diff --git a/mkdocs.yaml b/mkdocs.yaml index f739cf709..7f84ddbf3 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -58,6 +58,7 @@ nav: - Help: use/help/index.md - Example Model: use/library/index.md - DocGen: use/docgen/index.md + - Generate: use/gen/index.md - Get Thing: use/get/index.md - Server: use/server/index.md - Rosetta: use/rosetta/index.md diff --git a/models/enola.dev/enola.ttl b/models/enola.dev/enola.ttl index 83eef70d6..7fadf22e5 100644 --- a/models/enola.dev/enola.ttl +++ b/models/enola.dev/enola.ttl @@ -30,6 +30,10 @@ enola: a owl:Ontology; rdfs:label "Enola.dev's base/core Ontology."; enola:doc . +enola:UnknownClass a rdfs:Class; + schema:name "(Unknown)"; + rdfs:comment "Used when a Thing does not have a rdf:type but one is absolutely required e.g. by some tool.". + enola:Embedded a rdfs:Class; rdfs:comment "Parent (abstract) Class for things which have no IRI identity of their own and which thus should only ever appear in 'RDF blank nodes'. (Like owl:hasValue _:bNode on an owl:DatatypeProperty; without OWL dependency. Or like a sh:nodeKind sh:BlankNode SHACL shape.)". diff --git a/test/graph.expected.graphcommons.json b/test/graph.expected.graphcommons.json new file mode 100644 index 000000000..d3cf7a596 --- /dev/null +++ b/test/graph.expected.graphcommons.json @@ -0,0 +1,167 @@ +{ + "edgeTypes": [ + { + "color": "#338A22", + "directed": true, + "id": "https://enola.dev/origin", + "name": "enola:origin", + "sourceNodeTypeId": "https://enola.dev/UnknownClass", + "targetNodeTypeId": "https://enola.dev/UnknownClass" + }, + { + "color": "#92BE07", + "directed": true, + "id": "http://www.w3.org/2000/01/rdf-schema#subClassOf", + "name": "rdfs:subClassOf", + "sourceNodeTypeId": "https://enola.dev/UnknownClass", + "targetNodeTypeId": "https://enola.dev/UnknownClass" + }, + { + "color": "#B2FAE8", + "directed": true, + "id": "https://example.org/link", + "name": "ex:link", + "sourceNodeTypeId": "https://enola.dev/UnknownClass", + "targetNodeTypeId": "https://enola.dev/UnknownClass" + }, + { + "color": "#DA28DD", + "directed": true, + "id": "https://example.org/object", + "name": "ex:object", + "sourceNodeTypeId": "https://enola.dev/UnknownClass", + "targetNodeTypeId": "https://enola.dev/UnknownClass" + }, + { + "color": "#AC61A5", + "directed": true, + "id": "https://example.org/links", + "name": "ex:links", + "sourceNodeTypeId": "https://enola.dev/UnknownClass", + "targetNodeTypeId": "https://enola.dev/UnknownClass" + } + ], + "edges": [ + { + "id": "4d1d1849d7b930c01eca3956c96d1f0e", + "sourceId": "http://www.w3.org/2000/01/rdf-schema#Class", + "targetId": "classpath:/graph.ttl", + "typeId": "https://enola.dev/origin" + }, + { + "id": "21cfa23a9b722a169c9ac13377aae624", + "sourceId": "https://example.org/Salutation", + "targetId": "https://schema.org/TextObject", + "typeId": "http://www.w3.org/2000/01/rdf-schema#subClassOf" + }, + { + "id": "229e8eb18b16fe769a1258a912bb982b", + "sourceId": "https://example.org/Salutation", + "targetId": "classpath:/graph.ttl", + "typeId": "https://enola.dev/origin" + }, + { + "id": "556e11f833dda2831ed2a02ca5f22ada", + "sourceId": "https://example.org/TwoLinks", + "targetId": "https://example.org/greeting3", + "typeId": "https://example.org/links" + }, + { + "id": "2670042a95e2af4f285a16369f4d9a4b", + "sourceId": "https://example.org/TwoLinks", + "targetId": "https://example.org/world", + "typeId": "https://example.org/links" + }, + { + "id": "08560fda4c0bc12dab017f669ee0a6e3", + "sourceId": "https://example.org/TwoLinks", + "targetId": "classpath:/graph.ttl", + "typeId": "https://enola.dev/origin" + }, + { + "id": "c847cb80d8bea95cdd1af638f9f0979e", + "sourceId": "https://example.org/greeting3", + "targetId": "https://example.org/world", + "typeId": "https://example.org/object" + }, + { + "id": "3aa7344ecf2d2d035ebce05032eccf8a", + "sourceId": "https://example.org/greeting3", + "targetId": "classpath:/graph.ttl", + "typeId": "https://enola.dev/origin" + }, + { + "id": "2f16d34dd7d019a8b757681ece6dfc4e", + "sourceId": "https://example.org/orphan", + "targetId": "https://example.org/OnlyIRIThing", + "typeId": "https://example.org/link" + }, + { + "id": "490ec3136255edf949cccf9af167e406", + "sourceId": "https://example.org/orphan", + "targetId": "classpath:/graph.ttl", + "typeId": "https://enola.dev/origin" + }, + { + "id": "fa469a19d47dd9d96d31ff4b2c1a5571", + "sourceId": "https://example.org/world", + "targetId": "classpath:/graph.ttl", + "typeId": "https://enola.dev/origin" + } + ], + "name": "Enola.dev", + "nodeTypes": [ + { + "color": "violet", + "id": "https://example.org/Salutation", + "name": "ex:Salutation" + }, + { + "color": "#BCD296", + "id": "https://enola.dev/UnknownClass", + "name": "enola:UnknownClass" + }, + { + "color": "#B6F8A0", + "id": "http://www.w3.org/2000/01/rdf-schema#Class", + "name": "rdfs:Class" + }, + { + "color": "#7CE356", + "id": "https://example.org/Planet", + "name": "ex:Planet" + } + ], + "nodes": [ + { + "id": "http://www.w3.org/2000/01/rdf-schema#Class", + "name": "rdfs:Class", + "typeId": "https://enola.dev/UnknownClass" + }, + { + "id": "https://example.org/Salutation", + "name": "ex:Salutation", + "typeId": "http://www.w3.org/2000/01/rdf-schema#Class" + }, + { + "id": "https://example.org/TwoLinks", + "name": "ex:TwoLinks", + "typeId": "https://enola.dev/UnknownClass" + }, + { + "id": "https://example.org/greeting3", + "name": "ex:greeting3", + "typeId": "https://example.org/Salutation" + }, + { + "id": "https://example.org/orphan", + "name": "ex:orphan", + "typeId": "https://enola.dev/UnknownClass" + }, + { + "id": "https://example.org/world", + "name": "ex:world", + "typeId": "https://example.org/Planet" + } + ] +}