Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,22 @@ public enum UpdateStrategy
/**
* Request to update the entity is sent with the HTTP method PATCH and its payload contains the changed fields only.
*/
MODIFY_WITH_PATCH;
MODIFY_WITH_PATCH,

/**
* Request to update the entity is sent with the HTTP method PATCH and its payload contains the changed fields
* including the changes in nested fields.
*
* The request payload contains only the changed fields. Navigation properties are not supported.
*/
MODIFY_WITH_PATCH_RECURSIVE_DELTA,

/**
* Request to update the entity is sent with the HTTP method PATCH and its payload contains the changed fields
* including the changes in nested fields.
*
* The request payload contains the full value of complex fields for changes in any nested field. Navigation
* properties are not supported.
*/
MODIFY_WITH_PATCH_RECURSIVE_FULL;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package com.sap.cloud.sdk.datamodel.odata.helper;

import static com.sap.cloud.sdk.datamodel.odata.helper.ModifyPatchStrategy.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
Expand Down Expand Up @@ -211,28 +212,49 @@ void testUpdateBPatchUpdateNull()
}

@Test
@Disabled( " Test is failing as the getChangedFields() method on Complex Type is not working as expected." )
void testUpdatePatchComplexProperty()
void testUpdatePatchComplexPropertyDelta()
{
final ProductCount count1 = ProductCount.builder().productId(123).quantity(10).build();
final Receipt receipt = Receipt.builder().id(1001).customerId(9001).productCount1(count1).build();

final String expectedSerializedEntity = "{\"ProductCount1\":{\"Quantity\":20}}";

count1.setQuantity(20);

final ODataRequestUpdate receiptUpdate =
FluentHelperFactory
.withServicePath(ODATA_ENDPOINT_URL)
.update(ENTITY_COLLECTION, receipt)
.modifyingEntity(RECURSIVE_DELTA)
.toRequest();

assertThat(receiptUpdate).isNotNull();
assertThat(receiptUpdate.getSerializedEntity()).isEqualTo(expectedSerializedEntity);
}

@Test
void testUpdatePatchComplexPropertyFull()
{
final ProductCount count1 = ProductCount.builder().productId(123).quantity(10).build();
final Receipt receipt = Receipt.builder().id(1001).customerId(9001).productCount1(count1).build();

final String expectedSerializedEntity = "{\"ProductCount1\":{\"Quantity\":\"20\"}}";
final String expectedSerializedEntity = "{\"ProductCount1\":{\"ProductId\":123,\"Quantity\":20}}";

count1.setQuantity(20);

final ODataRequestUpdate receiptUpdate =
FluentHelperFactory
.withServicePath(ODATA_ENDPOINT_URL)
.update(ENTITY_COLLECTION, receipt)
.modifyingEntity()
.modifyingEntity(RECURSIVE_FULL)
.toRequest();

assertThat(receiptUpdate).isNotNull();
assertThat(receiptUpdate.getSerializedEntity()).isEqualTo(expectedSerializedEntity);
}

