Skip to content

Commit

Permalink
extend metadata creation for already existing fields to enable updati…
Browse files Browse the repository at this point in the history
…ng of existing metadata fields;

add unit test;

Signed-off-by: Stefan Maute <stefan.maute@bosch.io>
  • Loading branch information
Stefan Maute committed Jun 14, 2022
1 parent 7ebde5f commit 31c21ae
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@
import org.eclipse.ditto.base.model.headers.metadata.MetadataHeader;
import org.eclipse.ditto.base.model.headers.metadata.MetadataHeaderKey;
import org.eclipse.ditto.base.model.json.FieldType;
import org.eclipse.ditto.base.model.json.JsonSchemaVersion;
import org.eclipse.ditto.base.model.signals.Signal;
import org.eclipse.ditto.base.model.signals.WithOptionalEntity;
import org.eclipse.ditto.json.JsonFactory;
import org.eclipse.ditto.json.JsonField;
import org.eclipse.ditto.json.JsonKey;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.json.JsonPointer;
import org.eclipse.ditto.json.JsonValue;
import org.eclipse.ditto.things.model.Thing;

/**
* Creates or extends/modifies Metadata of an entity based on {@link MetadataHeader}s of a Signal's DittoHeaders.
Expand All @@ -46,29 +49,35 @@ public final class MetadataFromSignal implements Supplier<Metadata> {

private final Signal<?> signal;
private final WithOptionalEntity withOptionalEntity;
@Nullable private final Thing thingEntity;
@Nullable private final Metadata existingMetadata;

private MetadataFromSignal(final Signal<?> signal, final WithOptionalEntity withOptionalEntity,
private MetadataFromSignal(final Signal<?> signal,
final WithOptionalEntity withOptionalEntity,
@Nullable final Thing thingEntity,
@Nullable final Metadata existingMetadata) {
this.signal = signal;
this.withOptionalEntity = withOptionalEntity;
this.thingEntity = thingEntity;
this.existingMetadata = existingMetadata;
}

/**
* Returns an instance of {@code MetadataFromSignal}.
*
* @param signal provides modified paths and headers.
* @param withOptionalEntity provides the optional entity used to check which paths/leaves were actually modified.
* @param thingEntity provides the thing as json value.
* @param existingMetadata provides existing metadata to be extended.
* @return the instance.
* @throws NullPointerException if {@code signal} is {@code null}.
*/
public static MetadataFromSignal of(final Signal<?> signal,
final WithOptionalEntity withOptionalEntity,
@Nullable final Thing thingEntity,
@Nullable final Metadata existingMetadata) {
return new MetadataFromSignal(checkNotNull(signal, "signal"),
checkNotNull(withOptionalEntity, "withOptionalEntity"),
thingEntity,
existingMetadata);
}

Expand Down Expand Up @@ -101,11 +110,13 @@ public Metadata get() {
if (metadataHeaders.isEmpty()) {
result = existingMetadata;
} else {
result = buildMetadata(entityOptional.get(), metadataHeaders);
final JsonValue mergedThingJson = getMergedThingJson(entityOptional.get());
result = buildMetadata(mergedThingJson, metadataHeaders);
}
} else {
result = existingMetadata;
}

return result;
}

Expand Down Expand Up @@ -166,4 +177,20 @@ private static void addMetadataToLeaf(final JsonPointer path,
}
}

