Skip to content

Commit 441c45f

Browse files
committed
#2072: added PreDefinedExtraFieldsEnricherTest and fixed encountered problems with more complex, nested access only
1 parent 12ca2a3 commit 441c45f

File tree

5 files changed

+550
-88
lines changed

5 files changed

+550
-88
lines changed

internal/models/signalenrichment/src/main/java/org/eclipse/ditto/internal/models/signalenrichment/DittoCachingSignalEnrichmentFacade.java

+39-64
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull;
1616

1717
import java.util.ArrayList;
18-
import java.util.Collection;
1918
import java.util.List;
2019
import java.util.Optional;
2120
import java.util.Set;
@@ -24,7 +23,6 @@
2423
import java.util.concurrent.CompletionStage;
2524
import java.util.concurrent.Executor;
2625
import java.util.stream.Collectors;
27-
import java.util.stream.StreamSupport;
2826

2927
import javax.annotation.Nullable;
3028

@@ -42,9 +40,9 @@
4240
import org.eclipse.ditto.internal.utils.tracing.DittoTracing;
4341
import org.eclipse.ditto.internal.utils.tracing.span.SpanOperationName;
4442
import org.eclipse.ditto.json.JsonArray;
43+
import org.eclipse.ditto.json.JsonCollectors;
4544
import org.eclipse.ditto.json.JsonFactory;
4645
import org.eclipse.ditto.json.JsonFieldSelector;
47-
import org.eclipse.ditto.json.JsonKey;
4846
import org.eclipse.ditto.json.JsonObject;
4947
import org.eclipse.ditto.json.JsonObjectBuilder;
5048
import org.eclipse.ditto.json.JsonPointer;
@@ -174,44 +172,13 @@ public CompletionStage<JsonObject> retrievePartialThing(final ThingId thingId,
174172
return performPreDefinedExtraFieldsOptimization(
175173
thingId, jsonFieldSelector, dittoHeaders, signalHeaders, thingEvents
176174
);
175+
} else {
176+
// as second step only return what was originally requested as fields:
177+
final var cachingParameters =
178+
new CachingParameters(jsonFieldSelector, thingEvents, true, 0);
179+
return doRetrievePartialThing(thingId, dittoHeaders, null, cachingParameters)
180+
.thenApply(jsonObject -> applyJsonFieldSelector(jsonObject, jsonFieldSelector));
177181
}
178-
// as second step only return what was originally requested as fields:
179-
final var cachingParameters =
180-
new CachingParameters(jsonFieldSelector, thingEvents, true, 0);
181-
182-
return doRetrievePartialThing(thingId, dittoHeaders, null, cachingParameters)
183-
.thenApply(jsonObject -> applyJsonFieldSelector(jsonObject, jsonFieldSelector));
184-
}
185-
186-
/**
187-
* Retrieve parts of a thing.
188-
*
189-
* @param thingId ID of the thing.
190-
* @param jsonFieldSelector the selected fields of the thing.
191-
* @param dittoHeaders Ditto headers containing authorization information.
192-
* @param concernedSignals the Signals which caused that this partial thing retrieval was triggered
193-
* (e.g. a {@code ThingEvent})
194-
* @param minAcceptableSeqNr minimum sequence number of the concerned signals to not invalidate the cache.
195-
* @return future that completes with the parts of a thing or fails with an error.
196-
*/
197-
@SuppressWarnings({"java:S1612", "unused"})
198-
public CompletionStage<JsonObject> retrievePartialThing(final EntityId thingId,
199-
final JsonFieldSelector jsonFieldSelector,
200-
final DittoHeaders dittoHeaders,
201-
final Collection<? extends Signal<?>> concernedSignals,
202-
final long minAcceptableSeqNr) {
203-
204-
final List<ThingEvent<?>> thingEvents = concernedSignals.stream()
205-
.filter(signal -> signal instanceof ThingEvent && !Signal.isChannelLive(signal))
206-
.map(signal -> (ThingEvent<?>) signal)
207-
.collect(Collectors.toList());
208-
209-
// as second step only return what was originally requested as fields:
210-
final var cachingParameters =
211-
new CachingParameters(jsonFieldSelector, thingEvents, true, minAcceptableSeqNr);
212-
213-
return doRetrievePartialThing(thingId, dittoHeaders, null, cachingParameters)
214-
.thenApply(jsonObject -> applyJsonFieldSelector(jsonObject, jsonFieldSelector));
215182
}
216183

