From 8df8c86c59a4d09ec440fbeafc8d6d49cade0db7 Mon Sep 17 00:00:00 2001 From: shawnhatch Date: Sat, 17 May 2025 15:53:55 -0400 Subject: [PATCH] Fixed issue with the dimension context not properly handling multiple instances of plugin datas or builders. --- .../simulation/nucleus/DimensionContext.java | 185 +++++++++++--- .../gcm/simulation/nucleus/NucleusError.java | 1 + .../nucleus/AT_DimesionContext.java | 231 ++++++++++++++++-- 3 files changed, 366 insertions(+), 51 deletions(-) diff --git a/simulation/src/main/java/gov/hhs/aspr/ms/gcm/simulation/nucleus/DimensionContext.java b/simulation/src/main/java/gov/hhs/aspr/ms/gcm/simulation/nucleus/DimensionContext.java index 3b10e7aa5..628932fb5 100644 --- a/simulation/src/main/java/gov/hhs/aspr/ms/gcm/simulation/nucleus/DimensionContext.java +++ b/simulation/src/main/java/gov/hhs/aspr/ms/gcm/simulation/nucleus/DimensionContext.java @@ -13,10 +13,10 @@ */ public final class DimensionContext implements PluginDataBuilderContainer { - private Map, PluginDataBuilder> pluginDataBuilderBaseMap = new LinkedHashMap<>(); - private Map, PluginDataBuilder> pluginDataBuilderWorkingMap = new LinkedHashMap<>(); - private Map, PluginData> pluginDataBaseMap = new LinkedHashMap<>(); - private Map, PluginData> pluginDataWorkingMap = new LinkedHashMap<>(); + private Map, List> pluginDataBuilderBaseMap = new LinkedHashMap<>(); + private Map, List> pluginDataBuilderWorkingMap = new LinkedHashMap<>(); + private Map, List> pluginDataBaseMap = new LinkedHashMap<>(); + private Map, List> pluginDataWorkingMap = new LinkedHashMap<>(); private DimensionContext() { } @@ -28,8 +28,8 @@ public static class Builder { private Builder() { } - private Map, PluginDataBuilder> pluginDataBuilderMap = new LinkedHashMap<>(); - private Map, PluginData> pluginDataMap = new LinkedHashMap<>(); + private Map, List> pluginDataBuilderMap = new LinkedHashMap<>(); + private Map, List> pluginDataMap = new LinkedHashMap<>(); /** * Returns the DimensionContext instance composed from the inputs to this @@ -43,8 +43,8 @@ public DimensionContext build() { } /** - * Given a plugin Data, will add it and its clone builder to the internal map in - * this class + * Adds the plugin data to the context and returns the plugin data builder + * generated from the plugin data. * * @throws ContractException {@linkplain NucleusError#NULL_PLUGIN_DATA} if the * plugin data builder is null @@ -53,10 +53,20 @@ public PluginDataBuilder add(T t) { if (t == null) { throw new ContractException(NucleusError.NULL_PLUGIN_DATA); } - pluginDataMap.put(t.getClass(), t); - PluginDataBuilder builder = t.toBuilder(); + List pluginDatas = pluginDataMap.get(t.getClass()); + if (pluginDatas == null) { + pluginDatas = new ArrayList<>(); + pluginDataMap.put(t.getClass(), pluginDatas); + } + pluginDatas.add(t); - pluginDataBuilderMap.put(builder.getClass(), builder); + PluginDataBuilder builder = t.toBuilder(); + List pluginDataBuilders = pluginDataBuilderMap.get(builder.getClass()); + if (pluginDataBuilders == null) { + pluginDataBuilders = new ArrayList<>(); + pluginDataBuilderMap.put(builder.getClass(), pluginDataBuilders); + } + pluginDataBuilders.add(builder); return builder; } } @@ -68,36 +78,93 @@ public static Builder builder() { return new Builder(); } + /** + * Returns the stored plugin data builder matching the given class reference. + * + * @throws ContractException + *
    + *
  • {@linkplain NucleusError#NULL_PLUGIN_DATA_BUILDER_CLASS} + * if the class reference is null
  • + *
  • {@linkplain NucleusError#AMBIGUOUS_PLUGIN_DATA_BUILDER_CLASS} + * if more than one plugin data builder matches the + * given class reference
  • + *
  • {@linkplain NucleusError#UNKNOWN_PLUGIN_DATA_BUILDER_CLASS} + * if no plugin data builder matches the given class + * reference
  • + *
+ */ @Override public T getPluginDataBuilder(Class classRef) { + if (classRef == null) { + throw new ContractException(NucleusError.NULL_PLUGIN_DATA_BUILDER_CLASS); + } + List pluginDataBuilders = pluginDataBuilderWorkingMap.get(classRef); - PluginDataBuilder pluginDataBuilder = pluginDataBuilderWorkingMap.get(classRef); - if (pluginDataBuilder == null) { - List> candidates = new ArrayList<>(); + if (pluginDataBuilders == null) { + pluginDataBuilders = new ArrayList<>(); for (Class c : pluginDataBuilderBaseMap.keySet()) { if (classRef.isAssignableFrom(c)) { - candidates.add(c); + List list = pluginDataBuilderBaseMap.get(c); + pluginDataBuilders.addAll(list); } } - if (candidates.isEmpty()) { - throw new ContractException(NucleusError.UNKNOWN_PLUGIN_DATA_BUILDER_CLASS); - } - if (candidates.size() > 1) { - throw new ContractException(NucleusError.AMBIGUOUS_PLUGIN_DATA_BUILDER_CLASS); - } - - pluginDataBuilder = pluginDataBuilderBaseMap.get(candidates.get(0)); - pluginDataBuilderWorkingMap.put(classRef, pluginDataBuilder); + pluginDataBuilderWorkingMap.put(classRef, pluginDataBuilders); } + if (pluginDataBuilders.isEmpty()) { + throw new ContractException(NucleusError.UNKNOWN_PLUGIN_DATA_BUILDER_CLASS); + } + if (pluginDataBuilders.size() > 1) { + throw new ContractException(NucleusError.AMBIGUOUS_PLUGIN_DATA_BUILDER_CLASS); + } + PluginDataBuilder pluginDataBuilder = pluginDataBuilders.get(0); return classRef.cast(pluginDataBuilder); } + + /** + * Returns the stored plugin data builders matching the given class reference. + * + * @throws ContractException + *
    + *
  • {@linkplain NucleusError#NULL_PLUGIN_DATA_BUILDER_CLASS} + * if the class reference is null
  • * + *
+ */ + @SuppressWarnings("unchecked") + public List getPluginDataBuilders(Class classRef) { + if (classRef == null) { + throw new ContractException(NucleusError.NULL_PLUGIN_DATA_BUILDER_CLASS); + } + + List result = new ArrayList<>(); + + List pluginDataBuilders = pluginDataBuilderWorkingMap.get(classRef); + + if (pluginDataBuilders == null) { + pluginDataBuilders = new ArrayList<>(); + for (Class c : pluginDataBuilderBaseMap.keySet()) { + if (classRef.isAssignableFrom(c)) { + List list = pluginDataBuilderBaseMap.get(c); + pluginDataBuilders.addAll(list); + } + } + pluginDataBuilderWorkingMap.put(classRef, pluginDataBuilders); + } + + for(PluginDataBuilder pluginDataBuilder : pluginDataBuilders) { + result.add((T)pluginDataBuilder); + } + + return result; + } /** - * Returns the stored item matching the given class reference. + * Returns the stored plugin data matching the given class reference. * * @throws ContractException *
    + *
  • {@linkplain NucleusError#NULL_PLUGIN_DATA_CLASS} + * if the class reference is null
  • *
  • {@linkplain NucleusError#AMBIGUOUS_PLUGIN_DATA_CLASS} * if more than one plugin data matches the given * class reference
  • @@ -108,25 +175,69 @@ public T getPluginDataBuilder(Class classRef) { */ public T getPluginData(Class classRef) { - PluginData pluginData = pluginDataWorkingMap.get(classRef); - if (pluginData == null) { - List> candidates = new ArrayList<>(); + if (classRef == null) { + throw new ContractException(NucleusError.NULL_PLUGIN_DATA_CLASS); + } + + List pluginDatas = pluginDataWorkingMap.get(classRef); + if (pluginDatas == null) { + pluginDatas = new ArrayList<>(); for (Class c : pluginDataBaseMap.keySet()) { if (classRef.isAssignableFrom(c)) { - candidates.add(c); + List list = pluginDataBaseMap.get(c); + pluginDatas.addAll(list); } } - if (candidates.isEmpty()) { - throw new ContractException(NucleusError.UNKNOWN_PLUGIN_DATA_CLASS); - } - if (candidates.size() > 1) { - throw new ContractException(NucleusError.AMBIGUOUS_PLUGIN_DATA_CLASS); - } + pluginDataWorkingMap.put(classRef, pluginDatas); + } - pluginData = pluginDataBaseMap.get(candidates.get(0)); - pluginDataWorkingMap.put(classRef, pluginData); + if (pluginDatas.isEmpty()) { + throw new ContractException(NucleusError.UNKNOWN_PLUGIN_DATA_CLASS); + } + if (pluginDatas.size() > 1) { + throw new ContractException(NucleusError.AMBIGUOUS_PLUGIN_DATA_CLASS); } + PluginData pluginData = pluginDatas.get(0); + return classRef.cast(pluginData); } + + /** + * Returns the stored plugin datas matching the given class reference. + * + * @throws ContractException + *
      + *
    • {@linkplain NucleusError#NULL_PLUGIN_DATA_CLASS} + * if the class reference is null
    • + *
    + */ + @SuppressWarnings("unchecked") + public List getPluginDatas(Class classRef) { + + + if (classRef == null) { + throw new ContractException(NucleusError.NULL_PLUGIN_DATA_CLASS); + } + + List result = new ArrayList<>(); + + List pluginDatas = pluginDataWorkingMap.get(classRef); + if (pluginDatas == null) { + pluginDatas = new ArrayList<>(); + for (Class c : pluginDataBaseMap.keySet()) { + if (classRef.isAssignableFrom(c)) { + List list = pluginDataBaseMap.get(c); + pluginDatas.addAll(list); + } + } + pluginDataWorkingMap.put(classRef, pluginDatas); + } + + for(PluginData pluginData : pluginDatas) { + result.add((T)pluginData); + } + + return result; + } } diff --git a/simulation/src/main/java/gov/hhs/aspr/ms/gcm/simulation/nucleus/NucleusError.java b/simulation/src/main/java/gov/hhs/aspr/ms/gcm/simulation/nucleus/NucleusError.java index 6debb4700..ec0a59fdc 100644 --- a/simulation/src/main/java/gov/hhs/aspr/ms/gcm/simulation/nucleus/NucleusError.java +++ b/simulation/src/main/java/gov/hhs/aspr/ms/gcm/simulation/nucleus/NucleusError.java @@ -57,6 +57,7 @@ public enum NucleusError implements ContractError { NULL_PLUGIN_CONTEXT("Null plugin context"),// NULL_PLUGIN_DATA("Null plugin data"),// NULL_PLUGIN_DATA_CLASS("Null plugin data class"),// + NULL_PLUGIN_DATA_BUILDER_CLASS("Null plugin data builder class"),// NULL_PLUGIN_ID("Null plugin id"),// NULL_PLUGIN_INITIALIZER("Null plugin initializer"),// NULL_PLUGINS("Null collection of plugins"),// diff --git a/simulation/src/test/java/gov/hhs/aspr/ms/gcm/simulation/nucleus/AT_DimesionContext.java b/simulation/src/test/java/gov/hhs/aspr/ms/gcm/simulation/nucleus/AT_DimesionContext.java index acf302803..bade52155 100644 --- a/simulation/src/test/java/gov/hhs/aspr/ms/gcm/simulation/nucleus/AT_DimesionContext.java +++ b/simulation/src/test/java/gov/hhs/aspr/ms/gcm/simulation/nucleus/AT_DimesionContext.java @@ -4,8 +4,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; import org.junit.jupiter.api.Test; @@ -108,6 +110,37 @@ public PluginDataBuilder toBuilder() { } } + private static class PluginData4 implements PluginData { + + private static class Builder implements PluginDataBuilder { + + @Override + public PluginData build() { + return null; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof Builder)) { + return false; + } + + return true; + } + } + + @Override + public PluginDataBuilder toBuilder() { + return new Builder(); + } + } + @Test @UnitTestMethod(target = DimensionContext.class, name = "builder", args = {}) public void testBuilder() { @@ -117,11 +150,12 @@ public void testBuilder() { @Test @UnitTestMethod(target = DimensionContext.class, name = "getPluginDataBuilder", args = { Class.class }) public void testGetPluginDataBuilder() { + // Note that p2 and p4 are both of type2 and that there is no type4 instance PluginData p1 = new PluginData1(); PluginData p2 = new PluginData2(); - PluginData p3 = new PluginData1(); + PluginData p3 = new PluginData3(); PluginData p4 = new PluginData2(); @@ -134,38 +168,127 @@ public void testGetPluginDataBuilder() { DimensionContext dimensionContext = dimensionContextBuilder.build(); + // There should be exactly one type one PluginDataBuilder p = dimensionContext.getPluginDataBuilder(PluginData1.Builder.class); - assertEquals(p3.toBuilder(), p); + assertEquals(p1.toBuilder(), p); - p = dimensionContext.getPluginDataBuilder(PluginData2.Builder.class); - assertEquals(p4.toBuilder(), p); + // There should be exactly one type three + p = dimensionContext.getPluginDataBuilder(PluginData3.Builder.class); + assertEquals(p3.toBuilder(), p); /* - * precondition test : if more than one plugin data builder matches the - * given class reference + * precondition test : if the class reference is null */ ContractException contractException = assertThrows(ContractException.class, + () -> dimensionContext.getPluginDataBuilder(null)); + assertEquals(NucleusError.NULL_PLUGIN_DATA_BUILDER_CLASS, contractException.getErrorType()); + + /* + * precondition test : if more than one plugin data builder matches the given + * class reference + */ + + // there are 4 instances matching PluginData.Builder + contractException = assertThrows(ContractException.class, () -> dimensionContext.getPluginDataBuilder(PluginDataBuilder.class)); assertEquals(NucleusError.AMBIGUOUS_PLUGIN_DATA_BUILDER_CLASS, contractException.getErrorType()); + // there are 2 instances matching PluginData2 + contractException = assertThrows(ContractException.class, + () -> dimensionContext.getPluginDataBuilder(PluginData2.Builder.class)); + assertEquals(NucleusError.AMBIGUOUS_PLUGIN_DATA_BUILDER_CLASS, contractException.getErrorType()); + /* * precondition test : if no plugin data builder matches the given class * reference */ + // there are 0 instances matching PluginData4 contractException = assertThrows(ContractException.class, - () -> dimensionContext.getPluginDataBuilder(PluginData3.Builder.class)); + () -> dimensionContext.getPluginDataBuilder(PluginData4.Builder.class)); assertEquals(NucleusError.UNKNOWN_PLUGIN_DATA_BUILDER_CLASS, contractException.getErrorType()); } + @Test + @UnitTestMethod(target = DimensionContext.class, name = "getPluginDataBuilders", args = { Class.class }) + public void testGetPluginDataBuilders() { + + // Note that p2 and p4 are both of type2 and that there is no type4 instance + PluginData p1 = new PluginData1(); + + PluginData p2 = new PluginData2(); + + PluginData p3 = new PluginData3(); + + PluginData p4 = new PluginData2(); + + DimensionContext.Builder dimensionContextBuilder = DimensionContext.builder(); + + dimensionContextBuilder.add(p1); + dimensionContextBuilder.add(p2); + dimensionContextBuilder.add(p3); + dimensionContextBuilder.add(p4); + + DimensionContext dimensionContext = dimensionContextBuilder.build(); + + /* + * In the assertions that follow, we are guaranteed that two builders of the + * same type will be equal due to the implementation in the local plugin + * classes. + */ + + // There should be exactly one type1 builder + List pluginData1Builders = dimensionContext + .getPluginDataBuilders(PluginData1.Builder.class); + assertNotNull(pluginData1Builders); + assertEquals(1, pluginData1Builders.size()); + assertEquals(p1.toBuilder(), pluginData1Builders.get(0)); + + // There should be exactly one type3 builder + List pluginData3Builders = dimensionContext.getPluginDataBuilders(PluginData3.Builder.class); + assertNotNull(pluginData3Builders); + assertEquals(1, pluginData3Builders.size()); + assertEquals(p3.toBuilder(), pluginData3Builders.get(0)); + + // There should be exactly two type2 builders + List pluginData2Builders = dimensionContext.getPluginDataBuilders(PluginData2.Builder.class); + assertNotNull(pluginData2Builders); + assertEquals(2, pluginData2Builders.size()); + assertTrue(pluginData2Builders.contains(p2.toBuilder())); + assertTrue(pluginData2Builders.contains(p4.toBuilder())); + + // There should be exactly four PluginDatas + List pluginDataBuilders = dimensionContext.getPluginDataBuilders(PluginDataBuilder.class); + assertNotNull(pluginDataBuilders); + assertEquals(4, pluginDataBuilders.size()); + assertTrue(pluginDataBuilders.contains(p1.toBuilder())); + assertTrue(pluginDataBuilders.contains(p2.toBuilder())); + assertTrue(pluginDataBuilders.contains(p3.toBuilder())); + assertTrue(pluginDataBuilders.contains(p4.toBuilder())); + + // There should be exactly zero type4s + List pluginData4Builders = dimensionContext.getPluginDataBuilders(PluginData4.Builder.class); + assertNotNull(pluginData4Builders); + assertEquals(0, pluginData4Builders.size()); + + /* + * precondition test : if the class reference is null + */ + ContractException contractException = assertThrows(ContractException.class, + () -> dimensionContext.getPluginDataBuilders(null)); + assertEquals(NucleusError.NULL_PLUGIN_DATA_BUILDER_CLASS, contractException.getErrorType()); + } + @Test @UnitTestMethod(target = DimensionContext.class, name = "getPluginData", args = { Class.class }) public void testGetPluginData() { + + // Note that p2 and p4 are both of type2 and that there is no type4 instance PluginData p1 = new PluginData1(); PluginData p2 = new PluginData2(); - PluginData p3 = new PluginData1(); + PluginData p3 = new PluginData3(); PluginData p4 = new PluginData2(); @@ -178,30 +301,110 @@ public void testGetPluginData() { DimensionContext dimensionContext = dimensionContextBuilder.build(); + // There should be exactly one type one PluginData p = dimensionContext.getPluginData(PluginData1.class); - assertEquals(p3, p); + assertEquals(p1, p); - p = dimensionContext.getPluginData(PluginData2.class); - assertEquals(p4, p); + // There should be exactly one type three + p = dimensionContext.getPluginData(PluginData3.class); + assertEquals(p3, p); /* - * precondition test : if more than one plugin data builder matches the - * given class reference + * precondition test : if the class reference is null */ ContractException contractException = assertThrows(ContractException.class, + () -> dimensionContext.getPluginData(null)); + assertEquals(NucleusError.NULL_PLUGIN_DATA_CLASS, contractException.getErrorType()); + + /* + * precondition test : if more than one plugin data builder matches the given + * class reference + */ + + // there are 4 instances matching PluginData + contractException = assertThrows(ContractException.class, () -> dimensionContext.getPluginData(PluginData.class)); assertEquals(NucleusError.AMBIGUOUS_PLUGIN_DATA_CLASS, contractException.getErrorType()); + // there are 2 instances matching PluginData2 + contractException = assertThrows(ContractException.class, + () -> dimensionContext.getPluginData(PluginData2.class)); + assertEquals(NucleusError.AMBIGUOUS_PLUGIN_DATA_CLASS, contractException.getErrorType()); + /* * precondition test : if no plugin data builder matches the given class * reference */ + // there are 0 instances matching PluginData4 contractException = assertThrows(ContractException.class, - () -> dimensionContext.getPluginData(PluginData3.class)); + () -> dimensionContext.getPluginData(PluginData4.class)); assertEquals(NucleusError.UNKNOWN_PLUGIN_DATA_CLASS, contractException.getErrorType()); } + @Test + @UnitTestMethod(target = DimensionContext.class, name = "getPluginDatas", args = { Class.class }) + public void testGetPluginDatas() { + + // Note that p2 and p4 are both of type2 and that there is no type4 instance + PluginData p1 = new PluginData1(); + + PluginData p2 = new PluginData2(); + + PluginData p3 = new PluginData3(); + + PluginData p4 = new PluginData2(); + + DimensionContext.Builder dimensionContextBuilder = DimensionContext.builder(); + + dimensionContextBuilder.add(p1); + dimensionContextBuilder.add(p2); + dimensionContextBuilder.add(p3); + dimensionContextBuilder.add(p4); + + DimensionContext dimensionContext = dimensionContextBuilder.build(); + + // There should be exactly one type1s + List pluginData1s = dimensionContext.getPluginDatas(PluginData1.class); + assertNotNull(pluginData1s); + assertEquals(1, pluginData1s.size()); + assertEquals(p1, pluginData1s.get(0)); + + // There should be exactly one type3s + List pluginData3s = dimensionContext.getPluginDatas(PluginData3.class); + assertNotNull(pluginData3s); + assertEquals(1, pluginData3s.size()); + assertEquals(p3, pluginData3s.get(0)); + + // There should be exactly two type2s + List pluginData2s = dimensionContext.getPluginDatas(PluginData2.class); + assertNotNull(pluginData2s); + assertEquals(2, pluginData2s.size()); + assertTrue(pluginData2s.contains(p2)); + assertTrue(pluginData2s.contains(p4)); + + // There should be exactly four PluginDatas + List pluginDatas = dimensionContext.getPluginDatas(PluginData.class); + assertNotNull(pluginDatas); + assertEquals(4, pluginDatas.size()); + assertTrue(pluginDatas.contains(p1)); + assertTrue(pluginDatas.contains(p2)); + assertTrue(pluginDatas.contains(p3)); + assertTrue(pluginDatas.contains(p4)); + + // There should be exactly zero type4s + List pluginData4s = dimensionContext.getPluginDatas(PluginData4.class); + assertNotNull(pluginData4s); + assertEquals(0, pluginData4s.size()); + + /* + * precondition test : if the class reference is null + */ + ContractException contractException = assertThrows(ContractException.class, + () -> dimensionContext.getPluginDatas(null)); + assertEquals(NucleusError.NULL_PLUGIN_DATA_CLASS, contractException.getErrorType()); + } + @Test @UnitTestMethod(target = DimensionContext.Builder.class, name = "add", args = { PluginData.class }) public void testAdd() {