Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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 @@ -3,6 +3,8 @@
*/
package com.sap.cloud.sdk.datamodel.odata.client.request;

import com.google.common.annotations.Beta;

/**
* The strategy to use when updating existing entities.
*/
Expand All @@ -17,5 +19,28 @@ 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 non-entity type fields.
*
* The request payload contains only the changed fields. Navigation properties are not supported.
*
* @since 5.16.0
*/
@Beta
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 non-entity type fields.
*
* The request payload contains the full value of complex fields for changes in any nested field. Navigation
* properties are not supported.
*
* @since 5.16.0
*/
@Beta
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 All @@ -19,7 +20,6 @@
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.message.BasicHttpResponse;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
Expand Down Expand Up @@ -211,21 +211,41 @@ 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\"}}";
final String expectedSerializedEntity = "{\"ProductCount1\":{\"Quantity\":20}}";

count1.setQuantity(20);

final ODataRequestUpdate receiptUpdate =
FluentHelperFactory
.withServicePath(ODATA_ENDPOINT_URL)
.update(ENTITY_COLLECTION, receipt)
.modifyingEntity()
.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\":{\"ProductId\":123,\"Quantity\":20}}";

count1.setQuantity(20);

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

assertThat(receiptUpdate).isNotNull();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import org.apache.http.client.HttpClient;

import com.google.common.annotations.Beta;
import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpClientAccessor;
import com.sap.cloud.sdk.datamodel.odata.client.ODataProtocol;
Expand Down Expand Up @@ -144,23 +145,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 +202,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 +220,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 +262,35 @@ 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.
* @since 5.16.0
*/
@Beta
@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,23 @@
package com.sap.cloud.sdk.datamodel.odata.helper;

import com.google.common.annotations.Beta;

/**
* Strategy to determine how a patch operation should be applied to an entity.
*
* @since 5.16.0
*/
@Beta
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 @@ -104,7 +104,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 +122,136 @@ 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 -> containsNestedChangedFields(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 obj
* the complex object to check
* @return true if the complex object contains any changed fields, false otherwise
*/
private static boolean containsNestedChangedFields( final Object obj )
{
if( obj instanceof VdmComplex<?> vdmComplex ) {
if( !vdmComplex.getChangedFields().isEmpty() ) {
return true;
}
for( final Object complexField : vdmComplex.toMapOfFields().values() ) {
if( containsNestedChangedFields(complexField) ) {
return true;
}
}
}
return false;
}

/**
* 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 and recursively build patch for the complex field
vdmObject.toMapOfFields().forEach(( fieldName, val ) -> {
if( val instanceof VdmComplex<?> complexField ) {
final var childJsonObject = jsonObject.getAsJsonObject(fieldName);
final var childJsonObjectDelta = createPatchObjectRecursiveDelta(complexField, childJsonObject);
if( !childJsonObjectDelta.isEmpty() ) {
patch.add(fieldName, childJsonObjectDelta);
}
}
});

// Add explicitly changed 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