@Test
@Disabled( " Test is failing as the getChangedFields() method on Complex Type is not working as expected." )
void testIgnoreVersionIdentifier()
{
product.setVersionIdentifier(versionIdentifier);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,23 +144,31 @@ private String getSerializedEntity()
{
final EntityT entity = getEntity();
try {
final List<FieldReference> fieldsToExcludeUpdate =
excludedFields
.stream()
.map(EntitySelectable::getFieldName)
.map(FieldReference::of)
.collect(Collectors.toList());

final List<FieldReference> fieldsToIncludeInUpdate =
includedFields
.stream()
.map(EntitySelectable::getFieldName)
.map(FieldReference::of)
.collect(Collectors.toList());

switch( updateStrategy ) {
case REPLACE_WITH_PUT:
final List<FieldReference> fieldsToExcludeUpdate =
excludedFields
.stream()
.map(EntitySelectable::getFieldName)
.map(FieldReference::of)
.collect(Collectors.toList());
return ODataEntitySerializer.serializeEntityForUpdatePut(entity, fieldsToExcludeUpdate);
case MODIFY_WITH_PATCH:
final List<FieldReference> fieldsToIncludeInUpdate =
includedFields
.stream()
.map(EntitySelectable::getFieldName)
.map(FieldReference::of)
.collect(Collectors.toList());
return ODataEntitySerializer.serializeEntityForUpdatePatch(entity, fieldsToIncludeInUpdate);
return ODataEntitySerializer.serializeEntityForUpdatePatchShallow(entity, fieldsToIncludeInUpdate);
case MODIFY_WITH_PATCH_RECURSIVE_DELTA:
return ODataEntitySerializer
.serializeEntityForUpdatePatchRecursiveDelta(entity, fieldsToIncludeInUpdate);
case MODIFY_WITH_PATCH_RECURSIVE_FULL:
return ODataEntitySerializer
.serializeEntityForUpdatePatchRecursiveFull(entity, fieldsToIncludeInUpdate);
default:
throw new IllegalStateException("Unexpected update strategy:" + updateStrategy);
}
Expand Down Expand Up @@ -193,7 +201,6 @@ private String getSerializedEntity()
*
* @param fields
* The fields to be included in the update execution.
*
* @return The same fluent helper which will include the specified fields in an update request.
*/
@Nonnull
Expand All @@ -212,7 +219,6 @@ public final FluentHelperT includingFields( @Nonnull final EntitySelectable<Enti
*
* @param fields
* The fields to be excluded in the update execution.
*
* @return The same fluent helper which will exclude the specified fields in an update request.
*/
@Nonnull
Expand Down Expand Up @@ -255,4 +261,33 @@ public final FluentHelperT modifyingEntity()
updateStrategy = UpdateStrategy.MODIFY_WITH_PATCH;
return getThis();
}

/**
* Allows to control that the request to update the entity is sent with the HTTP method PATCH and its payload
* contains the changed fields only, with different strategies for handling nested fields.
*
* @param strategy
* The strategy to use for the PATCH update.
* @return The same fluent helper which will modify the entity in the remote system.
* @throws IllegalArgumentException
* If an unknown ModifyPatchStrategy is provided.
*/
@Nonnull
public final FluentHelperT modifyingEntity( @Nonnull final ModifyPatchStrategy strategy )
{
switch( strategy ) {
case SHALLOW:
return modifyingEntity();
case RECURSIVE_DELTA:
updateStrategy = UpdateStrategy.MODIFY_WITH_PATCH_RECURSIVE_DELTA;
break;
case RECURSIVE_FULL:
updateStrategy = UpdateStrategy.MODIFY_WITH_PATCH_RECURSIVE_FULL;
break;
default:
throw new IllegalArgumentException("Unknown ModifyPatchStrategy: " + strategy);
}
return getThis();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.sap.cloud.sdk.datamodel.odata.helper;

/**
* Strategy to determine how a patch operation should be applied to an entity.
*/
public enum ModifyPatchStrategy
{
/** Only the top level fields can be patched */
SHALLOW,

/** All top level and nested fields can be patched, resulting in JSON containing only the changed fields */
RECURSIVE_DELTA,

/**
* All top level and nested fields can be patched, resulting in JSON containing the full value of complex object.
*/
RECURSIVE_FULL
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
Expand Down Expand Up @@ -104,7 +105,7 @@ static String serializeEntityForCreate( @Nonnull final VdmEntity<?> entity )
* @return The serialized JSON string for entity update request.
*/
@Nonnull
static String serializeEntityForUpdatePatch(
static String serializeEntityForUpdatePatchShallow(
@Nonnull final VdmEntity<?> entity,
@Nonnull final Collection<FieldReference> includedFields )
{
Expand All @@ -122,6 +123,143 @@ static String serializeEntityForUpdatePatch(
return GSON_SERIALIZING_NULLS.toJson(partialEntity);
}

/**
* Serializes an entity for update request (PATCH) including changes in nested properties. Allowing null values.
* Resulting JSON contains the full value of complex fields for changing any nested field.
*
* @param entity
* The OData V2 entity reference.
* @param includedFields
* Collection of fields to be included in the update (PATCH) request.
* @return The serialized JSON string for entity update request.
*/
@Nonnull
static String serializeEntityForUpdatePatchRecursiveFull(
@Nonnull final VdmEntity<?> entity,
@Nonnull final Collection<FieldReference> includedFields )
{
final JsonObject fullEntityJson = GSON_SERIALIZING_NULLS.toJsonTree(entity).getAsJsonObject();
final JsonObject patchObject = new JsonObject();

final Set<String> changedFieldNames = new HashSet<>(entity.getChangedFields().keySet());
includedFields.stream().map(FieldReference::getFieldName).forEach(changedFieldNames::add);
changedFieldNames.forEach(key -> patchObject.add(key, fullEntityJson.get(key)));

entity
.toMapOfFields()
.entrySet()
.stream()
.filter(entry -> !patchObject.has(entry.getKey()))
.filter(entry -> entry.getValue() instanceof VdmComplex<?>)
.filter(entry -> containsNestedChangedFields((VdmComplex<?>) entry.getValue()))
.forEach(entry -> patchObject.add(entry.getKey(), fullEntityJson.get(entry.getKey())));

log.debug("The following object is serialized for update : {}.", patchObject);

return GSON_SERIALIZING_NULLS.toJson(patchObject);
}

/**
* Checks if the given complex object contains any changed fields in its nested fields.
*
* @param vdmComplex
* the complex object to check
* @return true if the complex object contains any changed fields, false otherwise
*/
private static boolean containsNestedChangedFields( final VdmComplex<?> vdmComplex )
{
if( !vdmComplex.getChangedFields().isEmpty() ) {
return true;
}

return vdmComplex
.toMapOfFields()
.values()
.stream()
.filter(complexField -> complexField instanceof VdmComplex<?>)
.map(complexField -> (VdmComplex<?>) complexField)
.anyMatch(ODataEntitySerializer::containsNestedChangedFields);
}

/**
* Serializes an entity for update request (PATCH) including changes in nested properties. Allowing null values.
* Resulting JSON contains only the changed fields (including nested changes).
*
* @param entity
* The OData V2 entity reference.
* @param includedFields
* Collection of fields to be included in the update (PATCH) request.
* @return The serialized JSON string for entity update request.
*/
@Nonnull
static String serializeEntityForUpdatePatchRecursiveDelta(
@Nonnull final VdmEntity<?> entity,
@Nonnull final Collection<FieldReference> includedFields )
{
final JsonObject fullEntityJson = GSON_SERIALIZING_NULLS.toJsonTree(entity).getAsJsonObject();
final JsonObject patchObject = new JsonObject();

// Recursively build patch object from changed fields
final JsonObject tempPatchObject = createPatchObjectRecursiveDelta(entity, fullEntityJson);

// Add included fields (from the root only)
includedFields
.stream()
.map(FieldReference::getFieldName)
.forEach(key -> patchObject.add(key, fullEntityJson.get(key)));

// Merge all fields from the tempPatchObject if not already present
tempPatchObject
.entrySet()
.stream()
.filter(entry -> !patchObject.has(entry.getKey()))
.forEach(entry -> patchObject.add(entry.getKey(), entry.getValue()));

log.debug("The following delta object is serialized for update : {}.", patchObject);

return GSON_SERIALIZING_NULLS.toJson(patchObject);
}

/**
* Recursively builds a patch object for a VdmObject by including only changed fields. Complex fields are traversed
* recursively.
*
* @param vdmObject
* the VdmObject (entity or complex) to build the patch from
* @param jsonObject
* the full JSON representation of this object
* @return a JsonObject that contains only changed fields (including nested changes)
*/
@Nonnull
private static
JsonObject
createPatchObjectRecursiveDelta( @Nonnull final VdmObject<?> vdmObject, @Nonnull final JsonObject jsonObject )
{
final JsonObject patch = new JsonObject();

// Process all complex fields
vdmObject
.toMapOfFields()
.entrySet()
.stream()
.filter(entry -> entry.getValue() instanceof VdmComplex<?>)
.map(entry -> {
final String fieldName = entry.getKey();
final VdmComplex<?> complexField = (VdmComplex<?>) entry.getValue();
// Recursively build patch for the complex field
final JsonObject childJsonObject =
createPatchObjectRecursiveDelta(complexField, jsonObject.getAsJsonObject(fieldName));
return Map.entry(fieldName, childJsonObject);
})
.filter(entry -> !entry.getValue().isEmpty())
.forEach(entry -> patch.add(entry.getKey(), entry.getValue()));

// Add changed primitive fields
vdmObject.getChangedFields().keySet().forEach(key -> patch.add(key, jsonObject.get(key)));

return patch;
}

private static void removeVersionIdentifier( @Nonnull final JsonObject jsonObject )
{
log.debug("Removing redundant \"versionIdentifier\" recursively from JSON object: {}", jsonObject);
Expand Down
Loading
Loading