diff --git a/src/main/java/org/warp/commonutils/type/HashAssociation.java b/src/main/java/org/warp/commonutils/type/HashAssociation.java index 2cbc68d..1fd6073 100644 --- a/src/main/java/org/warp/commonutils/type/HashAssociation.java +++ b/src/main/java/org/warp/commonutils/type/HashAssociation.java @@ -136,7 +136,9 @@ public class HashAssociation implements Association { @Override public Set getLinks(T src) { Objects.requireNonNull(src); - return associations.get(src).clone(); + var dests = associations.get(src); + if (dests == null) return Set.of(); + return dests.clone(); } @Override diff --git a/src/main/java/org/warp/commonutils/type/HashMultiAssociation.java b/src/main/java/org/warp/commonutils/type/HashMultiAssociation.java new file mode 100644 index 0000000..f8adc20 --- /dev/null +++ b/src/main/java/org/warp/commonutils/type/HashMultiAssociation.java @@ -0,0 +1,185 @@ +package org.warp.commonutils.type; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import it.unimi.dsi.fastutil.objects.ObjectSet; +import java.util.Objects; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class HashMultiAssociation implements MultiAssociation { + + private final Object2ObjectOpenHashMap> associations; + private final Object2ObjectOpenHashMap> inverseAssociations; + + public HashMultiAssociation() { + this.associations = new Object2ObjectOpenHashMap<>(); + this.inverseAssociations = new Object2ObjectOpenHashMap<>(); + } + + private HashMultiAssociation(Object2ObjectOpenHashMap> associations, + Object2ObjectOpenHashMap> inverseAssociations) { + this.associations = associations; + this.inverseAssociations = inverseAssociations; + } + + /** + * Remove the source from this destination + */ + @Nullable + private boolean removeSourceFromDest(@NotNull U dest, @NotNull T src) { + var sources = inverseAssociations.get(dest); + if (sources != null) { + sources.remove(src); + + // Keep the map clean + if (sources.isEmpty()) { + inverseAssociations.remove(dest); + } + + return true; + } + return false; + } + + /** + * Remove the destination from this source + */ + @Nullable + private boolean removeDestFromSource(@NotNull T src, @NotNull U dest) { + var dests = associations.get(src); + if (dests != null) { + dests.remove(dest); + + // Keep the map clean + if (dests.isEmpty()) { + associations.remove(src); + } + + return true; + } + return false; + } + + @Override + public boolean link(T src, U dest) { + return associations.computeIfAbsent(src, s -> new ObjectOpenHashSet<>()).add(dest); + } + + @Override + public boolean unlink(T src, U dest) { + var removed = removeDestFromSource(src, dest); + + if (removed) { + removeSourceFromDest(dest, src); + return true; + } + + return false; + } + + @Override + public Set unlink(T src) { + var dests = associations.remove(src); + + if (dests == null) { + return Set.of(); + } + + for (U dest : dests) { + removeSourceFromDest(dest, src); + } + return dests; + } + + @Override + public Set unlinkFromSource(U dest) { + var sources = inverseAssociations.remove(dest); + + if (sources == null) { + return Set.of(); + } + + for (T source : sources) { + removeDestFromSource(source, dest); + } + return sources; + } + + @Override + public boolean hasAnyLink(T src) { + var dests = associations.get(src); + if (dests == null) { + return false; + } + return !dests.isEmpty(); + } + + @Override + public boolean hasAnyLinkSource(U dest) { + var sources = inverseAssociations.get(dest); + if (sources == null) { + return false; + } + return !sources.isEmpty(); + } + + @Override + public boolean hasLink(T src, U dest) { + var dests = associations.get(src); + if (dests == null) { + return false; + } + return dests.contains(dest); + } + + @Override + public Set getLinks(T src) { + Objects.requireNonNull(src); + var dests = associations.get(src); + if (dests == null) return Set.of(); + return dests.clone(); + } + + @Override + public Set getLinkSources(U dest) { + Objects.requireNonNull(dest); + var sources = inverseAssociations.get(dest); + if (sources == null) return Set.of(); + return sources.clone(); + } + + @Override + public void clear() { + associations.clear(); + inverseAssociations.clear(); + } + + @Override + public int size() { + return Math.max(associations.size(), inverseAssociations.size()); + } + + @Override + public ObjectSet getSources() { + return associations.clone().keySet(); + } + + @Override + public ObjectSet getDestinations() { + return inverseAssociations.clone().keySet(); + } + + @SuppressWarnings("MethodDoesntCallSuperMethod") + @Override + public HashMultiAssociation clone() { + var associationsClone = associations.clone(); + associationsClone.replaceAll((item, destinations) -> destinations.clone()); + + var inverseAssociationsClone = inverseAssociations.clone(); + inverseAssociationsClone.replaceAll((item, sources) -> sources.clone()); + + return new HashMultiAssociation<>(associationsClone, inverseAssociationsClone); + } +} diff --git a/src/main/java/org/warp/commonutils/type/MultiAssociation.java b/src/main/java/org/warp/commonutils/type/MultiAssociation.java new file mode 100644 index 0000000..d926a05 --- /dev/null +++ b/src/main/java/org/warp/commonutils/type/MultiAssociation.java @@ -0,0 +1,123 @@ +package org.warp.commonutils.type; + +import it.unimi.dsi.fastutil.objects.ObjectSet; +import java.util.Set; + +/** + * One to many relationship + * + * o ------- o + * +--- o + * o ---+--- o + * +-|- o + * o -----|- o + * o ------- o + * o ---+--- o + * +--- o + * + * @param Source type + * @param Destination type + */ +public interface MultiAssociation { + + /** + * Link source to dest + * @param src Source + * @param dest Destination + * @return true if linked, false if it was already linked with that destination + */ + boolean link(T src, U dest); + + /** + * Unlink only if src is linked with dest + * @param src Source + * @param dest Destination + * @return true if unlinked, false if not present + */ + boolean unlink(T src, U dest); + + /** + * Unlink + * @param src Source + * @return previous linked destinations + */ + Set unlink(T src); + + /** + * Unlink + * @param dest Destination + * @return previous linked source + */ + Set unlinkFromSource(U dest); + + /** + * Check if link exists + * @param src Source + * @return true if source is linked with at least 1 destination + */ + default boolean hasAnyLink(T src) { + return !getLinks(src).isEmpty(); + } + + /** + * Check if link exists + * @param dest Destination + * @return true if destination is linked with at least 1 destination + */ + default boolean hasAnyLinkSource(U dest) { + return !getLinkSources(dest).isEmpty(); + } + + /** + * Check if link exists + * @param src Source + * @param dest Destination + * @return true if source and destination are linked together + */ + boolean hasLink(T src, U dest); + + /** + * Get a link destination + * @param src Source + * @return Existing linked destinations + */ + Set getLinks(T src); + + /** + * Get a link source + * @param dest Source + * @return Existing linked sources + */ + Set getLinkSources(U dest); + + /** + * Delete all links + */ + void clear(); + + /** + * Get the count of existing links + * @return size + */ + int size(); + + /** + * Get all the sources + * @return Set of sources + */ + ObjectSet getSources(); + + /** + * Get all the destinations + * @return Set of destinations + */ + ObjectSet getDestinations(); + + static MultiAssociation synchronize(MultiAssociation association) { + return new SynchronizedMultiAssociation<>(association); + } + + static MultiAssociation synchronize(MultiAssociation association, Object lock) { + return new SynchronizedMultiAssociation<>(association, lock); + } +} diff --git a/src/main/java/org/warp/commonutils/type/SynchronizedMultiAssociation.java b/src/main/java/org/warp/commonutils/type/SynchronizedMultiAssociation.java new file mode 100644 index 0000000..a876f92 --- /dev/null +++ b/src/main/java/org/warp/commonutils/type/SynchronizedMultiAssociation.java @@ -0,0 +1,111 @@ +package org.warp.commonutils.type; + +import it.unimi.dsi.fastutil.objects.ObjectSet; +import java.util.Set; + +public class SynchronizedMultiAssociation implements MultiAssociation { + + private final MultiAssociation association; + private final Object lock; + + SynchronizedMultiAssociation(MultiAssociation association) { + this.association = association; + this.lock = new Object(); + } + + SynchronizedMultiAssociation(MultiAssociation association, Object lock) { + this.association = association; + this.lock = lock; + } + + @Override + public boolean link(T src, U dest) { + synchronized (lock) { + return association.link(src, dest); + } + } + + @Override + public boolean unlink(T src, U dest) { + synchronized (lock) { + return association.unlink(src, dest); + } + } + + @Override + public Set unlink(T src) { + synchronized (lock) { + return association.unlink(src); + } + } + + @Override + public Set unlinkFromSource(U dest) { + synchronized (lock) { + return association.unlinkFromSource(dest); + } + } + + @Override + public boolean hasAnyLink(T src) { + synchronized (lock) { + return association.hasAnyLink(src); + } + } + + @Override + public boolean hasAnyLinkSource(U dest) { + synchronized (lock) { + return association.hasAnyLinkSource(dest); + } + } + + @Override + public boolean hasLink(T src, U dest) { + synchronized (lock) { + return association.hasLink(src, dest); + } + } + + @Override + public Set getLinks(T src) { + synchronized (lock) { + return association.getLinks(src); + } + } + + @Override + public Set getLinkSources(U dest) { + synchronized (lock) { + return association.getLinkSources(dest); + } + } + + @Override + public void clear() { + synchronized (lock) { + association.clear(); + } + } + + @Override + public int size() { + synchronized (lock) { + return association.size(); + } + } + + @Override + public ObjectSet getSources() { + synchronized (lock) { + return association.getSources(); + } + } + + @Override + public ObjectSet getDestinations() { + synchronized (lock) { + return association.getDestinations(); + } + } +}