diff --git a/CHANGELOG.md b/CHANGELOG.md
index e126c46ac..fdaa6be84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.4.0] - 2024-09-11
+
+### Changed
+
+- Fix InMemoryBackingStore by preventing updates to underlying store's Map while iterating over it [#2106](https://github.com/microsoftgraph/msgraph-sdk-java/issues/2106)
+- Use concurrent HashMap for In memory backing store registry to avoid race conditions.
+
## [1.3.0] - 2024-08-22
### Changed
diff --git a/components/abstractions/spotBugsExcludeFilter.xml b/components/abstractions/spotBugsExcludeFilter.xml
index c35270c2d..e2dbb38ce 100644
--- a/components/abstractions/spotBugsExcludeFilter.xml
+++ b/components/abstractions/spotBugsExcludeFilter.xml
@@ -52,7 +52,10 @@ xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubu
-
+
+
+
+
diff --git a/components/abstractions/src/main/java/com/microsoft/kiota/store/InMemoryBackingStore.java b/components/abstractions/src/main/java/com/microsoft/kiota/store/InMemoryBackingStore.java
index 37d540755..43d49fb27 100644
--- a/components/abstractions/src/main/java/com/microsoft/kiota/store/InMemoryBackingStore.java
+++ b/components/abstractions/src/main/java/com/microsoft/kiota/store/InMemoryBackingStore.java
@@ -12,6 +12,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
/** In-memory implementation of the backing store. Allows for dirty tracking of changes. */
public class InMemoryBackingStore implements BackingStore {
@@ -48,24 +49,43 @@ public Pair setValue1(B value1) {
private boolean isInitializationCompleted = true;
private boolean returnOnlyChangedValues;
- private final Map> store = new HashMap<>();
+ private final Map> store = new ConcurrentHashMap<>();
private final Map> subscriptionStore =
- new HashMap<>();
+ new ConcurrentHashMap<>();
public void setIsInitializationCompleted(final boolean value) {
this.isInitializationCompleted = value;
+ ensureCollectionPropertiesAreConsistent();
for (final Map.Entry> entry : this.store.entrySet()) {
final Pair wrapper = entry.getValue();
+ final Pair updatedValue = new Pair<>(!value, wrapper.getValue1());
+ entry.setValue(updatedValue);
+
if (wrapper.getValue1() instanceof BackedModel) {
BackedModel backedModel = (BackedModel) wrapper.getValue1();
backedModel
.getBackingStore()
.setIsInitializationCompleted(value); // propagate initialization
}
- ensureCollectionPropertyIsConsistent(
- entry.getKey(), this.store.get(entry.getKey()).getValue1());
- final Pair updatedValue = wrapper.setValue0(!value);
- entry.setValue(updatedValue);
+ if (isCollectionValue(wrapper)) {
+ final Pair, Integer> collectionTuple = (Pair, Integer>) wrapper.getValue1();
+ Object[] items = getObjectArrayFromCollectionWrapper(collectionTuple);
+ final boolean isCollection = collectionTuple.getValue0() instanceof Collection;
+
+ // No need to iterate over collection if first item is not BackedModel
+ if ((isCollection && items.length != 0 && items[0] instanceof BackedModel)
+ || !isCollection) {
+ for (final Object item : items) {
+ if (item instanceof BackedModel) {
+ BackedModel backedModel = (BackedModel) item;
+ backedModel
+ .getBackingStore()
+ .setIsInitializationCompleted(
+ value); // propagate initialization
+ }
+ }
+ }
+ }
}
}
@@ -75,6 +95,30 @@ public boolean getIsInitializationCompleted() {
public void setReturnOnlyChangedValues(final boolean value) {
this.returnOnlyChangedValues = value;
+ // propagate to nested backed models
+ for (final Map.Entry> entry : this.store.entrySet()) {
+ final Pair wrapper = entry.getValue();
+ if (wrapper.getValue1() instanceof BackedModel) {
+ final BackedModel item = (BackedModel) wrapper.getValue1();
+ item.getBackingStore().setReturnOnlyChangedValues(value);
+ }
+ if (isCollectionValue(wrapper)) {
+ final Pair, Integer> collectionTuple = (Pair, Integer>) wrapper.getValue1();
+ Object[] items = getObjectArrayFromCollectionWrapper(collectionTuple);
+ final boolean isCollection = collectionTuple.getValue0() instanceof Collection;
+
+ // No need to iterate over collection if first item is not BackedModel
+ if ((isCollection && items.length != 0 && items[0] instanceof BackedModel)
+ || !isCollection) {
+ for (final Object item : items) {
+ if (item instanceof BackedModel) {
+ BackedModel backedModel = (BackedModel) item;
+ backedModel.getBackingStore().setReturnOnlyChangedValues(value);
+ }
+ }
+ }
+ }
+ }
}
public boolean getReturnOnlyChangedValues() {
@@ -89,10 +133,10 @@ public void clear() {
final Map result = new HashMap<>();
for (final Map.Entry> entry : this.store.entrySet()) {
final Pair wrapper = entry.getValue();
- final Object value = this.getValueFromWrapper(entry.getKey(), wrapper);
+ final Object value = this.get(entry.getKey());
if (value != null) {
- result.put(entry.getKey(), wrapper.getValue1());
+ result.put(entry.getKey(), value);
} else if (Boolean.TRUE.equals(wrapper.getValue0())) {
result.put(entry.getKey(), null);
}
@@ -112,29 +156,37 @@ public void clear() {
return result;
}
- private Object getValueFromWrapper(final String entryKey, final Pair wrapper) {
- if (wrapper != null) {
- final Boolean hasChanged = wrapper.getValue0();
- if (!this.returnOnlyChangedValues || Boolean.TRUE.equals(hasChanged)) {
- if (Boolean.FALSE.equals(
- hasChanged)) { // no need property has already been flagged.
- ensureCollectionPropertyIsConsistent(entryKey, wrapper.getValue1());
- }
- if (wrapper.getValue1() instanceof Pair) {
- Pair, ?> collectionTuple = (Pair, ?>) wrapper.getValue1();
- return collectionTuple.getValue0();
- }
- return wrapper.getValue1();
- }
+ private Object getValueFromWrapper(final Pair wrapper) {
+ if (wrapper == null) {
+ return null;
}
- return null;
+ if (isCollectionValue(wrapper)) {
+ Pair, ?> collectionTuple = (Pair, ?>) wrapper.getValue1();
+ return collectionTuple.getValue0();
+ }
+ return wrapper.getValue1();
}
@SuppressWarnings("unchecked")
@Nullable public T get(@Nonnull final String key) {
Objects.requireNonNull(key);
final Pair wrapper = this.store.get(key);
- final Object value = this.getValueFromWrapper(key, wrapper);
+ if (wrapper == null) {
+ return null;
+ }
+ final Object value = this.getValueFromWrapper(wrapper);
+
+ boolean hasChanged = wrapper.getValue0();
+ if (getReturnOnlyChangedValues() && !hasChanged) {
+ if (isCollectionValue(wrapper)) {
+ ensureCollectionPropertiesAreConsistent();
+ hasChanged = this.store.get(key).getValue0();
+ }
+ if (!hasChanged) {
+ return null;
+ }
+ }
+
try {
return (T) value;
} catch (ClassCastException ex) {
@@ -212,39 +264,68 @@ private void setupNestedSubscriptions(
}
}
- private void ensureCollectionPropertyIsConsistent(final String key, final Object storeItem) {
- if (storeItem instanceof Pair) { // check if we put in a collection annotated with the size
- final Pair, Integer> collectionTuple = (Pair, Integer>) storeItem;
- Object[] items;
- if (collectionTuple.getValue0() instanceof Collection) {
- items = ((Collection