private JsonValue getMergedThingJson(final JsonValue thingJsonFromCommand){
final JsonSchemaVersion schemaVersion =
signal.getDittoHeaders().getSchemaVersion().orElse(signal.getLatestSchemaVersion());

final JsonValue mergedThingJson;
if (thingEntity != null) {
final JsonObject thingJson = thingEntity.toJson(schemaVersion);
mergedThingJson = JsonFactory.mergeJsonValues(thingJsonFromCommand,
thingJson.getValue(signal.getResourcePath().toString()).orElse(JsonObject.empty()));
} else {
mergedThingJson = thingJsonFromCommand;
}

return mergedThingJson;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,27 @@

import java.util.Optional;

import org.eclipse.ditto.json.JsonField;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.json.JsonPointer;
import org.eclipse.ditto.json.JsonValue;
import org.eclipse.ditto.base.model.entity.Entity;
import org.eclipse.ditto.base.model.entity.metadata.Metadata;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.base.model.headers.metadata.MetadataHeaderKey;
import org.eclipse.ditto.base.model.json.JsonSchemaVersion;
import org.eclipse.ditto.base.model.signals.Signal;
import org.eclipse.ditto.base.model.signals.WithOptionalEntity;
import org.eclipse.ditto.json.JsonField;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.json.JsonPointer;
import org.eclipse.ditto.json.JsonValue;
import org.eclipse.ditto.things.model.Feature;
import org.eclipse.ditto.things.model.FeatureDefinition;
import org.eclipse.ditto.things.model.FeatureProperties;
import org.eclipse.ditto.things.model.Thing;
import org.eclipse.ditto.things.model.ThingId;
import org.eclipse.ditto.base.model.signals.Signal;
import org.eclipse.ditto.base.model.signals.WithOptionalEntity;
import org.eclipse.ditto.things.model.signals.commands.modify.MergeThing;
import org.eclipse.ditto.things.model.signals.commands.modify.ModifyFeature;
import org.eclipse.ditto.things.model.signals.commands.modify.ThingModifyCommand;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
Expand All @@ -57,7 +57,7 @@ public final class MetadataFromSignalTest {
private static Feature fluxCapacitor;
private static Thing thingWithoutMetadata;

@Mock private ThingModifyCommand command;
@Mock private ThingModifyCommand<?> command;
@Mock private Entity<?> entity;

@BeforeClass
Expand Down Expand Up @@ -91,13 +91,13 @@ public void setUp() {
public void assertImmutability() {
assertInstancesOf(MetadataFromSignal.class,
areImmutable(),
provided(Signal.class, WithOptionalEntity.class, Metadata.class).areAlsoImmutable());
provided(Signal.class, WithOptionalEntity.class, Thing.class, Metadata.class).areAlsoImmutable());
}

@Test
public void tryToGetInstanceWithNullCommand() {
assertThatNullPointerException()
.isThrownBy(() -> MetadataFromSignal.of(null, null, entity.getMetadata().orElse(null)))
.isThrownBy(() -> MetadataFromSignal.of(null, null, null, entity.getMetadata().orElse(null)))
.withMessage("The signal must not be null!")
.withNoCause();
}
Expand All @@ -106,7 +106,8 @@ public void tryToGetInstanceWithNullCommand() {
public void getMetadataWhenEventHasNoEntityAndEntityHasNullExistingMetadata() {
Mockito.when(command.getEntity(Mockito.any())).thenReturn(Optional.empty());
Mockito.when(entity.getMetadata()).thenReturn(Optional.empty());
final MetadataFromSignal underTest = MetadataFromSignal.of(command, command, entity.getMetadata().orElse(null));
final MetadataFromSignal underTest =
MetadataFromSignal.of(command, command, null, entity.getMetadata().orElse(null));

assertThat(underTest.get()).isNull();
}
Expand All @@ -116,7 +117,8 @@ public void entityHasNoMetadataAndEventDittoHeadersHaveNoMetadata() {
Mockito.when(command.getEntity(Mockito.any())).thenReturn(Optional.of(thingWithoutMetadata.toJson()));
Mockito.when(command.getDittoHeaders()).thenReturn(DittoHeaders.empty());
Mockito.when(entity.getMetadata()).thenReturn(Optional.empty());
final MetadataFromSignal underTest = MetadataFromSignal.of(command, command, entity.getMetadata().orElse(null));
final MetadataFromSignal underTest =
MetadataFromSignal.of(command, command, null, entity.getMetadata().orElse(null));

assertThat(underTest.get()).isNull();
}
Expand All @@ -127,7 +129,8 @@ public void entityMetadataButEventDittoHeadersHaveNoMetadata() {
Mockito.when(command.getEntity(Mockito.any())).thenReturn(Optional.of(thingWithoutMetadata.toJson()));
Mockito.when(command.getDittoHeaders()).thenReturn(DittoHeaders.empty());
Mockito.when(entity.getMetadata()).thenReturn(Optional.of(existingMetadata));
final MetadataFromSignal underTest = MetadataFromSignal.of(command, command, entity.getMetadata().orElse(null));
final MetadataFromSignal underTest =
MetadataFromSignal.of(command, command, null, entity.getMetadata().orElse(null));

assertThat(underTest.get()).isEqualTo(existingMetadata);
}
Expand Down Expand Up @@ -157,7 +160,7 @@ public void createMetadataFromScratch() {
.set(JsonPointer.of("properties/grumbo/lastSeen"), 1955)
.build();

final MetadataFromSignal underTest = MetadataFromSignal.of(modifyFeature, modifyFeature, null);
final MetadataFromSignal underTest = MetadataFromSignal.of(modifyFeature, modifyFeature, null, null);

assertThat(underTest.get()).isEqualTo(expected);
}
Expand Down Expand Up @@ -192,7 +195,8 @@ public void modifyExistingMetadata() {
.set(JsonPointer.of("properties/grumbo/lastSeen"), 1955)
.build();

final MetadataFromSignal underTest = MetadataFromSignal.of(modifyFeature, modifyFeature, existingMetadata);
final MetadataFromSignal underTest =
MetadataFromSignal.of(modifyFeature, modifyFeature, null, existingMetadata);

assertThat(underTest.get()).isEqualTo(expected);
}
Expand Down Expand Up @@ -221,7 +225,7 @@ public void keyWithSpecificPathOverwritesKeyWithWildcardPath() {
.set(JsonPointer.of("properties/grumbo/type"), metric)
.build();

final MetadataFromSignal underTest = MetadataFromSignal.of(modifyFeature, modifyFeature,
final MetadataFromSignal underTest = MetadataFromSignal.of(modifyFeature, modifyFeature, null,
thingWithoutMetadata.getMetadata().orElse(null));

assertThat(underTest.get()).isEqualTo(expected);
Expand All @@ -244,11 +248,55 @@ public void ensureThatLeafsCanOnlyBeObjects() {
modifiedFeature,
dittoHeaders);

final MetadataFromSignal underTest = MetadataFromSignal.of(modifyFeature, modifyFeature, null);
final MetadataFromSignal underTest =
MetadataFromSignal.of(modifyFeature, modifyFeature, null, null);

assertThat(underTest.get())
.isNotEmpty()
.doesNotContain(JsonField.newInstance("/properties/capacitorNr", JsonValue.of("unlimited")));
}

@Test
public void createMetadataForPatchCommandWithEmptyBody() {
final Feature emptyFeature = Feature.newBuilder().withId(fluxCapacitor.getId()).build();
final DittoHeaders dittoHeaders = DittoHeaders.newBuilder()
.putMetadata(MetadataHeaderKey.parse("/scruplusFine"), JsonValue.of("^6,00.32"))
.putMetadata(MetadataHeaderKey.parse("/*/lastSeen"), JsonValue.of(1955))
.build();
final MergeThing mergeFeature = MergeThing.withFeature(thingWithoutMetadata.getEntityId().orElseThrow(),
emptyFeature,
dittoHeaders);
final Metadata expected = Metadata.newBuilder()
.set(JsonPointer.of("scruplusFine"), "^6,00.32")
.set(JsonPointer.of("properties/capacity/value/lastSeen"), 1955)
.set(JsonPointer.of("properties/capacity/unit/lastSeen"), 1955)
.build();

final MetadataFromSignal underTest =
MetadataFromSignal.of(mergeFeature, mergeFeature, thingWithoutMetadata, null);

assertThat(underTest.get()).isEqualTo(expected);
}

@Test
public void createMetadataForPatchCommandWithEmptyBodyAndExistingMetadata() {
final Metadata existingMetadata = Metadata.newBuilder()
.set(JsonPointer.of("thingId"), JsonObject.newBuilder()
.set("description", "The Id of the thing")
.build())
.set(JsonPointer.of("policyId"), JsonObject.newBuilder()
.set("description", "The policyId of the thing")
.build())
.set(JsonPointer.of("features/flux-capacitor/properties/capacity/value/lastSeen"), 1955)
.set(JsonPointer.of("features/flux-capacitor/properties/capacity/unit/lastSeen"), 1955)
.build();
final MergeThing mergeThing = MergeThing.withThing(thingWithoutMetadata.getEntityId().orElseThrow(),
Thing.newBuilder().build(),
DittoHeaders.empty());

final MetadataFromSignal underTest = MetadataFromSignal.of(mergeThing, mergeThing, null, existingMetadata);

assertThat(underTest.get()).isEqualTo(existingMetadata);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import org.eclipse.ditto.things.model.ThingException;

/**
* Thrown if metadata of a Thing can't be modified.
* Thrown if multiple metadata headers are contained in one request.
*
* @since 2.5.0
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ protected Optional<Metadata> calculateRelativeMetadata(@Nullable final Thing ent
if (command instanceof WithOptionalEntity withOptionalEntity &&
!dittoHeaders.getMetadataHeadersToPut().isEmpty()) {
final MetadataFromSignal relativeMetadata =
MetadataFromSignal.of(command, withOptionalEntity, existingRelativeMetadata);
MetadataFromSignal.of(command, withOptionalEntity, entity, existingRelativeMetadata);

return Optional.ofNullable(relativeMetadata.get());
} else if (command instanceof ThingModifyCommand<?> && !dittoHeaders.getMetadataFieldsToDelete().isEmpty()) {
return calculateMetadataForDeleteMetadataRequests(entity, command);
Expand Down Expand Up @@ -191,10 +192,10 @@ private Optional<Metadata> calculateMetadataForDeleteMetadataRequests(@Nullable

private Optional<Metadata> deleteMetadata(final Metadata existingMetadata,
final Set<JsonPointer> metadataFieldsToDelete) {
final MetadataBuilder metadataBuilder = existingMetadata.toBuilder();
metadataFieldsToDelete.forEach(metadataBuilder::remove);
final MetadataBuilder metadataBuilder = existingMetadata.toBuilder();
metadataFieldsToDelete.forEach(metadataBuilder::remove);

return Optional.of(metadataBuilder.build());
return Optional.of(metadataBuilder.build());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public abstract class PersistenceActorTestBase {
protected static final String FEATURE_ID = "featureId";
protected static final String FEATURE_KEY = "featureKey";
protected static final String FEATURE_VALUE = "featureValue";

protected static final Metadata METADATA = Metadata.newBuilder()
.set("attributes", JsonObject.newBuilder()
.set(ATTRIBUTE_KEY, JsonObject.newBuilder()
Expand Down

0 comments on commit 31c21ae

Please sign in to comment.