diff --git a/java/dev/enola/model/enola/HasChildren.java b/java/dev/enola/model/enola/HasChildren.java new file mode 100644 index 00000000..a9632a09 --- /dev/null +++ b/java/dev/enola/model/enola/HasChildren.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.model.enola; + +import dev.enola.thing.Link; +import dev.enola.thing.Thing; + +import org.jspecify.annotations.Nullable; + +import java.util.Set; + +public interface HasChildren extends Thing { + + default @Nullable Set childrenIRI() { + return get("https://enola.dev/children", Set.class); + } + + interface Builder extends Thing.Builder { // skipcq: JAVA-E0169 + default HasChildren.Builder childrenIRI(Set childrenIRI) { + set("https://enola.dev/children", childrenIRI); + return this; + } + } +} diff --git a/java/dev/enola/model/enola/HasParent.java b/java/dev/enola/model/enola/HasParent.java new file mode 100644 index 00000000..05622366 --- /dev/null +++ b/java/dev/enola/model/enola/HasParent.java @@ -0,0 +1,37 @@ +/* + * 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.thing.Link; +import dev.enola.thing.Thing; + +import org.jspecify.annotations.Nullable; + +public interface HasParent extends Thing { + + default @Nullable String parentIRI() { + return getString("https://enola.dev/parent"); + } + + interface Builder extends Thing.Builder { // skipcq: JAVA-E0169 + default HasParent.Builder parentIRI(String iri) { + set("https://enola.dev/parent", new Link(iri)); + return this; + } + } +} diff --git a/java/dev/enola/model/enola/mediatype/HasFileExtensions.java b/java/dev/enola/model/enola/mediatype/HasFileExtensions.java index 6fd624ad..e68f000c 100644 --- a/java/dev/enola/model/enola/mediatype/HasFileExtensions.java +++ b/java/dev/enola/model/enola/mediatype/HasFileExtensions.java @@ -35,5 +35,10 @@ default HasFileExtensions.Builder addFileExtension(String fileExtension) { add("https://enola.dev/fileExtensions", fileExtension); return this; } + + default HasFileExtensions.Builder addAllFileExtensions(Iterable fileExtensions) { + addAll("https://enola.dev/fileExtensions", fileExtensions); + return this; + } } } diff --git a/java/dev/enola/model/enola/mediatype/MediaType.java b/java/dev/enola/model/enola/mediatype/MediaType.java index 17e1348c..2f1d85c2 100644 --- a/java/dev/enola/model/enola/mediatype/MediaType.java +++ b/java/dev/enola/model/enola/mediatype/MediaType.java @@ -17,12 +17,20 @@ */ package dev.enola.model.enola.mediatype; +import dev.enola.model.enola.HasChildren; +import dev.enola.model.enola.HasParent; import dev.enola.model.w3.rdfs.HasComment; import dev.enola.model.w3.rdfs.HasLabel; import dev.enola.model.w3.rdfs.HasSeeAlso; public interface MediaType - extends HasLabel, HasComment, HasSeeAlso, HasFileExtensions, HasMediaType { + extends HasLabel, + HasComment, + HasSeeAlso, + HasFileExtensions, + HasMediaType, + HasParent, + HasChildren { // In theory: interface Builder extends ... // In practice, we know we're not going to further extend MediaType, so just: @@ -31,5 +39,7 @@ interface Builder // skipcq: JAVA-E0169 HasComment.Builder, HasSeeAlso.Builder, HasFileExtensions.Builder, - HasMediaType.Builder {} + HasMediaType.Builder, + HasParent.Builder, + HasChildren.Builder {} } diff --git a/java/dev/enola/model/enola/mediatype/TikaMediaTypesThingConverter.java b/java/dev/enola/model/enola/mediatype/TikaMediaTypesThingConverter.java index bce25f3c..61deb192 100644 --- a/java/dev/enola/model/enola/mediatype/TikaMediaTypesThingConverter.java +++ b/java/dev/enola/model/enola/mediatype/TikaMediaTypesThingConverter.java @@ -61,25 +61,39 @@ public boolean convertInto(URI from, TypedThingsBuilder uri.toString()).toList()); // TODO var uniformTypeIdentifier = tikaMimeType.getUniformTypeIdentifier(); // TODO var hasMagic = tikaMimeType.hasMagic(); - tikaMediaTypeRegistry.getChildTypes(tikaMediaType); - tikaMediaTypeRegistry.getSupertype(tikaMediaType); + // TODO Tika hard-codes :( a few special cases, and doesn't e.g. do +json... + var superType = tikaMediaTypeRegistry.getSupertype(tikaMediaType); + if (superType != null) thing.parentIRI(toIRI(superType)); - var baseType = tikaMediaType.getBaseType(); - if (!baseType.equals(tikaMediaType)) {} + // tikaMediaType.getBaseType() is a superset of getSupertype() + + // TODO Making the following a 1 liner... + // TODO Remove this once children are automagically set by generic Inference!! + // TODO Uncomment, once GraphvizGenerator more nicely coalesces parent & children + /* + var children = tikaMediaTypeRegistry.getChildTypes(tikaMediaType); + if (!children.isEmpty()) { + var childrenIRI = ImmutableSet.builderWithExpectedSize(children.size()); + for (var child : children) { + childrenIRI.add(new Link(toIRI(child))); + } + thing.childrenIRI(childrenIRI.build()); + } + */ } catch (MimeTypeException e) { LOG.warn("MediaType not found: {}", mediaTypeName, e); @@ -87,4 +101,8 @@ public boolean convertInto(URI from, TypedThingsBuilder addSeeAlso(String seeAlso) { add(IRI.Predicate.seeAlso, seeAlso); return this; } + + default HasSeeAlso.Builder addAllSeeAlso(Iterable seeAlso) { + addAll(IRI.Predicate.seeAlso, seeAlso, KIRI.SCHEMA.URL_DATATYPE); + return this; + } } } diff --git a/java/dev/enola/thing/KIRI.java b/java/dev/enola/thing/KIRI.java index 0d303bc2..28174385 100644 --- a/java/dev/enola/thing/KIRI.java +++ b/java/dev/enola/thing/KIRI.java @@ -117,13 +117,16 @@ public static final class SCHEMA { public static final String LOGO = NS + "logo"; /** - * URL 🔗 of the Thing, see https://schema.org/url. You *CAN* always http GET an URL. This - * is NOT the same as a logical URI/IRI, and thus not be to confused with the {@link #ID}. - * One example of this could be e.g. its use in Thing "metadata" about a file: URL; this - * would point to the actual file itself. + * IRI of Property for URL 🔗 of the Thing, see https://schema.org/url. You *CAN* always + * http GET an URL. This is NOT the same as a logical URI/IRI, and thus not be to confused + * with the {@link #ID}. One example of this could be e.g. its use in Thing "metadata" about + * a file: URL; this would point to the actual file itself. */ public static final String URL = NS + "url"; + /** IRI of URL Datatype. Used to mark properties which are links to webpages. */ + public static final String URL_DATATYPE = NS + "URL"; + /** * IRI of a Thing which is "the 🪞 same as this one", see https://schema.org/sameAs. For * example, the URL of a Wikipedia article about it. diff --git a/java/dev/enola/thing/Link.java b/java/dev/enola/thing/Link.java index 2e9013a0..6262069f 100644 --- a/java/dev/enola/thing/Link.java +++ b/java/dev/enola/thing/Link.java @@ -24,6 +24,7 @@ * be returned by {@link Thing#get(String)} and distinguished from a String which is not an IRI but * text. */ +// TODO Consider using a Datatype to indicate link? But which... // TODO Abandon this and just use java.net.URI in Things instead?! // Or change this record to a class and have an URI field, for 1 time conversion. // TODO Make it extend Thing; and voilà, it's a Property Graph! diff --git a/java/dev/enola/thing/PredicatesObjects.java b/java/dev/enola/thing/PredicatesObjects.java index 7a2f876b..2c8c6046 100644 --- a/java/dev/enola/thing/PredicatesObjects.java +++ b/java/dev/enola/thing/PredicatesObjects.java @@ -229,9 +229,15 @@ interface Builder2 extends PredicatesObjects.Builde */ <@ImmutableTypeParameter T> PredicatesObjects.Builder2 add(String predicateIRI, T value); + <@ImmutableTypeParameter T> PredicatesObjects.Builder2 addAll( + String predicateIRI, Iterable value); + <@ImmutableTypeParameter T> PredicatesObjects.Builder2 add( String predicateIRI, T value, @Nullable String datatypeIRI); + <@ImmutableTypeParameter T> PredicatesObjects.Builder2 addAll( + String predicateIRI, Iterable value, @Nullable String datatypeIRI); + /** * Adds one of possibly several value objects for the given predicate IRI - and preserves * order. diff --git a/java/dev/enola/thing/Thing.java b/java/dev/enola/thing/Thing.java index 483a4d72..b06426d7 100644 --- a/java/dev/enola/thing/Thing.java +++ b/java/dev/enola/thing/Thing.java @@ -65,17 +65,31 @@ interface Builder2 // skipcq: JAVA-E0169 Thing.Builder2 add(String predicateIRI, T value); + Thing.Builder2 addAll(String predicateIRI, Iterable value); + default Thing.Builder2 add(HasPredicateIRI predicate, T value) { return add(predicate.iri(), value); } + default Thing.Builder2 addAll(HasPredicateIRI predicate, Iterable value) { + return addAll(predicate.iri(), value); + } + Thing.Builder2 add(String predicateIRI, T value, @Nullable String datatypeIRI); + Thing.Builder2 addAll( + String predicateIRI, Iterable value, @Nullable String datatypeIRI); + default Thing.Builder2 add( HasPredicateIRI predicate, T value, @Nullable String datatypeIRI) { return add(predicate.iri(), value, datatypeIRI); } + default Thing.Builder2 addAll( + HasPredicateIRI predicate, Iterable value, @Nullable String datatypeIRI) { + return addAll(predicate.iri(), value, datatypeIRI); + } + Thing.Builder2 addOrdered(String predicateIRI, T value); default Thing.Builder2 addOrdered(HasPredicateIRI predicate, T value) { diff --git a/java/dev/enola/thing/ThingTester.java b/java/dev/enola/thing/ThingTester.java index 07769e0f..1c6d5a7d 100644 --- a/java/dev/enola/thing/ThingTester.java +++ b/java/dev/enola/thing/ThingTester.java @@ -25,6 +25,8 @@ import org.junit.Before; import org.junit.Test; +import java.util.Set; + public abstract class ThingTester { // TODO Move some of the generic tests from ImmutableThingTest up here @@ -108,4 +110,12 @@ public void setEmptyStringIsIgnored() { var thing = thingBuilder.build(); assertThat(thing.predicateIRIs()).isEmpty(); } + + @Test + public void setEmptyCollectionIsIgnored() { + thingBuilder.iri(THING_IRI); + thingBuilder.set(PREDICATE_IRI, Set.of()); + var thing = thingBuilder.build(); + assertThat(thing.predicateIRIs()).isEmpty(); + } } diff --git a/java/dev/enola/thing/gen/graphviz/GraphvizGenerator.java b/java/dev/enola/thing/gen/graphviz/GraphvizGenerator.java index c084676f..e2d638d9 100644 --- a/java/dev/enola/thing/gen/graphviz/GraphvizGenerator.java +++ b/java/dev/enola/thing/gen/graphviz/GraphvizGenerator.java @@ -39,6 +39,15 @@ public class GraphvizGenerator implements ThingsIntoAppendableConverter { + // TODO Coalesce e.g. enola:parent & enola:children (schema:inverseOf) into single dir=both link + // This would be useful e.g. for the TikaMediaTypesThingConverter produced graph diagram + // Note that strict digraph graphName { concentrate=true does not do this (because of labels; + // see https://stackoverflow.com/a/3463332/421602). + + // TODO Subgraphs? https://graphviz.org/doc/info/lang.html#subgraphs-and-clusters Classes? + + // TODO Links to other Things (not external HTTP) from within nested blank nodes? With ports?? + private static final int MAX_TEXT_LENGTH = 23; // NB: RosettaTest#testGraphviz() is the test coverage for this code @@ -51,10 +60,6 @@ public class GraphvizGenerator implements ThingsIntoAppendableConverter { // NB: We're intentionally *NOT* showing the Datatype of properties (it's "too much")- - // TODO Subgraphs? https://graphviz.org/doc/info/lang.html#subgraphs-and-clusters Classes? - - // TODO Links to other Things (not external HTTP) from within nested blank nodes? With ports?? - private final ThingMetadataProvider metadataProvider; public GraphvizGenerator(ThingMetadataProvider metadataProvider) { diff --git a/java/dev/enola/thing/impl/ImmutablePredicatesObjects.java b/java/dev/enola/thing/impl/ImmutablePredicatesObjects.java index 738f2be6..e5c48789 100644 --- a/java/dev/enola/thing/impl/ImmutablePredicatesObjects.java +++ b/java/dev/enola/thing/impl/ImmutablePredicatesObjects.java @@ -19,6 +19,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import com.google.errorprone.annotations.Immutable; import com.google.errorprone.annotations.ThreadSafe; @@ -137,6 +138,7 @@ static class Builder // skipcq: JAVA-E0169 public PredicatesObjects.Builder set(String predicateIRI, Object value) { if (value == null) return this; if (value instanceof String string && string.isEmpty()) return this; + if (value instanceof Iterable iterable && Iterables.isEmpty(iterable)) return this; ImmutableObjects.check(value); if (value instanceof Literal literal) set(predicateIRI, literal.value(), literal.datatypeIRI()); @@ -149,6 +151,7 @@ public PredicatesObjects.Builder set( String predicateIRI, Object value, @Nullable String datatypeIRI) { if (value == null) return this; if (value instanceof String string && string.isEmpty()) return this; + if (value instanceof Iterable iterable && Iterables.isEmpty(iterable)) return this; ImmutableObjects.check(value); if (datatypeIRI != null) { if (value instanceof Literal) diff --git a/java/dev/enola/thing/impl/MutablePredicatesObjects.java b/java/dev/enola/thing/impl/MutablePredicatesObjects.java index e1294b90..3f7f5559 100644 --- a/java/dev/enola/thing/impl/MutablePredicatesObjects.java +++ b/java/dev/enola/thing/impl/MutablePredicatesObjects.java @@ -20,6 +20,7 @@ import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import dev.enola.thing.Literal; import dev.enola.thing.PredicatesObjects; @@ -59,6 +60,7 @@ public MutablePredicatesObjects(int expectedSize) { public Builder2 set(String predicateIRI, Object value) { if (value == null) return this; if (value instanceof String string && string.isEmpty()) return this; + if (value instanceof Iterable iterable && Iterables.isEmpty(iterable)) return this; if (value instanceof Literal literal) set(predicateIRI, literal.value(), literal.datatypeIRI()); else properties.put(predicateIRI, value); @@ -69,6 +71,7 @@ public Builder2 set(String predicateIRI, Object value) { public Builder2 set(String predicateIRI, Object value, @Nullable String datatypeIRI) { if (value == null) return this; if (value instanceof String string && string.isEmpty()) return this; + if (value instanceof Iterable iterable && Iterables.isEmpty(iterable)) return this; if (datatypeIRI != null) { if (value instanceof Literal) throw new IllegalArgumentException("Cannot set Literal AND Datatype"); @@ -96,6 +99,22 @@ public Builder2 add(String predicateIRI, T value) { return this; } + @Override + public Builder2 addAll(String predicateIRI, Iterable value) { + if (value == null) return this; + var object = properties.get(predicateIRI); + if (object == null) { + var builder = ImmutableSet.builder(); + properties.put(predicateIRI, builder); + builder.addAll(value); + } else if (object instanceof ImmutableSet.Builder builder) { + builder.addAll(value); + } else + throw new IllegalStateException( + predicateIRI + " is not an ImmutableSet.Builder: " + object); + return this; + } + @Override @SuppressWarnings({"rawtypes", "unchecked"}) public Builder2 addOrdered(String predicateIRI, T value) { @@ -123,6 +142,15 @@ public Builder2 add(String predicateIRI, T value, @Nullable String dataty return this; } + @Override + public Builder2 addAll( + String predicateIRI, Iterable value, @Nullable String datatypeIRI) { + if (value == null) return this; + checkCollectionDatatype(predicateIRI, datatypeIRI); + addAll(predicateIRI, value); + return this; + } + @Override public Builder2 addOrdered(String predicateIRI, T value, @Nullable String datatypeIRI) { if (value == null) return this; @@ -137,7 +165,7 @@ private void checkCollectionDatatype(String predicateIRI, @Nullable String datat // ... but this is intentional and matches intended strongly type safe generated code. if (datatypeIRI != null) { var previous = datatypes.putIfAbsent(predicateIRI, datatypeIRI); - if (!datatypeIRI.equals(previous)) + if (previous != null && !datatypeIRI.equals(previous)) throw new IllegalStateException( predicateIRI + " has another Datatype: " + previous); } diff --git a/java/dev/enola/thing/impl/MutableThing.java b/java/dev/enola/thing/impl/MutableThing.java index b24ffb70..1b8b060c 100644 --- a/java/dev/enola/thing/impl/MutableThing.java +++ b/java/dev/enola/thing/impl/MutableThing.java @@ -99,12 +99,25 @@ public Thing.Builder2 add(String predicateIRI, T value) { return this; } + @Override + public Thing.Builder2 addAll(String predicateIRI, Iterable value) { + super.addAll(predicateIRI, value); + return this; + } + @Override public Thing.Builder2 add(String predicateIRI, T value, @Nullable String datatypeIRI) { super.add(predicateIRI, value, datatypeIRI); return this; } + @Override + public Thing.Builder2 addAll( + String predicateIRI, Iterable value, @Nullable String datatypeIRI) { + super.addAll(predicateIRI, value, datatypeIRI); + return this; + } + @Override public Thing.Builder2 addOrdered(String predicateIRI, T value) { super.addOrdered(predicateIRI, value); diff --git a/java/dev/enola/thing/impl/MutableThingTest.java b/java/dev/enola/thing/impl/MutableThingTest.java index 4265aa5a..6b36b920 100644 --- a/java/dev/enola/thing/impl/MutableThingTest.java +++ b/java/dev/enola/thing/impl/MutableThingTest.java @@ -19,12 +19,15 @@ import static com.google.common.truth.Truth.assertThat; +import dev.enola.thing.KIRI; import dev.enola.thing.Thing; import dev.enola.thing.ThingTester; import dev.enola.thing.java2.TBF; import org.junit.Test; +import java.util.List; + public class MutableThingTest extends ThingTester { @Override @@ -46,7 +49,9 @@ public > B create( }; } - @Test // TODO Once ImmutableThing.Builder extends (or is) Builder2, move this up to ThingTester + // TODO Once ImmutableThing.Builder extends (or is) Builder2, move this up to ThingTester + + @Test @SuppressWarnings({"rawtypes", "unchecked"}) public void add() { thingBuilder.iri(THING_IRI); @@ -58,4 +63,25 @@ public void add() { assertThat(thing.isIterable(PREDICATE_IRI)).isTrue(); assertThat(thing.isOrdered(PREDICATE_IRI)).isFalse(); } + + @Test + public void addAll() { + thingBuilder.iri(THING_IRI); + var thingBuilder2 = (Thing.Builder2) thingBuilder; + thingBuilder2.addAll(PREDICATE_IRI, List.of("a", "b")); + var thing = thingBuilder2.build(); + assertThat(thing.get(PREDICATE_IRI, Iterable.class)).containsExactly("a", "b"); + } + + @Test + public void addAllWithDatatype() { + thingBuilder.iri(THING_IRI); + var thingBuilder2 = (Thing.Builder2) thingBuilder; + thingBuilder2.addAll( + PREDICATE_IRI, List.of("https://vorburger.ch"), KIRI.SCHEMA.URL_DATATYPE); + var thing = thingBuilder2.build(); + assertThat(thing.get(PREDICATE_IRI, Iterable.class)) + .containsExactly("https://vorburger.ch"); + assertThat(thing.datatype(PREDICATE_IRI)).isEqualTo(KIRI.SCHEMA.URL_DATATYPE); + } } diff --git a/mkdocs.yaml b/mkdocs.yaml index d1a1f262..cda6bf57 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -58,7 +58,8 @@ nav: - Graph: concepts/graph.md - Timeline: concepts/timeline.md - Enola: - - MIME: models/enola.dev/mediaType/graph.gv.svg + - MIME Simple: models/enola.dev/mediaType/graph.gv.svg + - MIME Full: models/enola.dev/mediaType/graphviz.gv.svg - Concepts: - Core: concepts/core.md - Architecture Diagrams: concepts/core-arch.md diff --git a/models/build.bash b/models/build.bash index d25fc893..5f7cc7a0 100755 --- a/models/build.bash +++ b/models/build.bash @@ -47,6 +47,6 @@ dot -Tsvg -O docs/models/graphviz.gv # NB: --no-file-loader only marginally helps to make the picture clearer; what we need is real sparql: query support, to filter! # TODO Merge these two (once it works & looks well enough); this might need adding support for multiple repeating --load arguments... ./enola rosetta --no-file-loader --in models/enola.dev/mediaTypes.ttl --out=docs/models/enola.dev/mediaType/graph.gv && dot -Tsvg -O docs/models/enola.dev/mediaType/graph.gv -./enola -v gen graphviz --load=enola:TikaMediaTypes --output docs/models/enola.dev/mediaType/ && dot -Tsvg -O docs/models/enola.dev/mediaType/graphviz.gv +./enola -v gen graphviz --no-file-loader --load=enola:TikaMediaTypes --output docs/models/enola.dev/mediaType/ && dot -Tsvg -O docs/models/enola.dev/mediaType/graphviz.gv # TODO RDF* --load="models/**.ttl[s?]"