Skip to content

Commit

Permalink
adapt since annotation for next ditto release;
Browse files Browse the repository at this point in the history
filter out empty objects when deleting metadata;
add unit test;

Signed-off-by: Stefan <stefan.maute@bosch.io>
  • Loading branch information
Stefan committed Aug 4, 2022
1 parent c87b078 commit fae4920
Show file tree
Hide file tree
Showing 15 changed files with 97 additions and 28 deletions.
Expand Up @@ -58,7 +58,7 @@ public final class DittoHeaderNotSupportedException extends DittoRuntimeExceptio
/**
* Definition of an optional JSON field that contains the key of the not supported header.
*
* @since 2.5.0
* @since 3.0.0
*/
static final JsonFieldDefinition<String> JSON_FIELD_NOT_SUPPORTED_HEADER_KEY =
JsonFieldDefinition.ofString("notSupportedHeaderKey",
Expand Down Expand Up @@ -99,7 +99,7 @@ public static DittoHeaderNotSupportedException.Builder newInvalidTypeBuilder(fin
* @param headerValue the value of the header.
* @return the builder.
* @throws NullPointerException if any argument is {@code null}.
* @since 2.5.0
* @since 3.0.0
*/
public static DittoHeaderNotSupportedException.Builder newInvalidTypeBuilder(final HeaderDefinition headerDefinition,
@Nullable final CharSequence headerValue) {
Expand All @@ -112,7 +112,7 @@ public static DittoHeaderNotSupportedException.Builder newInvalidTypeBuilder(fin
* The returned builder is initialized with a default message and a default description.
*
* @return the builder.
* @since 2.5.0
* @since 3.0.0
*/
public static DittoHeaderNotSupportedException.Builder newBuilder() {
return new Builder();
Expand Down Expand Up @@ -141,7 +141,7 @@ public static DittoHeaderNotSupportedException fromJson(final JsonObject jsonObj
* Returns the key of the not supported header if known.
*
* @return an Optional that either contains the key of the not supported header or is empty if the key is unknown.
* @since 2.5.0
* @since 3.0.0
*/
public Optional<String> getNotSupportedHeaderKey() {
return Optional.ofNullable(notSupportedHeaderKey);
Expand Down Expand Up @@ -210,7 +210,7 @@ private Builder(final String headerName, @Nullable final CharSequence headerValu
*
* @param notSupportedHeaderKey the key of the not supported header.
* @return this builder instance for method chaining.
* @since 2.5.0
* @since 3.0.0
*/
public Builder withNotSupportedHeaderKey(@Nullable final CharSequence notSupportedHeaderKey) {
if (null != notSupportedHeaderKey) {
Expand Down
Expand Up @@ -327,7 +327,7 @@ public enum DittoHeaderDefinition implements HeaderDefinition {
* Key {@code "get-metadata"}, Java type: {@link String}.
* </p>
*
* @since 2.5.0
* @since 3.0.0
*/
GET_METADATA("get-metadata", String.class, true, false, HeaderValueValidators.getJsonFieldSelectorValidator()),

Expand All @@ -337,7 +337,7 @@ public enum DittoHeaderDefinition implements HeaderDefinition {
* Key {@code "delete-metadata"}, Java type: {@link String}.
* </p>
*
* @since 2.5.0
* @since 3.0.0
*/
DELETE_METADATA("delete-metadata", String.class, true, false, HeaderValueValidators.getJsonFieldSelectorValidator()),

Expand All @@ -347,7 +347,7 @@ public enum DittoHeaderDefinition implements HeaderDefinition {
* Key {@code "ditto-metadata"}, Java type: {@link JsonObject}.
* </p>
*
* @since 2.5.0
* @since 3.0.0
*/
DITTO_METADATA("ditto-metadata", JsonObject.class, false, true, HeaderValueValidators.getNoOpValidator()),

Expand Down Expand Up @@ -503,7 +503,7 @@ public enum DittoHeaderDefinition implements HeaderDefinition {
* @param readFromExternalHeaders whether Ditto reads this header from headers sent by externals.
* @param writeToExternalHeaders whether Ditto publishes this header to externals.
*/
private DittoHeaderDefinition(final String theKey,
DittoHeaderDefinition(final String theKey,
final Class<?> theType,
final boolean readFromExternalHeaders,
final boolean writeToExternalHeaders,
Expand All @@ -519,7 +519,7 @@ private DittoHeaderDefinition(final String theKey,
* @param readFromExternalHeaders whether Ditto reads this header from headers sent by externals.
* @param writeToExternalHeaders whether Ditto publishes this header to externals.
*/
private DittoHeaderDefinition(final String theKey,
DittoHeaderDefinition(final String theKey,
final Class<?> theType,
final Class<?> serializationType,
final boolean readFromExternalHeaders,
Expand Down
Expand Up @@ -352,7 +352,7 @@ static DittoHeadersBuilder newBuilder(final JsonObject jsonObject) {
*
* @return set of {@code JsonPointer}s to get {@code Metadata} for.
* Changes on the returned set are not reflected back to this DittoHeaders instance.
* @since 2.5.0
* @since 3.0.0
*/
Set<JsonPointer> getMetadataFieldsToGet();

Expand All @@ -361,7 +361,7 @@ static DittoHeadersBuilder newBuilder(final JsonObject jsonObject) {
*
* @return set of {@code JsonPointer}s to delete {@code Metadata} for.
* Changes on the returned set are not reflected back to this DittoHeaders instance.
* @since 2.5.0
* @since 3.0.0
*/
Set<JsonPointer> getMetadataFieldsToDelete();

Expand Down
Expand Up @@ -187,7 +187,7 @@ static ValueValidator getMetadataHeadersValidator() {
* Returns a validator for checking if a CharSequence represents a {@link org.eclipse.ditto.json.JsonFieldSelector}.
*
* @return the validator.
* @since 2.5.0
* @since 3.0.0
*/
static ValueValidator getJsonFieldSelectorValidator() {
return JsonFieldSelectorValidator.getInstance();
Expand Down
Expand Up @@ -24,7 +24,7 @@
* This validator parses a CharSequence to a {@link org.eclipse.ditto.json.JsonFieldSelector}.
* If parsing fails, a {@link org.eclipse.ditto.base.model.exceptions.DittoHeaderInvalidException} with detailed description is thrown.
*
* @since 2.5.0
* @since 3.0.0
*/
@Immutable
final class JsonFieldSelectorValidator extends AbstractHeaderValueValidator {
Expand Down
Expand Up @@ -71,7 +71,7 @@ enum ConciergeConfigValue implements KnownConfigValue {
/**
* The default namespace to use for creating things without specified namespace.
*
* @since 2.5.0
* @since 3.0.0
*/
DEFAULT_NAMESPACE("default-namespace", "org.eclipse.ditto");

Expand Down
Expand Up @@ -430,18 +430,18 @@ The `delete-metadata` header expects a comma separated list of metadata `{key}`.
For example a `PATCH` request to `https://{ditto-instance}/api/2/things/{namespace}:{name}` with HTTP header `delete-metadata`
and value `features/lamp/properties/color` will remove the complete `color` property from the thing metadata.

<b>Note:</b> When deleting things or parts of a thing like feature properties or attributes, their relative metadata
<b>Note:</b> When deleting things or parts of a thing, like feature properties or attributes, their relative metadata
is also deleted.

## Wildcard usage for metadata requests

When working with metadata there are some wildcards which can be used to modify, retrieve or delete metadata.
The following table gives an overview which Wildcards can be used for what requests.
The following table gives an overview which Wildcards can be used on top-level for what requests.

| Wildcard | PUT/PATCH | GET | DELETE |
|-----------------------------------------|------------------------------------------------------------------------|------------------------------------------------------------------------------|----------------------------------------------------------------------------|
| `*` | x | retrieve all metadata relative to the path | delete all metadata relative to the path |
| `*/key` | add metadata for the given `key` to all JSON leaves | retrieve all metadata with `key` | x |
| `*/key` | add metadata for the given `key` to all JSON leaves | retrieve all metadata with `key` | delete all metadata with `key` |
| `attributes/*/key` | add metadata for the given `key` to all attributes | retrieve metadata for given `key` from all attributes | delete metadata for given `key` from all attributes |
| `features/*/properties/*/key` | add metadata for the given `key` to all feature properties | retrieve metadata for given `key` from all feature properties | delete metadata for given `key` from all feature properties |
| `features/*/properties/{property}/key` | add metadata for the given `key` to all features with a given property | retrieve metadata for given `key` from all features with a specific property | delete metadata for given `key` from all features with a specific property |
Expand Down
Expand Up @@ -29,7 +29,7 @@
/**
* Thrown if metadata of a Thing can't be modified.
*
* @since 2.5.0
* @since 3.0.0
*/
@Immutable
@JsonParsableException(errorCode = MetadataHeadersConflictException.ERROR_CODE)
Expand Down
Expand Up @@ -29,7 +29,7 @@
/**
* Thrown if metadata of a Thing can't be modified.
*
* @since 2.5.0
* @since 3.0.0
*/
@Immutable
@JsonParsableException(errorCode = MetadataNotModifiableException.ERROR_CODE)
Expand Down
Expand Up @@ -12,7 +12,9 @@
*/
package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;

import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

Expand All @@ -23,12 +25,14 @@
import org.eclipse.ditto.base.model.entity.metadata.MetadataBuilder;
import org.eclipse.ditto.base.model.entity.metadata.MetadataModelFactory;
import org.eclipse.ditto.base.model.headers.DittoHeaderDefinition;
import org.eclipse.ditto.base.model.json.FieldType;
import org.eclipse.ditto.base.model.signals.WithOptionalEntity;
import org.eclipse.ditto.base.model.signals.commands.Command;
import org.eclipse.ditto.internal.utils.headers.conditional.ConditionalHeadersValidator;
import org.eclipse.ditto.internal.utils.persistentactors.etags.AbstractConditionHeaderCheckingCommandStrategy;
import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
import org.eclipse.ditto.json.JsonKey;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.json.JsonPointer;
import org.eclipse.ditto.json.JsonValue;
Expand Down Expand Up @@ -219,8 +223,9 @@ private Optional<Metadata> deleteMetadata(final Metadata existingMetadata,
final Set<JsonPointer> metadataFieldsToDelete) {
final MetadataBuilder metadataBuilder = existingMetadata.toBuilder();
metadataFieldsToDelete.forEach(metadataBuilder::remove);
final Metadata metadata = filterOutEmptyObjects(metadataBuilder);

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

@Override
Expand Down Expand Up @@ -268,4 +273,29 @@ private Set<JsonPointer> expandWildcardsInMetadataExpression(final Set<JsonPoint
return resolvedMetadataPointers;
}

private Metadata filterOutEmptyObjects(final MetadataBuilder metadataBuilder) {
final Metadata metadata = metadataBuilder.build();
final MetadataBuilder newMetadataBuilder = MetadataModelFactory.newMetadataBuilder();
final Map<String, JsonValue> leafs = new HashMap<>();
getNonEmptyLeafs(JsonPointer.empty(), metadata, leafs);
leafs.forEach(newMetadataBuilder::set);

return newMetadataBuilder.build();
}

private static void getNonEmptyLeafs(final JsonPointer path, final JsonValue entity,
final Map<String, JsonValue> leafs) {
if (entity.isObject()) {
final JsonObject jsonObject = entity.asObject();
jsonObject.stream()
.filter(field -> !(field.isMarkedAs(FieldType.SPECIAL) || field.isMarkedAs(FieldType.HIDDEN)))
.forEach(jsonField -> {
final JsonKey key = jsonField.getKey();
getNonEmptyLeafs(path.append(key.asPointer()), jsonField.getValue(), leafs);
});
} else {
leafs.put(path.toString(), entity);
}
}

}
Expand Up @@ -100,7 +100,8 @@ static MetadataFromCommand of(final Command<?> command,
final JsonObject mergedJson =
JsonFactory.newObject(entity.asObject(),
existingOrEmptyThing.toJson().get(resourcePath));
return ThingsModelFactory.newThing(existingOrEmptyThing.toJson().setValue(resourcePath, mergedJson));
return ThingsModelFactory.newThing(
existingOrEmptyThing.toJson().setValue(resourcePath, mergedJson));
} else {
return ThingsModelFactory.newThing(
existingOrEmptyThing.toJson().setValue(resourcePath, entity));
Expand Down
Expand Up @@ -29,6 +29,7 @@
import org.eclipse.ditto.base.model.auth.DittoAuthorizationContextType;
import org.eclipse.ditto.base.model.entity.Revision;
import org.eclipse.ditto.base.model.entity.metadata.Metadata;
import org.eclipse.ditto.base.model.entity.metadata.MetadataModelFactory;
import org.eclipse.ditto.base.model.exceptions.DittoRuntimeException;
import org.eclipse.ditto.base.model.headers.DittoHeaderDefinition;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
Expand Down Expand Up @@ -1441,8 +1442,7 @@ public void modifyThingWithWildcardInMetadata() {

// modify features with metadata
underTest.tell(modifyThing, getRef());
expectMsgEquals(java.time.Duration.ofSeconds(3600),
ETagTestUtils.modifyThingResponse(thing, thing, headers, false));
expectMsgEquals(ETagTestUtils.modifyThingResponse(thing, thing, headers, false));

final var modifiedMetadata = JsonObject.newBuilder()
.set("modified", "2022-06-23T06:49:05")
Expand Down Expand Up @@ -1487,7 +1487,7 @@ public void retrieveFeaturesMetadataWithGetMetadataHeader() {
final JsonObject commandJson = getJsonCommand(thing);
final CreateThing createThing = CreateThing.fromJson(commandJson, dittoHeadersV2);
underTest.tell(createThing, getRef());
expectMsgClass(java.time.Duration.ofSeconds(3600), CreateThingResponse.class);
expectMsgClass(CreateThingResponse.class);

// retrieve thing with metadata
final Metadata expectedMetadata = Metadata.newBuilder()
Expand Down Expand Up @@ -1910,6 +1910,44 @@ public void multipleMetadataHeadersResultsInException() {
}};
}

@Test
public void testRemovalOfEmptyMetadataAfterDeletion() {
final var thing = createThingV2WithRandomId();
final var putHeaders = DittoHeaders.newBuilder()
.putHeader(DittoHeaderDefinition.PUT_METADATA.getKey(),
"[{\"key\":\"*/modified\",\"value\":\"2022-06-23T06:49:05\"}]")
.build();
final var modifyThing = ModifyThing.of(getIdOrThrow(thing), thing, null, putHeaders);

final var deleteHeaders = DittoHeaders.newBuilder()
.putHeader(DittoHeaderDefinition.DELETE_METADATA.getKey(), "*/modified")
.build();
final var modifyThing1 = ModifyThing.of(getIdOrThrow(thing), thing, null, deleteHeaders);

new TestKit(actorSystem) {{
final ActorRef underTest = createPersistenceActorFor(thing);

// create thing
final JsonObject commandJson = getJsonCommand(thing);
final CreateThing createThing = CreateThing.fromJson(commandJson, dittoHeadersV2);
underTest.tell(createThing, getRef());
expectMsgClass(CreateThingResponse.class);

// modify features with metadata
underTest.tell(modifyThing, getRef());
expectMsgEquals(ETagTestUtils.modifyThingResponse(thing, thing, putHeaders, false));

underTest.tell(modifyThing1, getRef());
expectMsgEquals(ETagTestUtils.modifyThingResponse(thing.toBuilder().setRevision(2).build(), thing,
deleteHeaders, false));

// assert that metadata is empty
final Metadata expectedEmptyMetadata = MetadataModelFactory.emptyMetadata();

assertMetadataAsExpected(this, underTest, getIdOrThrow(thing), expectedEmptyMetadata);
}};
}

private void assertPublishEvent(final ThingEvent<?> event) {
final ThingEvent<?> msg = pubSubTestProbe.expectMsgClass(ThingEvent.class);
Assertions.assertThat(msg.toJson())
Expand Down
Expand Up @@ -65,7 +65,7 @@ public static SearchResult newSearchResult(final JsonArray items, final long nex
* @param lastModified the last modified timestamp.
* @return the new immutable search results object.
* @throws NullPointerException if {@code items} is {@code null}.
* @since 2.5.0
* @since 3.0.0
*/
public static SearchResult newSearchResult(final JsonArray items, final long nextPageOffset,
@Nullable final Instant lastModified) {
Expand Down
Expand Up @@ -86,7 +86,7 @@ default SearchResultBuilder toBuilder() {
* Get the timestamp of the last modification of the search result.
*
* @return the timestamp.
* @since 2.5.0
* @since 3.0.0
*/
Optional<Instant> getLastModified();

Expand Down
Expand Up @@ -48,7 +48,7 @@ public interface SearchResultBuilder {
*
* @param lastModified the timestamp.
* @return this builder.
* @since 2.5.0
* @since 3.0.0
*/
SearchResultBuilder lastModified(@Nullable Instant lastModified);

Expand Down

0 comments on commit fae4920

Please sign in to comment.