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 collectionTuple = (Pair) 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 collectionTuple = (Pair) 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 collectionTuple = (Pair) storeItem; - Object[] items; - if (collectionTuple.getValue0() instanceof Collection) { - items = ((Collection) collectionTuple.getValue0()).toArray(); - } else { // it is a map - items = ((Map) collectionTuple.getValue0()).values().toArray(); - } + private void ensureCollectionPropertiesAreConsistent() { + final HashMap currentStoreDirtyCollections = new HashMap<>(); + final List nestedBackedModelsToEnumerate = new ArrayList<>(); - for (final Object item : items) { - touchNestedProperties(item); // call get on nested properties + for (final Map.Entry> entry : this.store.entrySet()) { + final Pair wrapper = entry.getValue(); + if (isCollectionValue(wrapper)) { + final Pair collectionTuple = (Pair) 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) { + final BackedModel backedModel = (BackedModel) item; + nestedBackedModelsToEnumerate.add(backedModel); + } + } + } + if (collectionTuple.getValue1() + != items.length) { // and the size has changed since we last updated + currentStoreDirtyCollections.put(entry.getKey(), collectionTuple.getValue0()); + } } + } - if (collectionTuple.getValue1() - != items.length) { // and the size has changed since we last updated - set( - key, - collectionTuple.getValue0()); // ensure the store is notified the collection - // property is "dirty" + // Enumerate nested backed models first since they may trigger the parent to be dirty + for (BackedModel nestedBackedModel : nestedBackedModelsToEnumerate) { + nestedBackedModel.getBackingStore().enumerate(); + } + + // Only update parent properties that haven't been marked as dirty by the nested models + for (final Map.Entry entry : currentStoreDirtyCollections.entrySet()) { + // Always set() if there were no nested models + if (nestedBackedModelsToEnumerate.isEmpty()) { + set(entry.getKey(), entry.getValue()); + continue; + } + boolean hasChanged = this.store.get(entry.getKey()).getValue0(); + if (!hasChanged) { + set(entry.getKey(), entry.getValue()); } } - touchNestedProperties(storeItem); // call get on nested properties } - private void touchNestedProperties(final Object nestedObject) { - if (nestedObject instanceof BackedModel) { - // Call Get<>() on nested properties so that this method may be called recursively to - // ensure collections are consistent - final BackedModel backedModel = (BackedModel) nestedObject; - for (final String itemKey : backedModel.getBackingStore().enumerate().keySet()) { - backedModel.getBackingStore().get(itemKey); - } + private Object[] getObjectArrayFromCollectionWrapper(final Pair collectionTuple) { + if (collectionTuple == null) { + throw new IllegalArgumentException("collectionTuple cannot be null"); + } + if (collectionTuple.getValue0() instanceof Collection) { + final Collection items = (Collection) collectionTuple.getValue0(); + return items.toArray(); + } + if (collectionTuple.getValue0() instanceof Map) { + final Map items = (Map) collectionTuple.getValue0(); + return items.values().toArray(); } + throw new IllegalArgumentException("collectionTuple must be a Collection or a Map"); + } + + private boolean isCollectionValue(final Pair wrapper) { + return wrapper.getValue1() instanceof Pair; } } diff --git a/components/abstractions/src/test/java/com/microsoft/kiota/BaseCollectionPaginationCountResponse.java b/components/abstractions/src/test/java/com/microsoft/kiota/BaseCollectionPaginationCountResponse.java new file mode 100644 index 000000000..39c448691 --- /dev/null +++ b/components/abstractions/src/test/java/com/microsoft/kiota/BaseCollectionPaginationCountResponse.java @@ -0,0 +1,133 @@ +package com.microsoft.kiota; + +import com.microsoft.kiota.serialization.AdditionalDataHolder; +import com.microsoft.kiota.serialization.Parsable; +import com.microsoft.kiota.serialization.ParseNode; +import com.microsoft.kiota.serialization.SerializationWriter; +import com.microsoft.kiota.store.BackedModel; +import com.microsoft.kiota.store.BackingStore; +import com.microsoft.kiota.store.BackingStoreFactorySingleton; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class BaseCollectionPaginationCountResponse + implements AdditionalDataHolder, BackedModel, Parsable { + + /** + * Stores model information. + */ + @jakarta.annotation.Nonnull protected BackingStore backingStore; + + /** + * Instantiates a new {@link BaseCollectionPaginationCountResponse} and sets the default values. + */ + public BaseCollectionPaginationCountResponse() { + this.backingStore = BackingStoreFactorySingleton.instance.createBackingStore(); + this.setAdditionalData(new HashMap<>()); + } + + /** + * Creates a new instance of the appropriate class based on discriminator value + * @param parseNode The parse node to use to read the discriminator value and create the object + * @return a {@link BaseCollectionPaginationCountResponse} + */ + @jakarta.annotation.Nonnull public static BaseCollectionPaginationCountResponse createFromDiscriminatorValue( + @jakarta.annotation.Nonnull final ParseNode parseNode) { + Objects.requireNonNull(parseNode); + return new BaseCollectionPaginationCountResponse(); + } + + /** + * Gets the AdditionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + * @return a {@link Map} + */ + @jakarta.annotation.Nonnull public Map getAdditionalData() { + Map value = this.backingStore.get("additionalData"); + if (value == null) { + value = new HashMap<>(); + this.setAdditionalData(value); + } + return value; + } + + /** + * Gets the backingStore property value. Stores model information. + * @return a {@link BackingStore} + */ + @jakarta.annotation.Nonnull public BackingStore getBackingStore() { + return this.backingStore; + } + + /** + * The deserialization information for the current model + * @return a {@link Map>} + */ + @jakarta.annotation.Nonnull public Map> getFieldDeserializers() { + final HashMap> deserializerMap = + new HashMap>(2); + deserializerMap.put( + "@odata.count", + n -> { + this.setOdataCount(n.getLongValue()); + }); + deserializerMap.put( + "@odata.nextLink", + n -> { + this.setOdataNextLink(n.getStringValue()); + }); + return deserializerMap; + } + + /** + * Gets the @odata.count property value. The OdataCount property + * @return a {@link Long} + */ + @jakarta.annotation.Nullable public Long getOdataCount() { + return this.backingStore.get("odataCount"); + } + + /** + * Gets the @odata.nextLink property value. The OdataNextLink property + * @return a {@link String} + */ + @jakarta.annotation.Nullable public String getOdataNextLink() { + return this.backingStore.get("odataNextLink"); + } + + /** + * Serializes information the current object + * @param writer Serialization writer to use to serialize this model + */ + public void serialize(@jakarta.annotation.Nonnull final SerializationWriter writer) { + Objects.requireNonNull(writer); + writer.writeLongValue("@odata.count", this.getOdataCount()); + writer.writeStringValue("@odata.nextLink", this.getOdataNextLink()); + writer.writeAdditionalData(this.getAdditionalData()); + } + + /** + * Sets the AdditionalData property value. Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + * @param value Value to set for the AdditionalData property. + */ + public void setAdditionalData(@jakarta.annotation.Nullable final Map value) { + this.backingStore.set("additionalData", value); + } + + /** + * Sets the @odata.count property value. The OdataCount property + * @param value Value to set for the @odata.count property. + */ + public void setOdataCount(@jakarta.annotation.Nullable final Long value) { + this.backingStore.set("odataCount", value); + } + + /** + * Sets the @odata.nextLink property value. The OdataNextLink property + * @param value Value to set for the @odata.nextLink property. + */ + public void setOdataNextLink(@jakarta.annotation.Nullable final String value) { + this.backingStore.set("odataNextLink", value); + } +} diff --git a/components/abstractions/src/test/java/com/microsoft/kiota/TestEntity.java b/components/abstractions/src/test/java/com/microsoft/kiota/TestEntity.java index 03d5111c1..68a7d36d6 100644 --- a/components/abstractions/src/test/java/com/microsoft/kiota/TestEntity.java +++ b/components/abstractions/src/test/java/com/microsoft/kiota/TestEntity.java @@ -13,6 +13,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Consumer; public class TestEntity implements Parsable, AdditionalDataHolder, BackedModel { @@ -107,4 +108,10 @@ public void serialize(@Nonnull SerializationWriter writer) { // TODO Auto-generated method stub } + + public static TestEntity createFromDiscriminatorValue( + @jakarta.annotation.Nonnull final ParseNode parseNode) { + Objects.requireNonNull(parseNode); + return new TestEntity(); + } } diff --git a/components/abstractions/src/test/java/com/microsoft/kiota/TestEntityCollectionResponse.java b/components/abstractions/src/test/java/com/microsoft/kiota/TestEntityCollectionResponse.java new file mode 100644 index 000000000..bed02a3e7 --- /dev/null +++ b/components/abstractions/src/test/java/com/microsoft/kiota/TestEntityCollectionResponse.java @@ -0,0 +1,77 @@ +package com.microsoft.kiota; + +import com.microsoft.kiota.serialization.Parsable; +import com.microsoft.kiota.serialization.ParseNode; +import com.microsoft.kiota.serialization.SerializationWriter; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class TestEntityCollectionResponse extends BaseCollectionPaginationCountResponse + implements Parsable { + + /** + * Instantiates a new {@link TestEntityCollectionResponse} and sets the default values. + */ + public TestEntityCollectionResponse() { + super(); + } + + /** + * Creates a new instance of the appropriate class based on discriminator value + * @param parseNode The parse node to use to read the discriminator value and create the object + * @return a {@link TestEntityCollectionResponse} + */ + @jakarta.annotation.Nonnull public static TestEntityCollectionResponse createFromDiscriminatorValue( + @jakarta.annotation.Nonnull final ParseNode parseNode) { + Objects.requireNonNull(parseNode); + return new TestEntityCollectionResponse(); + } + + /** + * The deserialization information for the current model + * @return a {@link Map>} + */ + @Override + @jakarta.annotation.Nonnull public Map> getFieldDeserializers() { + final HashMap> deserializerMap = + new HashMap>( + super.getFieldDeserializers()); + deserializerMap.put( + "value", + n -> { + this.setValue( + n.getCollectionOfObjectValues( + TestEntity::createFromDiscriminatorValue)); + }); + return deserializerMap; + } + + /** + * Gets the value property value. The value property + * @return a {@link java.util.List} + */ + @jakarta.annotation.Nullable public java.util.List getValue() { + return this.backingStore.get("value"); + } + + /** + * Serializes information the current object + * @param writer Serialization writer to use to serialize this model + */ + @Override + public void serialize(@jakarta.annotation.Nonnull final SerializationWriter writer) { + Objects.requireNonNull(writer); + super.serialize(writer); + writer.writeCollectionOfObjectValues("value", this.getValue()); + } + + /** + * Sets the value property value. The value property + * @param value Value to set for the value property. + */ + public void setValue(@jakarta.annotation.Nullable final java.util.List value) { + this.backingStore.set("value", value); + } +} diff --git a/components/abstractions/src/test/java/com/microsoft/kiota/store/InMemoryBackingStoreTest.java b/components/abstractions/src/test/java/com/microsoft/kiota/store/InMemoryBackingStoreTest.java index 081dc0f47..39f97e711 100644 --- a/components/abstractions/src/test/java/com/microsoft/kiota/store/InMemoryBackingStoreTest.java +++ b/components/abstractions/src/test/java/com/microsoft/kiota/store/InMemoryBackingStoreTest.java @@ -3,12 +3,15 @@ import static org.junit.jupiter.api.Assertions.*; import com.microsoft.kiota.TestEntity; +import com.microsoft.kiota.TestEntityCollectionResponse; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; @@ -422,4 +425,164 @@ void TestsBackingStoreEmbeddedWithMultipleNestedModelsCollectionsAndAdditionalDa assertEquals(2, invocationCount.get()); // only for setting the id and the colleagues } + + @Test + void TestsBackingStoreUpdateToItemInNestedCollectionWithAnotherBackedModel() { + // Arrange dummy user with initialized backing store + var colleagues = new ArrayList(); + for (int i = 0; i < 10; i++) { + var colleague = new TestEntity(); + colleague.setId(UUID.randomUUID().toString()); + colleague.setBusinessPhones(Arrays.asList(new String[] {"+1 234 567 891"})); + colleague.getAdditionalData().put("count", i); + colleagues.add(colleague); + colleague.getBackingStore().setIsInitializationCompleted(true); + } + + var testUserCollectionResponse = new TestEntityCollectionResponse(); + testUserCollectionResponse.setValue(colleagues); + // After set(), while adding nested subscriptions, all values in the collection now have + // initializationCompleted=false & their properties are all dirty + testUserCollectionResponse.getBackingStore().setIsInitializationCompleted(true); + + // Act on the data by making a change + var manager = new TestEntity(); + manager.setId("2fe22fe5-1132-42cf-90f9-1dc17e325a74"); + manager.getBackingStore().setIsInitializationCompleted(true); + var collectionValues = testUserCollectionResponse.getValue(); + collectionValues.get(0).setManager(manager); + + // Assert by retrieving only changed values + testUserCollectionResponse.getBackingStore().setReturnOnlyChangedValues(true); + var changedValues = testUserCollectionResponse.getBackingStore().enumerate(); + assertEquals(1, changedValues.size()); + assertEquals("value", changedValues.keySet().toArray()[0]); + assertEquals(10, ((List) changedValues.values().toArray()[0]).size()); + assertTrue( + ((TestEntity) ((List) changedValues.values().toArray()[0]).get(0)) + .getBackingStore() + .enumerate() + .containsKey("manager")); + } + + @Test + void testInitializationCompletedIsPropagatedToMapItems() { + var colleagues = new HashMap(); + for (int i = 0; i < 10; i++) { + var colleague = new TestEntity(); + colleague.setId(UUID.randomUUID().toString()); + colleague.setBusinessPhones(Arrays.asList(new String[] {"+1 234 567 891"})); + colleague.getAdditionalData().put("count", i); + colleagues.put(colleague.getId(), colleague); + colleague.getBackingStore().setIsInitializationCompleted(false); + } + + var testUser = new TestEntity(); + testUser.setId("1"); + testUser.setAdditionalData(colleagues); + + testUser.getBackingStore().setIsInitializationCompleted(true); + for (Map.Entry colleague : testUser.getAdditionalData().entrySet()) { + var backedModel = (BackedModel) colleague.getValue(); + assertTrue(backedModel.getBackingStore().getIsInitializationCompleted()); + } + } + + @Test + void testInitializationCompletedIsPropagatedToCollectionItems() { + var colleagues = new ArrayList(); + for (int i = 0; i < 10; i++) { + var colleague = new TestEntity(); + colleague.setId(UUID.randomUUID().toString()); + colleague.setBusinessPhones(Arrays.asList(new String[] {"+1 234 567 891"})); + colleague.getAdditionalData().put("count", i); + colleagues.add(colleague); + colleague.getBackingStore().setIsInitializationCompleted(false); + } + + var testUser = new TestEntityCollectionResponse(); + testUser.setValue(colleagues); + + testUser.getBackingStore().setIsInitializationCompleted(true); + for (TestEntity colleague : testUser.getValue()) { + assertTrue(colleague.getBackingStore().getIsInitializationCompleted()); + } + } + + @Test + void testCollectionPropertyConsistencyChecksSizeChangesInAllNestedItemsInCollection() { + var colleagues = new ArrayList(); + for (int i = 0; i < 10; i++) { + var colleague = new TestEntity(); + colleague.setId(UUID.randomUUID().toString()); + colleague.setBusinessPhones(Arrays.asList(new String[] {"+1 234 567 891"})); + var manager = new TestEntity(); + manager.setId(UUID.randomUUID().toString()); + colleague.getAdditionalData().put("count", i); + colleague.getAdditionalData().put("random", "randomString"); + colleague.getAdditionalData().put("manager", manager); + colleagues.add(colleague); + colleague.getBackingStore().setIsInitializationCompleted(true); + } + + var testUser = new TestEntity(); + testUser.setId(UUID.randomUUID().toString()); + testUser.setColleagues(colleagues); + testUser.getBackingStore().setIsInitializationCompleted(true); + + testUser.getBackingStore().setReturnOnlyChangedValues(true); + assertEquals(0, testUser.getBackingStore().enumerate().size()); + assertNull(testUser.getColleagues()); // null since value is not dirty + + // Update nested backed model + testUser.getBackingStore().setReturnOnlyChangedValues(false); + testUser.getColleagues().get(9).getAdditionalData().put("moreRandom", 123); + + // collection consistency should loop through all nested backed models in the collection and + // find one with a dirty additional data map + testUser.getBackingStore().setReturnOnlyChangedValues(true); + assertNotNull(testUser.getColleagues()); + var changedValues = testUser.getBackingStore().enumerate(); + assertEquals(1, changedValues.size()); + } + + @Test + void + testCollectionPropertyConsistencyChecksEnumeratesNestedBackedModelsInAllNestedCollections() { + var colleagues = new ArrayList(); + for (int i = 0; i < 10; i++) { + var colleague = new TestEntity(); + colleague.setId(UUID.randomUUID().toString()); + colleague.setBusinessPhones(Arrays.asList(new String[] {"+1 234 567 891"})); + var manager = new TestEntity(); + manager.setId(UUID.randomUUID().toString()); + colleague.getAdditionalData().put("count", i); + colleague.getAdditionalData().put("random", "randomString"); + colleague.getAdditionalData().put("manager", manager); + colleagues.add(colleague); + colleague.getBackingStore().setIsInitializationCompleted(true); + } + + var testUser = new TestEntity(); + testUser.setId(UUID.randomUUID().toString()); + testUser.setColleagues(colleagues); + testUser.getBackingStore().setIsInitializationCompleted(true); + + testUser.getBackingStore().setReturnOnlyChangedValues(true); + assertEquals(0, testUser.getBackingStore().enumerate().size()); + assertNull(testUser.getColleagues()); // null since value is not dirty + + // Update nested backed model + testUser.getBackingStore().setReturnOnlyChangedValues(false); + ((TestEntity) testUser.getColleagues().get(9).getAdditionalData().get("manager")) + .getAdditionalData() + .put("moreRandom", 123); + + // collection consistency should loop through all nested backed models in the collection and + // find one with a dirty additional data map + testUser.getBackingStore().setReturnOnlyChangedValues(true); + var changedValues = testUser.getBackingStore().enumerate(); + assertNotNull(testUser.getColleagues()); + assertEquals(1, changedValues.size()); + } } diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java index 2eb271beb..c8dba6011 100644 --- a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java +++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/middleware/options/UserAgentHandlerOption.java @@ -13,7 +13,7 @@ public UserAgentHandlerOption() {} private boolean enabled = true; @Nonnull private String productName = "kiota-java"; - @Nonnull private String productVersion = "1.3.0"; + @Nonnull private String productVersion = "1.4.0"; /** * Gets the product name to be used in the user agent header diff --git a/gradle.properties b/gradle.properties index ff6dd0baf..434f746a4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,7 +25,7 @@ org.gradle.caching=true mavenGroupId = com.microsoft.kiota mavenMajorVersion = 1 -mavenMinorVersion = 3 +mavenMinorVersion = 4 mavenPatchVersion = 0 mavenArtifactSuffix =