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/collect/Immutables.java b/java/dev/enola/common/collect/Immutables.java new file mode 100644 index 000000000..5313f4ab7 --- /dev/null +++ b/java/dev/enola/common/collect/Immutables.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.common.collect; + +import com.google.common.collect.ImmutableList; + +import java.util.List; + +public final class Immutables { + + public static ImmutableList join(List a, List b) { + var builder = ImmutableList.builderWithExpectedSize(a.size() + b.size()); + return builder.addAll(a).addAll(b).build(); + } + + private Immutables() {} +} diff --git a/java/dev/enola/thing/BUILD b/java/dev/enola/thing/BUILD index cdbd94647..4f9608087 100644 --- a/java/dev/enola/thing/BUILD +++ b/java/dev/enola/thing/BUILD @@ -71,6 +71,7 @@ java_library( deps = [ ":thing_java_proto", "//java/dev/enola/common", + "//java/dev/enola/common/collect", "//java/dev/enola/common/convert", "//java/dev/enola/common/function", "//java/dev/enola/common/io", diff --git a/java/dev/enola/thing/PredicatesObjects.java b/java/dev/enola/thing/PredicatesObjects.java index f0ab38ed4..ac3787c05 100644 --- a/java/dev/enola/thing/PredicatesObjects.java +++ b/java/dev/enola/thing/PredicatesObjects.java @@ -120,8 +120,11 @@ default Optional getOptional(String predicateIRI, Class klass) { // TODO get... other types. + // TODO Remove un-used copy() Builder copy(); + Builder newBuilder(); + @SuppressFBWarnings("NM_SAME_SIMPLE_NAME_AS_INTERFACE") interface Builder // skipcq: JAVA-E0169 extends dev.enola.common.Builder { @@ -129,7 +132,7 @@ interface Builder // skipcq: JAVA-E0169 <@ImmutableTypeParameter T> PredicatesObjects.Builder set(String predicateIRI, T value); <@ImmutableTypeParameter T> PredicatesObjects.Builder set( - String predicateIRI, T value, String datatypeIRI); + String predicateIRI, T value, @Nullable String datatypeIRI); @Override B build(); diff --git a/java/dev/enola/thing/Thing.java b/java/dev/enola/thing/Thing.java index ede1772dd..005facca3 100644 --- a/java/dev/enola/thing/Thing.java +++ b/java/dev/enola/thing/Thing.java @@ -19,6 +19,8 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.jspecify.annotations.Nullable; + /** * Thing is the central data structure of Enola. * @@ -34,17 +36,25 @@ public interface Thing extends PredicatesObjects { String iri(); + // TODO @Override Builder newBuilder(); + @Override - Builder copy(); + Builder copy(); // TODO Remove un-used copy() @SuppressFBWarnings("NM_SAME_SIMPLE_NAME_AS_INTERFACE") interface Builder extends PredicatesObjects.Builder { // skipcq: JAVA-E0169 + default Builder copy(Thing thing) { + iri(thing.iri()); + thing.properties().forEach((p, v) -> set(p, v, thing.datatype(p))); + return this; + } + Builder iri(String iri); Builder set(String predicateIRI, Object value); - Builder set(String predicateIRI, Object value, String datatypeIRI); + Builder set(String predicateIRI, Object value, @Nullable String datatypeIRI); @Override B build(); diff --git a/java/dev/enola/thing/impl/ImmutableThing.java b/java/dev/enola/thing/impl/ImmutableThing.java index 4c02d1b79..48a99424b 100644 --- a/java/dev/enola/thing/impl/ImmutableThing.java +++ b/java/dev/enola/thing/impl/ImmutableThing.java @@ -44,6 +44,15 @@ protected ImmutableThing( this.iri = Objects.requireNonNull(iri, "iri"); } + public static ImmutableThing copyOf(Thing thing) { + if (thing instanceof ImmutableThing immutableThing) return immutableThing; + + return new ImmutableThing( + thing.iri(), + ImmutableMap.copyOf(thing.properties()), + ImmutableMap.copyOf(thing.datatypes())); + } + public static Thing.Builder builder() { return new Builder<>(); } @@ -124,7 +133,8 @@ public Thing.Builder set(String predicateIRI, Object value) { } @Override - public Thing.Builder set(String predicateIRI, Object value, String datatypeIRI) { + public Thing.Builder set( + String predicateIRI, Object value, @Nullable String datatypeIRI) { properties.put(predicateIRI, value); if (datatypeIRI != null) datatypes.put(predicateIRI, datatypeIRI); return this; diff --git a/java/dev/enola/thing/io/Loader.java b/java/dev/enola/thing/io/Loader.java index 1108a2000..72b2adc26 100644 --- a/java/dev/enola/thing/io/Loader.java +++ b/java/dev/enola/thing/io/Loader.java @@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.List; import java.util.stream.Stream; public class Loader implements ConverterInto, Store> { @@ -58,7 +59,7 @@ private void load(ReadableResource resource, Store store) { things.get() .forEach( thingBuilder -> { - thingBuilder.set(KIRI.E.ORIGIN, resource.uri()); + thingBuilder.set(KIRI.E.ORIGIN, List.of(resource.uri())); var thing = thingBuilder.build(); store.merge(thing); }); diff --git a/java/dev/enola/thing/repo/ThingMerger.java b/java/dev/enola/thing/repo/ThingMerger.java index d9dfb3e40..7a45ebec4 100644 --- a/java/dev/enola/thing/repo/ThingMerger.java +++ b/java/dev/enola/thing/repo/ThingMerger.java @@ -17,39 +17,59 @@ */ package dev.enola.thing.repo; +import static dev.enola.common.collect.Immutables.join; + import dev.enola.thing.KIRI; import dev.enola.thing.Thing; +import dev.enola.thing.impl.ImmutableThing; +import dev.enola.thing.impl.MutableThing; + +import java.util.List; class ThingMerger { // TODO Implement missing ThingMergerTest coverage! - public static Thing merge(Thing existing, Thing update) { - if (!existing.iri().equals(update.iri())) throw new IllegalArgumentException(); - - if (existing.predicateIRIs().isEmpty()) return update; - if (update.predicateIRIs().isEmpty()) return existing; - - var merged = existing.copy(); - var properties = update.properties(); - properties.forEach( - (predicate, value) -> { - var old = existing.get(predicate); - if (old == null) merged.set(predicate, value, update.datatype(predicate)); - else if (old.equals(value)) { - // That's fine! - } else if (predicate.equals(KIRI.E.ORIGIN)) { - // TODO Implement merging both into a List, with test coverage! - } else - throw new IllegalStateException( - "Cannot merge " - + predicate - + " of an " - + existing.iri() - + " from " - + existing.getString(KIRI.E.ORIGIN) - + " and " - + update.getString(KIRI.E.ORIGIN)); - }); - return merged.build(); + public static Thing merge(Thing t1, Thing t2) { + if (!t1.iri().equals(t2.iri())) throw new IllegalArgumentException(); + + var t1predis = t1.predicateIRIs(); + var t2predis = t2.predicateIRIs(); + if (t1predis.isEmpty()) return t2; + if (t2predis.isEmpty()) return t1; + + // TODO Use copy() instead of new MutableThing + ImmutableThing.copyOf() + + var merged = new MutableThing<>(t2predis.size()); + t2.properties() + .forEach( + (predicate, t2obj) -> { + var t1obj = t1.get(predicate); + var t1dt = t1.datatype(predicate); + var t2dt = t2.datatype(predicate); + var sameDt = t1dt != null && t1dt.equals(t2dt); + + if (t1obj == null) merged.set(predicate, t2obj, t2dt); + else if (t1obj.equals(t2obj) && sameDt) + merged.set(predicate, t2obj, t2dt); + // skipcq: JAVA-C1003 + else if (t1obj instanceof List t1list && t2obj instanceof List t2list) { + merged.set(predicate, join(t1list, t2list)); + } else + throw new IllegalStateException( + "Cannot merge " + + predicate + + " of an " + + t1.iri() + + " from " + + t1.getString(KIRI.E.ORIGIN) + + " and " + + t2.getString(KIRI.E.ORIGIN)); + }); + + try { + return ImmutableThing.copyOf(merged.build()); + } catch (IllegalArgumentException | NullPointerException e) { + throw new IllegalArgumentException("Cannot merge " + t1.iri(), e); + } } }