diff --git a/java/dev/enola/cli/CommandWithModel.java b/java/dev/enola/cli/CommandWithModel.java index f12eebf15..6af3706e4 100644 --- a/java/dev/enola/cli/CommandWithModel.java +++ b/java/dev/enola/cli/CommandWithModel.java @@ -35,7 +35,6 @@ import dev.enola.datatype.DatatypeRepositoryBuilder; import dev.enola.rdf.RdfResourceIntoThingConverter; import dev.enola.thing.ThingMetadataProvider; -import dev.enola.thing.impl.ImmutableThing; import dev.enola.thing.io.Loader; import dev.enola.thing.io.ResourceIntoThingConverter; import dev.enola.thing.message.ThingProviderAdapter; @@ -91,8 +90,7 @@ public final void run() throws Exception { // TODO Replace DatatypeRepository with store itself, once a Datatype is a Thing DatatypeRepository dtr = new DatatypeRepositoryBuilder().build(); ThingMemoryRepositoryROBuilder store = new ThingMemoryRepositoryROBuilder(); - ResourceIntoThingConverter ritc = - new RdfResourceIntoThingConverter(dtr, ImmutableThing::builder); + ResourceIntoThingConverter ritc = new RdfResourceIntoThingConverter(dtr); var loader = new Loader(ritc); var fgrp = new GlobResourceProviders(); for (var globIRI : group.load) { diff --git a/java/dev/enola/common/collect/BUILD b/java/dev/enola/common/collect/BUILD new file mode 100644 index 000000000..85b49d3e1 --- /dev/null +++ b/java/dev/enola/common/collect/BUILD @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2023 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. + +load("@rules_java//java:defs.bzl", "java_library") + +java_library( + name = "collect", + srcs = glob( + ["*.java"], + exclude = ["*Test.java"], + ), + visibility = ["//:__subpackages__"], + deps = [ + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) diff --git a/java/dev/enola/common/MoreIterables.java b/java/dev/enola/common/collect/MoreIterables.java similarity index 93% rename from java/dev/enola/common/MoreIterables.java rename to java/dev/enola/common/collect/MoreIterables.java index de02e3864..31c47a0cc 100644 --- a/java/dev/enola/common/MoreIterables.java +++ b/java/dev/enola/common/collect/MoreIterables.java @@ -15,12 +15,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dev.enola.common; +package dev.enola.common.collect; import java.util.Collection; import java.util.OptionalInt; -// TODO Move this into package dev.enola.common.collect public final class MoreIterables { /** diff --git a/java/dev/enola/common/collect/MoreIterators.java b/java/dev/enola/common/collect/MoreIterators.java new file mode 100644 index 000000000..22a4ad3a4 --- /dev/null +++ b/java/dev/enola/common/collect/MoreIterators.java @@ -0,0 +1,29 @@ +/* + * 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.common.collect; + +import java.util.Iterator; + +public final class MoreIterators { + + public static Iterable toIterable(Iterator iterator) { + return () -> iterator; + } + + private MoreIterators() {} +} diff --git a/java/dev/enola/common/collect/package-info.java b/java/dev/enola/common/collect/package-info.java new file mode 100644 index 000000000..38516c9dd --- /dev/null +++ b/java/dev/enola/common/collect/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ +@NullMarked +package dev.enola.common.collect; + +import org.jspecify.annotations.NullMarked; diff --git a/java/dev/enola/common/context/Context.java b/java/dev/enola/common/context/Context.java index 9fc1292d5..9cca6dc77 100644 --- a/java/dev/enola/common/context/Context.java +++ b/java/dev/enola/common/context/Context.java @@ -58,7 +58,8 @@ public Context() { * implementations; in practice, at least the key often IS actually simply a {@link String} (but * it technically does not necessarily have to be). * - * @param key Key, which must implement {@link #equals(Object)} correctly. + * @param key Key, which must implement {@link #equals(Object)} correctly. By convention, often + * a String formatted like getClass().getName() + "#METHOD/PARAMETER". * @param value Value to associate with the key. * @return this, for chaining. */ @@ -68,7 +69,7 @@ public Context push(Object key, Object value) { return this; } - /** Get the value for the given key from this or its parent context. */ + /** Get the value for the given key, from this or its parent context. May be wnull. */ public @Nullable Object get(Object key) { check(); var current = last; @@ -82,6 +83,31 @@ public Context push(Object key, Object value) { else return null; } + public Context push(Class key, T value) { + push(key, (Object) value); // NOT .getName() + return this; + } + + /** + * Gets the instance of Class, from this or its parent context. Never null, may throw + * IllegalStateException. + */ + @SuppressWarnings("unchecked") + public T get(Class klass) { + var key = klass; // NOT .getName(); + if (isEmpty()) throw new IllegalStateException("Context is empty"); + var object = get((Object) key); + if (object == null) + throw new IllegalStateException("Context has no " + key + "; only:\n" + toString(" ")); + if (klass.isInstance(object)) return (T) object; + throw new IllegalStateException( + "Context's " + key + " is a " + object.getClass() + " instead of " + klass); + } + + private boolean isEmpty() { + return last == null && parent == null; + } + // Nota bene: This (kind of) Stack-like data structure (intentionally) // does not have (need) any pop() ("goes the weasel”) kind of method! @@ -102,7 +128,7 @@ void append(Appendable a, String indent) { } } - String toString(String indent) { + private String toString(String indent) { var sb = new StringBuilder(); append(sb, indent); return sb.toString(); diff --git a/java/dev/enola/common/context/TLC.java b/java/dev/enola/common/context/TLC.java index d1ce0e744..8481dfa41 100644 --- a/java/dev/enola/common/context/TLC.java +++ b/java/dev/enola/common/context/TLC.java @@ -51,11 +51,20 @@ public static Context open() { /** See {@link dev.enola.common.context.Context#get(java.lang.Object). */ public static @Nullable Object get(Object key) { + return context().get(key); + } + + /** See {@link dev.enola.common.context.Context#get(java.lang.Class). */ + public static T get(Class klass) { + return context().get(klass); + } + + private static Context context() { var tlc = threadLocalContext.get(); if (tlc == null) { - throw new IllegalStateException("open() first!"); + throw new IllegalStateException("Missing TLC.open() in call chain!"); } - return tlc.get(key); + return tlc; } /* package-local, always keep; never make public! */ diff --git a/java/dev/enola/common/convert/BUILD b/java/dev/enola/common/convert/BUILD index e1e7b4de3..77f8daafe 100644 --- a/java/dev/enola/common/convert/BUILD +++ b/java/dev/enola/common/convert/BUILD @@ -26,6 +26,7 @@ java_library( visibility = ["//:__subpackages__"], deps = [ "//java/dev/enola/common", + "//java/dev/enola/common/context", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:org_jspecify_jspecify", diff --git a/java/dev/enola/common/convert/ConversionException.java b/java/dev/enola/common/convert/ConversionException.java index 0ef8c8e87..db9dd0010 100644 --- a/java/dev/enola/common/convert/ConversionException.java +++ b/java/dev/enola/common/convert/ConversionException.java @@ -18,6 +18,7 @@ package dev.enola.common.convert; import dev.enola.common.URILineColumnMessage; +import dev.enola.common.context.ContextualizedRuntimeException; import java.net.URI; @@ -25,7 +26,7 @@ * Failures encountered by {@link ConverterInto}, {@link Converter} and {@link BiConverter} * implementations. */ -public class ConversionException extends RuntimeException { +public class ConversionException extends ContextualizedRuntimeException { // TODO Re-try making this extends Exception instead Runtime, now that I have 👟 MoreStreams public ConversionException(String message, URI uri, long line, long column, Throwable cause) { diff --git a/java/dev/enola/common/function/MoreStreams.java b/java/dev/enola/common/function/MoreStreams.java index 751b95635..fd1f0f04e 100644 --- a/java/dev/enola/common/function/MoreStreams.java +++ b/java/dev/enola/common/function/MoreStreams.java @@ -24,10 +24,16 @@ /** Static utility methods related to {@code Stream} instances. {@link Streams} has more. */ public final class MoreStreams { + // TODO Move to package dev.enola.common.collect (but must figure out Sneaker relationship) + // TODO Eventually adopting one of (but which?) real reactive frameworks in Enola overall // and rm this may be better? (That would likely be better than doing something such as e.g. // https://stackoverflow.com/questions/30117134/aggregate-runtime-exceptions-in-java-8-streams) + public static Iterable toIterable(Stream stream) { + return stream::iterator; + } + // While waiting for e.g. something like https://bugs.openjdk.org/browse/JDK-8148917 public static void forEach( Stream stream, CheckedConsumer action) throws E { diff --git a/java/dev/enola/rdf/BUILD b/java/dev/enola/rdf/BUILD index 7e2fcba57..c51c2be3f 100644 --- a/java/dev/enola/rdf/BUILD +++ b/java/dev/enola/rdf/BUILD @@ -29,6 +29,7 @@ java_library( plugins = ["//tools/bazel/java_plugin:autoservice"], visibility = ["//:__subpackages__"], deps = [ + "//java/dev/enola/common/context", "//java/dev/enola/common/convert", "//java/dev/enola/common/io", "//java/dev/enola/common/protobuf", @@ -58,11 +59,13 @@ junit_tests( srcs_utils = glob(["*Subject.java"]), deps = [ ":rdf", + "//java/dev/enola/common/context", "//java/dev/enola/common/convert", "//java/dev/enola/common/io", "//java/dev/enola/common/protobuf", "//java/dev/enola/common/protobuf:test_java_proto", "//java/dev/enola/common/yamljson", + "//java/dev/enola/datatype", "//java/dev/enola/thing:thing_java", "//java/dev/enola/thing:thing_java_proto", "//test", diff --git a/java/dev/enola/rdf/RdfResourceIntoThingConverter.java b/java/dev/enola/rdf/RdfResourceIntoThingConverter.java index 9eb8ce897..534fabbe7 100644 --- a/java/dev/enola/rdf/RdfResourceIntoThingConverter.java +++ b/java/dev/enola/rdf/RdfResourceIntoThingConverter.java @@ -17,8 +17,10 @@ */ package dev.enola.rdf; +import com.google.auto.service.AutoService; import com.google.common.collect.ImmutableList; +import dev.enola.common.context.TLC; import dev.enola.common.convert.ConversionException; import dev.enola.common.io.resource.ReadableResource; import dev.enola.datatype.DatatypeRepository; @@ -35,6 +37,8 @@ import java.util.Optional; import java.util.function.Supplier; +@SuppressWarnings("rawtypes") +@AutoService(ResourceIntoThingConverter.class) public class RdfResourceIntoThingConverter implements ResourceIntoThingConverter { @@ -56,9 +60,14 @@ public RdfResourceIntoThingConverter( @SuppressWarnings("unchecked") public RdfResourceIntoThingConverter(DatatypeRepository datatypeRepository) { + // TODO Instead ImmutableThing.builder() it should look-up GenJavaThing subclass, if any this(datatypeRepository, () -> (Builder) ImmutableThing.builder()); } + public RdfResourceIntoThingConverter() { + this(TLC.get(DatatypeRepository.class)); + } + @Override public Optional>> convert(ReadableResource input) throws ConversionException { var optProtoList = rdfResourceIntoProtoThingConverter.convert(input); diff --git a/java/dev/enola/rdf/RdfResourceIntoThingConverterTest.java b/java/dev/enola/rdf/RdfResourceIntoThingConverterTest.java new file mode 100644 index 000000000..2d072f79a --- /dev/null +++ b/java/dev/enola/rdf/RdfResourceIntoThingConverterTest.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.rdf; + +import static com.google.common.truth.Truth.assertThat; + +import dev.enola.common.context.TLC; +import dev.enola.common.io.resource.ClasspathResource; +import dev.enola.datatype.DatatypeRepository; +import dev.enola.datatype.DatatypeRepositoryBuilder; +import dev.enola.thing.Thing; +import dev.enola.thing.io.ResourceIntoThingConverters; + +import org.junit.Test; + +public class RdfResourceIntoThingConverterTest { + + @Test + public void convert() { + try (var ctx = TLC.open()) { + ctx.push(DatatypeRepository.class, new DatatypeRepositoryBuilder().build()); + ResourceIntoThingConverters c = new ResourceIntoThingConverters<>(); + var thing = c.convert(new ClasspathResource("picasso.ttl")).getFirst().build(); + assertThat(thing.iri()).isEqualTo("http://example.enola.dev/Dalí"); + } + } +} diff --git a/java/dev/enola/thing/BUILD b/java/dev/enola/thing/BUILD index cdbd94647..b75469f68 100644 --- a/java/dev/enola/thing/BUILD +++ b/java/dev/enola/thing/BUILD @@ -60,6 +60,7 @@ java_proto_library( java_library( name = "thing_java", srcs = glob( + # TODO Separate BUILD for each sub-package ["**/*.java"], exclude = [ "**/*Test*.java", @@ -71,6 +72,8 @@ java_library( deps = [ ":thing_java_proto", "//java/dev/enola/common", + "//java/dev/enola/common/collect", + "//java/dev/enola/common/context", "//java/dev/enola/common/convert", "//java/dev/enola/common/function", "//java/dev/enola/common/io", diff --git a/java/dev/enola/thing/io/ResourceIntoThingConverters.java b/java/dev/enola/thing/io/ResourceIntoThingConverters.java new file mode 100644 index 000000000..9ec25f514 --- /dev/null +++ b/java/dev/enola/thing/io/ResourceIntoThingConverters.java @@ -0,0 +1,62 @@ +/* + * 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.io; + +import com.google.common.collect.ImmutableList; + +import dev.enola.common.context.TLC; +import dev.enola.common.convert.ConversionException; +import dev.enola.common.convert.Converter; +import dev.enola.common.io.resource.ReadableResource; +import dev.enola.thing.Thing; + +import java.util.List; +import java.util.ServiceLoader; +import java.util.stream.Stream; + +public class ResourceIntoThingConverters + implements Converter>> { + + private final ImmutableList> converters; + + public ResourceIntoThingConverters(Iterable> converters) { + this.converters = ImmutableList.copyOf(converters); + } + + public ResourceIntoThingConverters() { + this(ServiceLoader.load(ResourceIntoThingConverter.class).stream()); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private ResourceIntoThingConverters( + Stream> providers) { + this(providers.map(p -> (ResourceIntoThingConverter) p.get()).toList()); + } + + public List> convert(ReadableResource input) throws ConversionException { + try (var ctx = TLC.open()) { + ctx.push(getClass().getName() + "#convert/input", input); + for (var converter : converters) { + var opt = converter.convert(input); + if (opt.isPresent()) return opt.get(); + } + } + throw new ConversionException( + "None of the registered Thing converters could read: " + input); + } +} diff --git a/java/dev/enola/thing/template/TemplateThingRepository.java b/java/dev/enola/thing/template/TemplateThingRepository.java index 243e42700..4e4953a17 100644 --- a/java/dev/enola/thing/template/TemplateThingRepository.java +++ b/java/dev/enola/thing/template/TemplateThingRepository.java @@ -23,7 +23,7 @@ import com.google.common.base.Function; import com.google.common.collect.Iterables; -import dev.enola.common.MoreIterables; +import dev.enola.common.collect.MoreIterables; import dev.enola.common.io.iri.template.URITemplateMatcherChain; import dev.enola.common.io.iri.template.VariableMaps; import dev.enola.thing.*; @@ -37,7 +37,7 @@ public class TemplateThingRepository implements ThingRepository, TemplateService { - private final record Match(String iriTemplate, Function, Thing> function) {} + private record Match(String iriTemplate, Function, Thing> function) {} private final ThingRepository delegate; private final URITemplateMatcherChain iriTemplateChain;