Skip to content

Commit

Permalink
implement support for wildcard (*) in selected fields for RetrieveThi…
Browse files Browse the repository at this point in the history
…ng, SudoRetrieveThing and RetrieveFeatures

Signed-off-by: Dominik Guggemos <dominik.guggemos@bosch.io>
  • Loading branch information
dguggemos committed Jan 10, 2022
1 parent 61ee3f7 commit d736766
Show file tree
Hide file tree
Showing 10 changed files with 350 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,29 @@
*/
package org.eclipse.ditto.internal.models.signalenrichment;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletionStage;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.Nullable;

import org.eclipse.ditto.json.JsonFactory;
import org.eclipse.ditto.json.JsonFieldSelector;
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;
import org.eclipse.ditto.things.model.ThingId;
import org.eclipse.ditto.things.model.signals.events.ThingEvent;

/**
* {@link SignalEnrichmentFacade}, adding functionality to retrieve a whole thing.
*/
public interface CachingSignalEnrichmentFacade extends SignalEnrichmentFacade{
public interface CachingSignalEnrichmentFacade extends SignalEnrichmentFacade {

/**
* Retrieve thing given a list of thing events.
Expand All @@ -35,4 +47,36 @@ public interface CachingSignalEnrichmentFacade extends SignalEnrichmentFacade{
*/
CompletionStage<JsonObject> retrieveThing(ThingId thingId, List<ThingEvent<?>> events, long minAcceptableSeqNr);

default JsonObject applyJsonFieldSelector(final JsonObject jsonObject,
@Nullable final JsonFieldSelector fieldSelector) {
if (fieldSelector == null) {
return jsonObject;
} else {
final Collection<JsonKey> featureIds = jsonObject.getValue(Thing.JsonFields.FEATURES.getPointer())
.filter(JsonValue::isObject)
.map(JsonValue::asObject)
.map(JsonObject::getKeys)
.orElse(Collections.emptyList());
final JsonFieldSelector jsonPointers = expandFeatureIdWildcard(fieldSelector, featureIds);
return jsonObject.get(jsonPointers);
}
}

private static JsonFieldSelector expandFeatureIdWildcard(final JsonFieldSelector fieldSelector,
final Collection<JsonKey> featureIds) {

return JsonFactory.newFieldSelector(fieldSelector.getPointers().stream().flatMap(p -> {
if (p.getLevelCount() > 1
&&
p.getRoot().map(k -> Thing.JsonFields.FEATURES.getPointer().equals(JsonPointer.of(k))).orElse(false)
&& p.get(1).map(k -> JsonKey.of("*").equals(k)).orElse(false)) {
return featureIds.stream()
.map(fid -> Thing.JsonFields.FEATURES.getPointer()
.append(JsonPointer.of(fid))
.append(p.getSubPointer(2).orElse(JsonPointer.empty())));
} else {
return Stream.of(p);
}
}).collect(Collectors.toList()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,7 @@ private DittoCachingSignalEnrichmentFacade(final SignalEnrichmentFacade cacheLoa
final var cacheLoader = SignalEnrichmentCacheLoader.of(cacheLoaderFacade);
final var cacheName = cacheNamePrefix + CACHE_NAME_SUFFIX;

extraFieldsCache = CacheFactory.createCache(
cacheLoader,
cacheConfig,
cacheName,
cacheLoaderExecutor);
extraFieldsCache = CacheFactory.createCache(cacheLoader, cacheConfig, cacheName, cacheLoaderExecutor);
}

/**
Expand Down Expand Up @@ -113,8 +109,7 @@ public CompletionStage<JsonObject> retrieveThing(final ThingId thingId, final Li

@Override
public CompletionStage<JsonObject> retrievePartialThing(final ThingId thingId,
@Nullable final JsonFieldSelector jsonFieldSelector,
final DittoHeaders dittoHeaders,
@Nullable final JsonFieldSelector jsonFieldSelector, final DittoHeaders dittoHeaders,
@Nullable final Signal<?> concernedSignal) {

final List<ThingEvent<?>> thingEvents =
Expand All @@ -124,7 +119,7 @@ public CompletionStage<JsonObject> retrievePartialThing(final ThingId thingId,
// as second step only return what was originally requested as fields:
final var cachingParameters = new CachingParameters(jsonFieldSelector, thingEvents, true, 0);
return doRetrievePartialThing(thingId, dittoHeaders, cachingParameters)
.thenApply(jsonObject -> null != jsonFieldSelector ? jsonObject.get(jsonFieldSelector) : jsonObject);
.thenApply(jsonObject -> applyJsonFieldSelector(jsonObject, jsonFieldSelector));
}

/**
Expand Down Expand Up @@ -153,7 +148,7 @@ public CompletionStage<JsonObject> retrievePartialThing(final EntityId thingId,
// as second step only return what was originally requested as fields:
final var cachingParameters = new CachingParameters(jsonFieldSelector, thingEvents, true, minAcceptableSeqNr);
return doRetrievePartialThing(thingId, dittoHeaders, cachingParameters)
.thenApply(jsonObject -> jsonObject.get(jsonFieldSelector));
.thenApply(jsonObject -> applyJsonFieldSelector(jsonObject, jsonFieldSelector));
}

private CompletionStage<JsonObject> doRetrievePartialThing(final EntityId thingId,
Expand All @@ -174,8 +169,9 @@ private CompletionStage<JsonObject> doRetrievePartialThing(final EntityId thingI
return smartUpdateCachedObject(idWithResourceType, cachingParametersWithEnhancedFieldSelector);
}

private static @Nullable
JsonFieldSelector enhanceFieldSelectorWithRevision(@Nullable final Iterable<JsonPointer> fieldSelector) {
@Nullable
private static JsonFieldSelector enhanceFieldSelectorWithRevision(
@Nullable final Iterable<JsonPointer> fieldSelector) {
final JsonFieldSelector result;
if (fieldSelector == null) {
result = null;
Expand Down Expand Up @@ -276,8 +272,7 @@ private static DittoHeaders getLastDittoHeaders(final List<? extends Signal<?>>
private CompletableFuture<JsonObject> doCacheLookup(final SignalEnrichmentCacheKey cacheKey,
final DittoHeaders dittoHeaders) {

LOGGER.withCorrelationId(dittoHeaders)
.debug("Looking up cache entry for <{}>", cacheKey);
LOGGER.withCorrelationId(dittoHeaders).debug("Looking up cache entry for <{}>", cacheKey);
return extraFieldsCache.get(cacheKey)
.thenApply(optionalJsonObject -> optionalJsonObject.orElseGet(JsonObject::empty));
}
Expand All @@ -287,8 +282,7 @@ private static boolean thingEventsStartWithCreated(final List<ThingEvent<?>> thi
}

private CompletionStage<JsonObject> doSmartUpdateCachedObject(final SignalEnrichmentCacheKey cacheKey,
final JsonObject cachedJsonObject,
final CachingParameters cachingParameters,
final JsonObject cachedJsonObject, final CachingParameters cachingParameters,
final DittoHeaders dittoHeaders) {

final CompletionStage<JsonObject> result;
Expand Down Expand Up @@ -327,10 +321,8 @@ private static <T> T getFirst(final List<T> list) {
return list.get(0);
}

private CompletionStage<JsonObject> handleNextExpectedThingEvents(
final SignalEnrichmentCacheKey cacheKey,
final JsonObject cachedJsonObject,
final CachingParameters cachingParameters) {
private CompletionStage<JsonObject> handleNextExpectedThingEvents(final SignalEnrichmentCacheKey cacheKey,
final JsonObject cachedJsonObject, final CachingParameters cachingParameters) {

final var concernedSignals = cachingParameters.concernedEvents;
final var enhancedFieldSelector = cachingParameters.fieldSelector;
Expand Down Expand Up @@ -401,9 +393,7 @@ private static JsonObject getDefaultJsonObject(final JsonObject jsonObject, fina
}

private Optional<CompletionStage<JsonObject>> invalidateCacheOnPolicyChange(final SignalEnrichmentCacheKey cacheKey,
final JsonObject jsonObject,
@Nullable final String cachedPolicyIdOpt,
final DittoHeaders dittoHeaders) {
final JsonObject jsonObject, @Nullable final String cachedPolicyIdOpt, final DittoHeaders dittoHeaders) {

final boolean shouldInvalidate = Optional.ofNullable(cachedPolicyIdOpt).flatMap(cachedPolicyId ->
jsonObject.getValue(Thing.JsonFields.POLICY_ID)
Expand All @@ -419,7 +409,7 @@ private Optional<CompletionStage<JsonObject>> invalidateCacheOnPolicyChange(fina
}
}

private static JsonObject enhanceJsonObject(final JsonObject jsonObject, final List<ThingEvent<?>> concernedSignals,
private JsonObject enhanceJsonObject(final JsonObject jsonObject, final List<ThingEvent<?>> concernedSignals,
@Nullable final JsonFieldSelector enhancedFieldSelector) {

final ThingEvent<?> last = getLast(concernedSignals);
Expand All @@ -432,9 +422,8 @@ private static JsonObject enhanceJsonObject(final JsonObject jsonObject, final L
jsonObjectBuilder.set(Thing.JsonFields.CREATED, timestamp.toString())));
last.getTimestamp().ifPresent(timestamp ->
jsonObjectBuilder.set(Thing.JsonFields.MODIFIED, timestamp.toString()));
return enhancedFieldSelector == null
? jsonObjectBuilder.build()
: jsonObjectBuilder.build().get(enhancedFieldSelector);

return applyJsonFieldSelector(jsonObjectBuilder.build(), enhancedFieldSelector);
}

private static final class CachingParameters {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2019 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.things.service.persistence.actors.strategies.commands;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.concurrent.Immutable;

import org.eclipse.ditto.base.model.signals.commands.Command;
import org.eclipse.ditto.json.JsonFactory;
import org.eclipse.ditto.json.JsonFieldSelector;
import org.eclipse.ditto.json.JsonPointer;
import org.eclipse.ditto.things.model.Feature;
import org.eclipse.ditto.things.model.Features;

/**
* Abstract base class for {@link org.eclipse.ditto.things.model.signals.commands.ThingCommand} strategies.
*
* @param <C> the type of the handled command - of type {@code Command} as also
* {@link org.eclipse.ditto.things.api.commands.sudo.SudoCommand} are handled which are no ThingCommands.
*/
@Immutable
abstract class AbstractRetrieveThingCommandStrategy<C extends Command<C>> extends AbstractThingCommandStrategy<C> {

private static final JsonPointer FEATURE_ID_WILDCARD = JsonPointer.of("*");

protected AbstractRetrieveThingCommandStrategy(final Class<C> theMatchingClass) {
super(theMatchingClass);
}

static JsonFieldSelector expandFeatureIdWildcard(final JsonFieldSelector fieldSelector,
final JsonPointer prefixPointer, final Features features) {

if (features.isEmpty()) {
return fieldSelector;
} else {
final List<String> featureIds = features.stream().map(Feature::getId).collect(Collectors.toList());
final JsonPointer prefixWithWildcard = prefixPointer.append(FEATURE_ID_WILDCARD);
final List<JsonPointer> expanded = fieldSelector.getPointers().stream().flatMap(p -> {
final boolean isWildcardSelector = p.getPrefixPointer(prefixWithWildcard.getLevelCount())
.stream()
.anyMatch(pp -> pp.equals(prefixWithWildcard));
if (isWildcardSelector) {
return featureIds.stream()
.map(fid -> replaceLevel(p, prefixWithWildcard.getLevelCount() - 1, JsonPointer.of(fid)));
} else {
return Stream.of(p);
}
}).collect(Collectors.toList());
return JsonFactory.newFieldSelector(expanded);
}

}

static JsonPointer replaceLevel(final JsonPointer source, final int level, final JsonPointer replacement) {

if (source.getLevelCount() <= level) {
return source;
} else {
if (level == 0) {
return replacement.append(source.nextLevel());
} else if (level == source.getLevelCount() - 1) {
return source.cutLeaf().append(replacement);
} else {
return source.getPrefixPointer(level)
.map(p -> p.append(replacement))
.map(p -> p.append(source.getSubPointer(level + 1).orElse(JsonPointer.empty())))
.orElse(JsonPointer.empty());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.base.model.entity.metadata.Metadata;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
import org.eclipse.ditto.json.JsonFieldSelector;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.json.JsonPointer;
import org.eclipse.ditto.things.model.Features;
import org.eclipse.ditto.things.model.Thing;
import org.eclipse.ditto.things.model.ThingId;
import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
import org.eclipse.ditto.things.model.signals.commands.query.RetrieveFeatures;
import org.eclipse.ditto.things.model.signals.commands.query.RetrieveFeaturesResponse;
import org.eclipse.ditto.things.model.signals.events.ThingEvent;
Expand All @@ -34,7 +36,7 @@
* This strategy handles the {@link org.eclipse.ditto.things.model.signals.commands.query.RetrieveFeatures} command.
*/
@Immutable
final class RetrieveFeaturesStrategy extends AbstractThingCommandStrategy<RetrieveFeatures> {
final class RetrieveFeaturesStrategy extends AbstractRetrieveThingCommandStrategy<RetrieveFeatures> {

/**
* Constructs a new {@code RetrieveFeaturesStrategy} object.
Expand Down Expand Up @@ -68,7 +70,11 @@ private Optional<Features> extractFeatures(final @Nullable Thing thing) {

private static JsonObject getFeaturesJson(final Features features, final RetrieveFeatures command) {
return command.getSelectedFields()
.map(selectedFields -> features.toJson(command.getImplementedSchemaVersion(), selectedFields))
.map(selectedFields -> {
final JsonFieldSelector expandedFieldSelector =
expandFeatureIdWildcard(selectedFields, JsonPointer.empty(), features);
return features.toJson(command.getImplementedSchemaVersion(), expandedFieldSelector);
})
.orElseGet(() -> features.toJson(command.getImplementedSchemaVersion()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
import org.eclipse.ditto.base.model.headers.entitytag.EntityTag;
import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
import org.eclipse.ditto.json.JsonFieldSelector;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.things.model.Thing;
import org.eclipse.ditto.things.model.ThingId;
import org.eclipse.ditto.things.model.ThingsModelFactory;
import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingNotAccessibleException;
import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThing;
import org.eclipse.ditto.things.model.signals.commands.query.RetrieveThingResponse;
Expand All @@ -36,7 +38,7 @@
* This strategy handles the {@link RetrieveThing} command.
*/
@Immutable
final class RetrieveThingStrategy extends AbstractThingCommandStrategy<RetrieveThing> {
final class RetrieveThingStrategy extends AbstractRetrieveThingCommandStrategy<RetrieveThing> {

/**
* Constructs a new {@code RetrieveThingStrategy} object.
Expand Down Expand Up @@ -80,7 +82,12 @@ private static DittoHeadersSettable<?> getRetrieveThingResponse(@Nullable final

private static JsonObject getThingJson(final Thing thing, final ThingQueryCommand<RetrieveThing> command) {
return command.getSelectedFields()
.map(selectedFields -> thing.toJson(command.getImplementedSchemaVersion(), selectedFields))
.map(selectedFields -> {
final JsonFieldSelector expandedFieldSelector = expandFeatureIdWildcard(selectedFields,
Thing.JsonFields.FEATURES.getPointer(),
thing.getFeatures().orElse(ThingsModelFactory.emptyFeatures()));
return thing.toJson(command.getImplementedSchemaVersion(), expandedFieldSelector);
})
.orElseGet(() -> thing.toJson(command.getImplementedSchemaVersion()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,21 @@
import org.eclipse.ditto.base.model.json.JsonSchemaVersion;
import org.eclipse.ditto.internal.utils.persistentactors.results.Result;
import org.eclipse.ditto.internal.utils.persistentactors.results.ResultFactory;
import org.eclipse.ditto.json.JsonFieldSelector;
import org.eclipse.ditto.json.JsonObject;
import org.eclipse.ditto.things.api.commands.sudo.SudoRetrieveThing;
import org.eclipse.ditto.things.api.commands.sudo.SudoRetrieveThingResponse;
import org.eclipse.ditto.things.model.Thing;
import org.eclipse.ditto.things.model.ThingId;
import org.eclipse.ditto.things.model.ThingsModelFactory;
import org.eclipse.ditto.things.model.signals.commands.exceptions.ThingNotAccessibleException;
import org.eclipse.ditto.things.model.signals.events.ThingEvent;

/**
* This strategy handles the {@link SudoRetrieveThing} command.
*/
@Immutable
final class SudoRetrieveThingStrategy extends AbstractThingCommandStrategy<SudoRetrieveThing> {
final class SudoRetrieveThingStrategy extends AbstractRetrieveThingCommandStrategy<SudoRetrieveThing> {

/**
* Constructs a new {@code SudoRetrieveThingStrategy} object.
Expand Down Expand Up @@ -68,7 +70,12 @@ protected Result<ThingEvent<?>> doApply(final Context<ThingId> context,

final JsonSchemaVersion jsonSchemaVersion = determineSchemaVersion(command, theThing);
final JsonObject thingJson = command.getSelectedFields()
.map(selectedFields -> theThing.toJson(jsonSchemaVersion, selectedFields, FieldType.regularOrSpecial()))
.map(selectedFields -> {
final JsonFieldSelector expandedFieldSelector = expandFeatureIdWildcard(selectedFields,
Thing.JsonFields.FEATURES.getPointer(),
thing.getFeatures().orElse(ThingsModelFactory.emptyFeatures()));
return theThing.toJson(jsonSchemaVersion, expandedFieldSelector, FieldType.regularOrSpecial());
})
.orElseGet(() -> theThing.toJson(jsonSchemaVersion, FieldType.regularOrSpecial()));

return ResultFactory.newQueryResult(command,
Expand Down
Loading

0 comments on commit d736766

Please sign in to comment.