217184
private CompletionStage<JsonObject> performPreDefinedExtraFieldsOptimization(final ThingId thingId,
@@ -230,14 +197,17 @@ private CompletionStage<JsonObject> performPreDefinedExtraFieldsOptimization(fin
230197

231198
final JsonObject preDefinedExtraFields =
232199
JsonObject.of(signalHeaders.get(DittoHeaderDefinition.PRE_DEFINED_EXTRA_FIELDS_OBJECT.getKey()));
233-
final CompletionStage<JsonObject> filteredPreDefinedExtraFieldsReadGranted =
200+
final JsonObject filteredPreDefinedExtraFieldsReadGranted =
234201
filterPreDefinedExtraReadGrantedObject(jsonFieldSelector, dittoHeaders, signalHeaders,
235202
preDefinedExtraFields);
236203

237204
final boolean allExtraFieldsPresent =
238205
allConfiguredPredefinedExtraFields.containsAll(jsonFieldSelector.getPointers());
239206
if (allExtraFieldsPresent) {
240-
return filteredPreDefinedExtraFieldsReadGranted;
207+
LOGGER.withCorrelationId(dittoHeaders)
208+
.debug("All asked for extraFields for thing <{}> we present in pre-defined fields, " +
209+
"skipping cache retrieval: <{}>", thingId, jsonFieldSelector);
210+
return CompletableFuture.completedStage(filteredPreDefinedExtraFieldsReadGranted);
241211
} else {
242212
// optimization to only fetch extra fields which were not pre-defined
243213
final List<JsonPointer> missingFieldsPointers = new ArrayList<>(jsonFieldSelector.getPointers());
@@ -246,46 +216,51 @@ private CompletionStage<JsonObject> performPreDefinedExtraFieldsOptimization(fin
246216
final var cachingParameters =
247217
new CachingParameters(missingFieldsSelector, thingEvents, true, 0);
248218

219+
LOGGER.withCorrelationId(dittoHeaders)
220+
.debug("Fetching non pre-defined extraFields for thing <{}>: <{}>", thingId, missingFieldsPointers);
221+
249222
return doRetrievePartialThing(thingId, dittoHeaders, null, cachingParameters)
250-
.thenCompose(jsonObject -> filteredPreDefinedExtraFieldsReadGranted
251-
.thenApply(preDefinedObject ->
252-
JsonFactory.newObject( // merge
253-
applyJsonFieldSelector(jsonObject, missingFieldsSelector),
254-
preDefinedObject
255-
)
223+
.thenApply(jsonObject ->
224+
JsonFactory.newObject( // merge
225+
applyJsonFieldSelector(jsonObject, missingFieldsSelector),
226+
filteredPreDefinedExtraFieldsReadGranted
256227
)
257228
);
258229
}
259230
}
260231

261-
private static CompletionStage<JsonObject> filterPreDefinedExtraReadGrantedObject(
232+
private static JsonObject filterPreDefinedExtraReadGrantedObject(
262233
final JsonFieldSelector jsonFieldSelector,
263-
final DittoHeaders dittoHeaders, final DittoHeaders signalHeaders, final JsonObject preDefinedExtraFields) {
234+
final DittoHeaders dittoHeaders,
235+
final DittoHeaders signalHeaders,
236+
final JsonObject preDefinedExtraFields
237+
) {
264238
final JsonObject preDefinedExtraFieldsReadGrant = JsonObject.of(
265239
signalHeaders.get(DittoHeaderDefinition.PRE_DEFINED_EXTRA_FIELDS_READ_GRANT_OBJECT.getKey())
266240
);
267241
final JsonFieldSelector grantedReadJsonFieldSelector = filterAskedForFieldSelectorToGrantedFields(
268-
jsonFieldSelector, preDefinedExtraFieldsReadGrant,
242+
jsonFieldSelector,
243+
preDefinedExtraFieldsReadGrant,
269244
dittoHeaders.getAuthorizationContext().getAuthorizationSubjectIds()
270245
);
271-
return CompletableFuture.completedStage(preDefinedExtraFields.get(grantedReadJsonFieldSelector));
246+
return preDefinedExtraFields.get(grantedReadJsonFieldSelector);
272247
}
273248

274249
private static JsonFieldSelector filterAskedForFieldSelectorToGrantedFields(
275250
final JsonFieldSelector jsonFieldSelector,
276251
final JsonObject preDefinedExtraFieldsReadGrant,
277-
final List<String> authorizationSubjectIds)
278-
{
279-
final List<JsonPointer> allowedPointers = StreamSupport.stream(jsonFieldSelector.spliterator(), false)
280-
.filter(pointer -> preDefinedExtraFieldsReadGrant.getValue(JsonKey.of(pointer.toString()))
281-
.filter(JsonValue::isArray)
282-
.map(JsonValue::asArray)
283-
.filter(readGrantArray -> readGrantArray.stream()
284-
.filter(JsonValue::isString)
285-
.map(JsonValue::asString)
286-
.anyMatch(authorizationSubjectIds::contains)
287-
).isPresent()
288-
).toList();
252+
final List<String> authorizationSubjectIds
253+
) {
254+
final List<JsonValue> authSubjects = authorizationSubjectIds.stream().map(JsonValue::of).toList();
255+
final JsonObject scopedPreDefinedExtraFieldsReadGrant = preDefinedExtraFieldsReadGrant.stream()
256+
.filter(field -> field.getValue().asArray().stream().anyMatch(authSubjects::contains))
257+
.collect(JsonCollectors.fieldsToObject());
258+
final List<JsonPointer> allowedPointers = scopedPreDefinedExtraFieldsReadGrant.getKeys().stream()
259+
.filter(key -> jsonFieldSelector.getPointers().stream()
260+
.anyMatch(p -> key.toString().startsWith(p.toString()))
261+
)
262+
.map(key -> JsonPointer.of(key.toString().substring(1)))
263+
.toList();
289264
return JsonFactory.newFieldSelector(allowedPointers);
290265
}
291266

internal/models/signalenrichment/src/test/java/org/eclipse/ditto/internal/models/signalenrichment/DittoCachingSignalEnrichmentFacadeTest.java

+73-8
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,15 @@ public final class DittoCachingSignalEnrichmentFacadeTest extends AbstractCachin
6363
4L,
6464
Instant.EPOCH,
6565
DittoHeaders.newBuilder()
66-
.putHeader(DittoHeaderDefinition.PRE_DEFINED_EXTRA_FIELDS.getKey(),
67-
"[\"/definition\",\"/attributes/pre\",\"/attributes/pre2\"]")
68-
.putHeader(DittoHeaderDefinition.PRE_DEFINED_EXTRA_FIELDS_READ_GRANT_OBJECT.getKey(),
69-
"{\"/definition\":[\"test:user\"],\"/attributes/pre\":[\"test:user\"]}")
70-
.putHeader(DittoHeaderDefinition.PRE_DEFINED_EXTRA_FIELDS_OBJECT.getKey(),
71-
"{\"definition\":\"some:cool:definition\",\"attributes\":{\"pre\":{\"bar\": [1,2,3]}}}")
66+
.putHeader(DittoHeaderDefinition.PRE_DEFINED_EXTRA_FIELDS.getKey(), """
67+
["/definition","/attributes/pre","/attributes/pre2","/attributes/folder"]
68+
""")
69+
.putHeader(DittoHeaderDefinition.PRE_DEFINED_EXTRA_FIELDS_READ_GRANT_OBJECT.getKey(), """
70+
{"/definition":["test:user"],"/attributes/pre":["test:user"],"/attributes/folder":["test:user"],"/attributes/folder/public":["test:limited"]}
71+
""")
72+
.putHeader(DittoHeaderDefinition.PRE_DEFINED_EXTRA_FIELDS_OBJECT.getKey(), """
73+
{"definition":"some:cool:definition","attributes":{"pre":{"bar": [1,2,3]},"folder":{"public":"public","private":"private"}}}
74+
""")
7275
.build(),
7376
MetadataModelFactory.newMetadataBuilder()
7477
.set("type", "x attribute")
@@ -157,11 +160,73 @@ public void enrichedEventWithPreDefinedExtraFieldsAndAdditionalRequestedOnesLead
157160
// AND: the resulting thing JSON includes the with the updated value:
158161
final JsonObject expectedThingJson = EXPECTED_THING_JSON_PRE_DEFINED_EXTRA.toBuilder()
159162
.remove("attributes/pre2") // we don't have the read grant for this field
160-
.set(JsonPointer.of("attributes/x"), 42) // we expect the updated value (as part of the modify event)
161-
.set(JsonPointer.of("attributes/unchanged"), "foo") // we expect the updated value (retrieved via cache)
163+
.set(JsonPointer.of("attributes/x"),
164+
42) // we expect the updated value (as part of the modify event)
165+
.set(JsonPointer.of("attributes/unchanged"),
166+
"foo") // we expect the updated value (retrieved via cache)
162167
.build();
163168
softly.assertThat(askResult).isCompletedWithValue(expectedThingJson);
164169
});
165170
}
166171

172+
@Test
173+
public void enrichedEventWithPreDefinedExtraFieldsWithMoreComplexStructure() {
174+
DittoTestSystem.run(this, kit -> {
175+
final SignalEnrichmentFacade underTest =
176+
createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L));
177+
final ThingId thingId = ThingId.generateRandom();
178+
final String userId = ISSUER_PREFIX + "user";
179+
final DittoHeaders headers = DittoHeaders.newBuilder()
180+
.authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED,
181+
AuthorizationSubject.newInstance(userId)))
182+
.randomCorrelationId()
183+
.build();
184+
final JsonFieldSelector fieldSelector =
185+
JsonFieldSelector.newInstance("attributes/folder");
186+
final CompletionStage<JsonObject> askResult = underTest.retrievePartialThing(thingId, fieldSelector,
187+
headers, THING_EVENT_PRE_DEFINED_EXTRA_FIELDS);
188+
189+
// THEN: no cache lookup should be done
190+
kit.expectNoMessage(Duration.ofSeconds(1));
191+
askResult.toCompletableFuture().join();
192+
// AND: the resulting thing JSON includes the with the updated value:
193+
final JsonObject expectedThingJson = JsonObject.of("""
194+
{
195+
"attributes": {"folder": {"public": "public", "private": "private"}}
196+
}"""
197+
);
198+
softly.assertThat(askResult).isCompletedWithValue(expectedThingJson);
199+
});
200+
}
201+
202+
@Test
203+
public void enrichedEventWithPreDefinedExtraFieldsWithMoreComplexStructureLimitedUser() {
204+
DittoTestSystem.run(this, kit -> {
205+
final SignalEnrichmentFacade underTest =
206+
createSignalEnrichmentFacadeUnderTest(kit, Duration.ofSeconds(10L));
207+
final ThingId thingId = ThingId.generateRandom();
208+
final String userId = ISSUER_PREFIX + "limited";
209+
final DittoHeaders headers = DittoHeaders.newBuilder()
210+
.authorizationContext(AuthorizationContext.newInstance(DittoAuthorizationContextType.UNSPECIFIED,
211+
AuthorizationSubject.newInstance(userId)))
212+
.randomCorrelationId()
213+
.build();
214+
final JsonFieldSelector fieldSelector =
215+
JsonFieldSelector.newInstance("attributes/folder");
216+
final CompletionStage<JsonObject> askResult = underTest.retrievePartialThing(thingId, fieldSelector,
217+
headers, THING_EVENT_PRE_DEFINED_EXTRA_FIELDS);
218+
219+
// THEN: no cache lookup should be done
220+
kit.expectNoMessage(Duration.ofSeconds(1));
221+
askResult.toCompletableFuture().join();
222+
// AND: the resulting thing JSON includes the with the updated value:
223+
final JsonObject expectedThingJson = JsonObject.of("""
224+
{
225+
"attributes": {"folder": {"public": "public"}}
226+
}"""
227+
);
228+
softly.assertThat(askResult).isCompletedWithValue(expectedThingJson);
229+
});
230+
}
231+
167232
}

0 commit comments

Comments
 (0)