diff --git a/base/model/src/main/java/org/eclipse/ditto/base/model/entity/metadata/ImmutableMetadata.java b/base/model/src/main/java/org/eclipse/ditto/base/model/entity/metadata/ImmutableMetadata.java index 4cdf72145b..086cfedf7e 100644 --- a/base/model/src/main/java/org/eclipse/ditto/base/model/entity/metadata/ImmutableMetadata.java +++ b/base/model/src/main/java/org/eclipse/ditto/base/model/entity/metadata/ImmutableMetadata.java @@ -12,8 +12,8 @@ */ package org.eclipse.ditto.base.model.entity.metadata; -import static org.eclipse.ditto.json.JsonFactory.newValue; import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; +import static org.eclipse.ditto.json.JsonFactory.newValue; import java.io.IOException; import java.util.Iterator; @@ -27,6 +27,7 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import org.eclipse.ditto.base.model.json.JsonSchemaVersion; import org.eclipse.ditto.json.JsonArray; import org.eclipse.ditto.json.JsonCollectors; import org.eclipse.ditto.json.JsonFactory; @@ -38,7 +39,6 @@ import org.eclipse.ditto.json.JsonPointer; import org.eclipse.ditto.json.JsonValue; import org.eclipse.ditto.json.SerializationContext; -import org.eclipse.ditto.base.model.json.JsonSchemaVersion; /** * Immutable implementation of {@link org.eclipse.ditto.base.model.entity.metadata.Metadata}. @@ -211,6 +211,11 @@ public boolean contains(final CharSequence key) { return wrapped.contains(key); } + @Override + public boolean containsFlatteningArrays(final CharSequence key) { + return wrapped.containsFlatteningArrays(key); + } + @Override public JsonObject get(final JsonPointer pointer) { return wrapped.get(pointer); @@ -241,6 +246,11 @@ public Optional getValue(final CharSequence key) { return wrapped.getValue(key); } + @Override + public Optional getValueFlatteningArrays(final CharSequence key) { + return wrapped.getValueFlatteningArrays(key); + } + @Override public List getKeys() { return wrapped.getKeys(); diff --git a/base/model/src/main/java/org/eclipse/ditto/base/model/entity/metadata/NullMetadata.java b/base/model/src/main/java/org/eclipse/ditto/base/model/entity/metadata/NullMetadata.java index a0194b3131..f48c95a68b 100644 --- a/base/model/src/main/java/org/eclipse/ditto/base/model/entity/metadata/NullMetadata.java +++ b/base/model/src/main/java/org/eclipse/ditto/base/model/entity/metadata/NullMetadata.java @@ -24,6 +24,7 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import org.eclipse.ditto.base.model.json.JsonSchemaVersion; import org.eclipse.ditto.json.JsonArray; import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonField; @@ -35,7 +36,6 @@ import org.eclipse.ditto.json.JsonPointer; import org.eclipse.ditto.json.JsonValue; import org.eclipse.ditto.json.SerializationContext; -import org.eclipse.ditto.base.model.json.JsonSchemaVersion; /** * JSON NULL value version of {@link org.eclipse.ditto.base.model.entity.metadata.Metadata}. @@ -190,11 +190,21 @@ public boolean contains(final CharSequence key) { return false; } + @Override + public boolean containsFlatteningArrays(final CharSequence key) { + return false; + } + @Override public Optional getValue(final CharSequence name) { return Optional.empty(); } + @Override + public Optional getValueFlatteningArrays(final CharSequence key) { + return Optional.empty(); + } + @Override public JsonObject get(final JsonPointer pointer) { return this; diff --git a/json/src/main/java/org/eclipse/ditto/json/ImmutableJsonObject.java b/json/src/main/java/org/eclipse/ditto/json/ImmutableJsonObject.java index da198ea5bc..396eff213b 100644 --- a/json/src/main/java/org/eclipse/ditto/json/ImmutableJsonObject.java +++ b/json/src/main/java/org/eclipse/ditto/json/ImmutableJsonObject.java @@ -29,6 +29,7 @@ import java.util.Objects; import java.util.Optional; import java.util.ServiceLoader; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -223,7 +224,16 @@ private static boolean isEmpty(final Iterable iterable) { @Override public boolean contains(final CharSequence key) { requireNonNull(key, "The key or pointer to check the existence of a value for must not be null!"); + return internalContains(key, false); + } + + @Override + public boolean containsFlatteningArrays(final CharSequence key) { + requireNonNull(key, "The key or pointer to check the existence of a value for must not be null!"); + return internalContains(key, true); + } + boolean internalContains(final CharSequence key, final boolean flatteningArrays) { final boolean result; final JsonPointer pointer = JsonPointer.of(key); @@ -231,17 +241,26 @@ public boolean contains(final CharSequence key) { if (1 >= pointer.getLevelCount()) { result = pointer.getRoot().map(this::containsKey).orElse(false); } else { - result = pointer.getRoot() - .flatMap(this::getValueForKey) - .filter(JsonValue::isObject) - .map(JsonValue::asObject) - .map(jsonObject -> jsonObject.contains(pointer.nextLevel())) - .orElse(false); + result = containsPointer(pointer, flatteningArrays); } return result; } + private Boolean containsPointer(final JsonPointer pointer, final boolean flatteningArrays) { + return pointer.getRoot() + .flatMap(this::getValueForKey) + .filter(val -> val.isObject() || (flatteningArrays && val.isArray())) + .map(val -> val.isObject() ? Stream.of(val.asObject()) : + val.asArray().stream().filter(JsonValue::isObject).map(JsonValue::asObject) + ) + .map(stream -> stream.anyMatch(jsonObject -> flatteningArrays ? + jsonObject.containsFlatteningArrays(pointer.nextLevel()) : + jsonObject.contains(pointer.nextLevel())) + ) + .orElse(false); + } + private boolean containsKey(final CharSequence key) { return fieldMap.containsKey(key.toString()); } @@ -249,10 +268,16 @@ private boolean containsKey(final CharSequence key) { @Override public Optional getValue(final CharSequence key) { requireNonNull(key, "The key or pointer of the value to be retrieved must not be null!"); - return getValueForPointer(JsonPointer.of(key)); + return getValueForPointer(JsonPointer.of(key), false); + } + + @Override + public Optional getValueFlatteningArrays(final CharSequence key) { + requireNonNull(key, "The key or pointer of the value to be retrieved must not be null!"); + return getValueForPointer(JsonPointer.of(key), true); } - private Optional getValueForPointer(final JsonPointer pointer) { + private Optional getValueForPointer(final JsonPointer pointer, final boolean flatteningArrays) { final Optional result; final JsonKey rootKey = pointer.getRoot().orElse(ROOT_KEY); @@ -263,10 +288,31 @@ private Optional getValueForPointer(final JsonPointer pointer) { // same as getting a value for a key result = getValueForKey(rootKey); } else { - result = getValueForKey(rootKey) - .filter(JsonValue::isObject) - .map(JsonValue::asObject) - .flatMap(jsonObject -> jsonObject.getValue(pointer.nextLevel())); + final AtomicReference valueIsArray = new AtomicReference<>(false); + final List collected = getValueForKey(rootKey).map(Stream::of).orElse(Stream.empty()) + .filter(val -> val.isObject() || (flatteningArrays && val.isArray())) + .flatMap(val -> { + if (val.isObject()) { + return Stream.of(val.asObject()); + } else { + valueIsArray.set(true); + return val.asArray().stream().filter(JsonValue::isObject).map(JsonValue::asObject); + } + }) + .flatMap(jsonObject -> flatteningArrays ? + jsonObject.getValueFlatteningArrays(pointer.nextLevel()) + .map(Stream::of).orElseGet(Stream::empty) : + jsonObject.getValue(pointer.nextLevel()) + .map(Stream::of).orElseGet(Stream::empty) + ).collect(Collectors.toList()); + + if (collected.isEmpty()) { + result = Optional.empty(); + } else if (Boolean.TRUE.equals(valueIsArray.get())) { + result = Optional.of(collected.stream().collect(JsonCollectors.valuesToArray())); + } else { + result = Optional.of(collected.get(0)); + } } return result; @@ -281,7 +327,7 @@ private Optional getValueForKey(final CharSequence key) { public Optional getValue(final JsonFieldDefinition fieldDefinition) { checkFieldDefinition(fieldDefinition); - return getValueForPointer(fieldDefinition.getPointer()).map(fieldDefinition::mapValue); + return getValueForPointer(fieldDefinition.getPointer(), false).map(fieldDefinition::mapValue); } private static void checkFieldDefinition(final JsonFieldDefinition fieldDefinition) { @@ -308,7 +354,7 @@ public JsonObject get(final JsonPointer pointer) { final Optional rootKeyDefinition = getDefinitionForKey(rootKey); if (1 >= pointer.getLevelCount()) { result = rootKeyValue.map( - jsonValue -> JsonField.newInstance(rootKey, jsonValue, rootKeyDefinition.orElse(null))) + jsonValue -> JsonField.newInstance(rootKey, jsonValue, rootKeyDefinition.orElse(null))) .map(jsonField -> Collections.singletonMap(jsonField.getKeyName(), jsonField)) .map(ImmutableJsonObject::of) .orElseGet(ImmutableJsonObject::empty); @@ -321,16 +367,16 @@ public JsonObject get(final JsonPointer pointer) { .isPresent(); result = rootKeyValue.map(jsonValue -> { - if (jsonValue.isObject()) { - if (containsNextLevelRootKey.test(jsonValue.asObject())) { - return jsonValue.asObject().get(nextPointerLevel); // Recursion - } else { - return null; - } - } else { - return jsonValue; - } - }) + if (jsonValue.isObject()) { + if (containsNextLevelRootKey.test(jsonValue.asObject())) { + return jsonValue.asObject().get(nextPointerLevel); // Recursion + } else { + return null; + } + } else { + return jsonValue; + } + }) .map(jsonValue -> JsonField.newInstance(rootKey, jsonValue, rootKeyDefinition.orElse(null))) .map(jsonField -> Collections.singletonMap(jsonField.getKeyName(), jsonField)) .map(ImmutableJsonObject::of) @@ -360,7 +406,7 @@ public JsonObject get(final JsonFieldSelector fieldSelector) { final List pointersContainedInThis = fieldSelector.getPointers() .stream() - .filter(this::contains) + .filter(this::containsFlatteningArrays) .collect(Collectors.toList()); if (pointersContainedInThis.isEmpty()) { @@ -381,9 +427,17 @@ private static JsonObject filterByTrie(final JsonObject self, final JsonFieldSel for (final JsonKey key : trie.getKeys()) { self.getField(key).ifPresent(child -> { final JsonValue childValue = child.getValue(); - final JsonValue filteredChildValue = childValue.isObject() - ? filterByTrie(childValue.asObject(), trie.descend(key)) - : childValue; + final JsonValue filteredChildValue; + if (childValue.isObject()) { + filteredChildValue = filterByTrie(childValue.asObject(), trie.descend(key)); // recurse! + } else if (childValue.isArray()) { + filteredChildValue = childValue.asArray().stream() + .filter(JsonValue::isObject) + .map(value -> filterByTrie(value.asObject(), trie.descend(key))) // recurse! + .collect(JsonCollectors.valuesToArray()); + } else { + filteredChildValue = childValue; + } final Optional childFieldDefinition = child.getDefinition(); if (childFieldDefinition.isPresent()) { builder.set(childFieldDefinition.get(), filteredChildValue); @@ -422,7 +476,8 @@ private JsonObject removeForPointer(final JsonPointer pointer) { .map(JsonValue::asObject) .filter(containsNextLevelRootKey) .map(jsonObject -> jsonObject.remove(nextPointerLevel)) // Recursion - .map(withoutValue -> JsonField.newInstance(rootKey, withoutValue, getDefinitionForKey(rootKey).orElse(null))) + .map(withoutValue -> JsonField.newInstance(rootKey, withoutValue, + getDefinitionForKey(rootKey).orElse(null))) .map(this::set) .orElse(this); } @@ -568,7 +623,7 @@ private SoftReferencedFieldMap(final Map jsonFieldMap, if (CBOR_FACTORY.isCborAvailable()) { try { this.cborObjectRepresentation = CBOR_FACTORY.createCborRepresentation(jsonFieldMap, - guessSerializedSize()); + guessSerializedSize()); } catch (final IOException e) { assert false; // this should not happen, so assertions will throw during testing jsonObjectStringRepresentation = createStringRepresentation(jsonFieldMap); diff --git a/json/src/main/java/org/eclipse/ditto/json/ImmutableJsonObjectNull.java b/json/src/main/java/org/eclipse/ditto/json/ImmutableJsonObjectNull.java index 93421c8f8c..91f0cb670e 100755 --- a/json/src/main/java/org/eclipse/ditto/json/ImmutableJsonObjectNull.java +++ b/json/src/main/java/org/eclipse/ditto/json/ImmutableJsonObjectNull.java @@ -112,6 +112,11 @@ public boolean contains(final CharSequence key) { return false; } + @Override + public boolean containsFlatteningArrays(final CharSequence key) { + return false; + } + @Override public ImmutableJsonObjectNull get(final JsonPointer pointer) { return this; @@ -127,6 +132,11 @@ public Optional getValue(final CharSequence key) { return Optional.empty(); } + @Override + public Optional getValueFlatteningArrays(final CharSequence key) { + return Optional.empty(); + } + @Override public Optional getValue(final JsonFieldDefinition fieldDefinition) { return Optional.empty(); diff --git a/json/src/main/java/org/eclipse/ditto/json/JsonObject.java b/json/src/main/java/org/eclipse/ditto/json/JsonObject.java index 7370d81a65..8014ad779d 100755 --- a/json/src/main/java/org/eclipse/ditto/json/JsonObject.java +++ b/json/src/main/java/org/eclipse/ditto/json/JsonObject.java @@ -220,6 +220,37 @@ default JsonObjectBuilder toBuilder() { */ boolean contains(CharSequence key); + /** + * Indicates whether this JSON object contains a field at the key defined location. When encountering a + * {@link JsonArray}, its contents, if those are {@link JsonObject}s themselves, are "flattened", effectively + * treating JsonArrays as invisible wrapper for JsonObjects. + * If, for example, on the following JSON object + *
+     *    {
+     *       "thingId": "myThing",
+     *       "attributes": {
+     *          "someArr": [
+     *             {
+     *                "subsel": 42
+     *             },
+     *             {
+     *                "subsel": 90
+     *             }
+     *          ],
+     *          "anotherAttr": "baz"
+     *       }
+     *    }
+     * 
+ *

+ * this method with the pointer {@code "attributes/someArr/subsel"} is called, the response would be {@code true}. + *

+ * @return {@code true} if this JSON object contains a field at {@code key} + * (flattening JsonArrays in the hierarchy), {@code false} else. + * @throws NullPointerException if {@code key} is {@code null}. + * @since 3.5.0 + */ + boolean containsFlatteningArrays(CharSequence key); + /** * Returns a new JSON object containing the whole object hierarchy of the value which is defined by the given * pointer. If, for example, on the following JSON object @@ -321,8 +352,9 @@ default JsonObjectBuilder toBuilder() { * } * } * - * this method is called with key {@code "attributes/someAttr/subsel"} an empty Optional is returned. Is the key - * {@code "thingId"} used instead the returned Optional would contain {@code "myThing"}. + * this method is called with key {@code "attributes/someAttr/subsel"}, the JsonValue {@code 42} will be contained + * in the returned Optional. If the key {@code "thingId"} used instead the returned Optional would + * contain {@code "myThing"}. * If the specified key is empty or {@code "/"} this object reference is returned within the result. * * @param key defines which value to get. @@ -331,6 +363,35 @@ default JsonObjectBuilder toBuilder() { */ Optional getValue(CharSequence key); + /** + * Returns the value which is associated with the specified key. This method is similar to {@link #get(JsonPointer)} + * however it does not maintain any hierarchy but returns simply the value. If, for example, on the following JSON + * object + *
+     *    {
+     *       "thingId": "myThing",
+     *       "attributes": {
+     *          "someArr": [
+     *             {
+     *                "subsel": 42
+     *             },
+     *             {
+     *                "subsel": 90
+     *             }
+     *          ],
+     *          "anotherAttr": "baz"
+     *       }
+     *    }
+     * 
+ * this method is called with key {@code "attributes/someArr/subsel"}, the JsonArray with 2 JsonValues {@code 42} + * and {@code 90} will be contained in the returned Optional. + * + * @param key defines which value to get. + * @return the JSON value at the key-defined location within this object, flattening encountered JsonArrays. + * @since 3.5.0 + */ + Optional getValueFlatteningArrays(CharSequence key); + /** * Returns the plain Java typed value of the field whose location is defined by the JsonPointer of the specified * JsonFieldDefinition. The expected Java type is the value type of the JsonFieldDefinition. If this JsonObject diff --git a/json/src/test/java/org/eclipse/ditto/json/ImmutableJsonObjectTest.java b/json/src/test/java/org/eclipse/ditto/json/ImmutableJsonObjectTest.java index 910b36d8b5..08e6ab9284 100644 --- a/json/src/test/java/org/eclipse/ditto/json/ImmutableJsonObjectTest.java +++ b/json/src/test/java/org/eclipse/ditto/json/ImmutableJsonObjectTest.java @@ -48,9 +48,16 @@ public final class ImmutableJsonObjectTest { private static final JsonKey KNOWN_KEY_FOO = JsonKey.of("foo"); private static final JsonKey KNOWN_KEY_BAR = JsonKey.of("bar"); private static final JsonKey KNOWN_KEY_BAZ = JsonKey.of("baz"); + private static final JsonKey KNOWN_KEY_ARRAY = JsonKey.of("array"); + private static final JsonKey KNOWN_KEY_ARRAY_OBJS = JsonKey.of("array_objs"); private static final JsonValue KNOWN_VALUE_FOO = JsonValue.of("bar"); private static final JsonValue KNOWN_VALUE_BAR = JsonValue.of("baz"); private static final JsonValue KNOWN_VALUE_BAZ = JsonValue.of(KNOWN_INT_42); + private static final JsonValue KNOWN_VALUE_ARRAY = JsonArray.of(KNOWN_INT_23, KNOWN_INT_42); + private static final JsonValue KNOWN_VALUE_ARRAY_OBJS = JsonArray.of( + JsonObject.newBuilder().set("num", KNOWN_INT_23).build(), + JsonObject.newBuilder().set("num", KNOWN_INT_42).set("bool", false).build() + ); private static final Map KNOWN_FIELDS = new LinkedHashMap<>(); private static final String KNOWN_JSON_STRING; @@ -58,11 +65,15 @@ public final class ImmutableJsonObjectTest { KNOWN_FIELDS.put(KNOWN_KEY_FOO.toString(), toField(KNOWN_KEY_FOO, KNOWN_VALUE_FOO)); KNOWN_FIELDS.put(KNOWN_KEY_BAR.toString(), toField(KNOWN_KEY_BAR, KNOWN_VALUE_BAR)); KNOWN_FIELDS.put(KNOWN_KEY_BAZ.toString(), toField(KNOWN_KEY_BAZ, KNOWN_VALUE_BAZ)); + KNOWN_FIELDS.put(KNOWN_KEY_ARRAY.toString(), toField(KNOWN_KEY_ARRAY, KNOWN_VALUE_ARRAY)); + KNOWN_FIELDS.put(KNOWN_KEY_ARRAY_OBJS.toString(), toField(KNOWN_KEY_ARRAY_OBJS, KNOWN_VALUE_ARRAY_OBJS)); KNOWN_JSON_STRING = "{" + "\"" + KNOWN_KEY_FOO + "\":\"" + KNOWN_VALUE_FOO.asString() + "\"," + "\"" + KNOWN_KEY_BAR + "\":\"" + KNOWN_VALUE_BAR.asString() + "\"," - + "\"" + KNOWN_KEY_BAZ + "\":" + KNOWN_VALUE_BAZ.asInt() + + "\"" + KNOWN_KEY_BAZ + "\":" + KNOWN_VALUE_BAZ.asInt() + "," + + "\"" + KNOWN_KEY_ARRAY + "\":" + KNOWN_VALUE_ARRAY + "," + + "\"" + KNOWN_KEY_ARRAY_OBJS + "\":" + KNOWN_VALUE_ARRAY_OBJS + "}"; } @@ -181,7 +192,7 @@ public void getInstanceReturnsExpected() { assertThat(underTest).isObject() .isNotEmpty() - .hasSize(3); + .hasSize(5); assertThat(underTest.asObject()).isSameAs(underTest); assertThat(underTest.toString()).hasToString(KNOWN_JSON_STRING); } @@ -527,6 +538,13 @@ public void getExistingValueByName() { assertThat(underTest.getValue(KNOWN_KEY_BAZ)).contains(KNOWN_VALUE_BAZ); } + @Test + public void getExistingArrayValueByName() { + final JsonObject underTest = ImmutableJsonObject.of(KNOWN_FIELDS); + + assertThat(underTest.getValue(KNOWN_KEY_ARRAY)).contains(KNOWN_VALUE_ARRAY); + } + @Test public void getNonExistingValueByNameReturnsEmptyOptional() { final JsonObject underTest = ImmutableJsonObject.of(KNOWN_FIELDS); @@ -683,7 +701,8 @@ public void removeExistingValueByPointerReturnsExpected() { @Test public void getKeysReturnsExpected() { final JsonObject underTest = ImmutableJsonObject.of(KNOWN_FIELDS); - final JsonKey[] expectedKeys = new JsonKey[]{KNOWN_KEY_FOO, KNOWN_KEY_BAR, KNOWN_KEY_BAZ}; + final JsonKey[] expectedKeys = + new JsonKey[]{KNOWN_KEY_FOO, KNOWN_KEY_BAR, KNOWN_KEY_BAZ, KNOWN_KEY_ARRAY, KNOWN_KEY_ARRAY_OBJS}; final List actualKeys = underTest.getKeys(); assertThat(actualKeys).containsOnly(expectedKeys); @@ -908,6 +927,8 @@ public void iteratorWorksAsExpected() { expectedJsonFields.add(toField(KNOWN_KEY_FOO, KNOWN_VALUE_FOO)); expectedJsonFields.add(toField(KNOWN_KEY_BAR, KNOWN_VALUE_BAR)); expectedJsonFields.add(toField(KNOWN_KEY_BAZ, KNOWN_VALUE_BAZ)); + expectedJsonFields.add(toField(KNOWN_KEY_ARRAY, KNOWN_VALUE_ARRAY)); + expectedJsonFields.add(toField(KNOWN_KEY_ARRAY_OBJS, KNOWN_VALUE_ARRAY_OBJS)); final Iterator underTest = jsonObject.iterator(); int index = 0; @@ -999,6 +1020,24 @@ public void containsShouldReturnFalseOnPointerDeeperThanObject() { assertThat(underTest.contains(deeperThanObject)).isFalse(); } + @Test + public void containsNotFlatteningArrayNotRespectsObjectsInArrays() { + final ImmutableJsonObject underTest = + ImmutableJsonObject.of(toMap(KNOWN_KEY_ARRAY_OBJS, KNOWN_VALUE_ARRAY_OBJS)); + final JsonPointer deeperThanObject = KNOWN_KEY_ARRAY_OBJS.asPointer().append(JsonPointer.of("/num")); + + assertThat(underTest.contains(deeperThanObject)).isFalse(); + } + + @Test + public void containsFlatteningArrayRespectsObjectsInArrays() { + final ImmutableJsonObject underTest = + ImmutableJsonObject.of(toMap(KNOWN_KEY_ARRAY_OBJS, KNOWN_VALUE_ARRAY_OBJS)); + final JsonPointer deeperThanObject = KNOWN_KEY_ARRAY_OBJS.asPointer().append(JsonPointer.of("/num")); + + assertThat(underTest.containsFlatteningArrays(deeperThanObject)).isTrue(); + } + @Test(expected = NullPointerException.class) public void tryToGetJsonObjectWithNullJsonPointer() { final JsonObject underTest = ImmutableJsonObject.empty(); @@ -1091,6 +1130,37 @@ public void getExistingValueForPointerReturnsExpected() { assertThat(actual).contains(expected); } + @Test + public void getExistingValueFlatteningArraysForPointerReturnsExpectedInArray() { + /* + * JSON object: + * + * { + * "someObjectAttribute": { + * "someKey": { + * "someNestedKey": [ + * { + * "num": 23 + * }, + * { + * "num": 42 + * } + * ] + * } + * } + * } + */ + final JsonObject nestedJsonObject = ImmutableJsonObject.of(toMap("someNestedKey", KNOWN_VALUE_ARRAY_OBJS)); + final JsonObject attributeJsonObject = ImmutableJsonObject.of(toMap("someKey", nestedJsonObject)); + final JsonObject underTest = ImmutableJsonObject.of(toMap("someObjectAttribute", attributeJsonObject)); + final JsonPointer jsonPointer = JsonPointer.of("someObjectAttribute/someKey/someNestedKey/num"); + + final JsonValue expected = JsonArray.of(JsonValue.of(KNOWN_INT_23), JsonValue.of(KNOWN_INT_42)); + final Optional actual = underTest.getValueFlatteningArrays(jsonPointer); + + assertThat(actual).contains(expected); + } + @Test public void getFieldWithEmptyStringReturnsEmptyOptional() { final ImmutableJsonObject underTest = ImmutableJsonObject.of(toMap(KNOWN_KEY_FOO, KNOWN_VALUE_FOO)); diff --git a/rql/query/src/main/java/org/eclipse/ditto/rql/query/things/ThingPredicatePredicateVisitor.java b/rql/query/src/main/java/org/eclipse/ditto/rql/query/things/ThingPredicatePredicateVisitor.java index 3fef861ef9..015292cd27 100644 --- a/rql/query/src/main/java/org/eclipse/ditto/rql/query/things/ThingPredicatePredicateVisitor.java +++ b/rql/query/src/main/java/org/eclipse/ditto/rql/query/things/ThingPredicatePredicateVisitor.java @@ -22,6 +22,7 @@ import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Pattern; +import java.util.stream.Stream; import javax.annotation.Nullable; @@ -105,8 +106,8 @@ public Function> visitEq(@Nullable final Object value) @Nullable final Object resolvedValue = resolveValue(value); return fieldName -> thing -> getThingField(fieldName, thing) - .flatMap(ThingPredicatePredicateVisitor::mapJsonValueToJava) - .filter(obj -> { + .map(ThingPredicatePredicateVisitor::mapJsonValueToJava) + .filter(stream -> stream.anyMatch(obj -> { // special NULL handling if (NULL_LITERAL == obj && null == resolvedValue) { return true; @@ -114,7 +115,7 @@ public Function> visitEq(@Nullable final Object value) return compare((Comparable) resolvedValue, (Comparable) obj) == 0; } return false; - }) + })) .isPresent(); } @@ -123,8 +124,8 @@ public Function> visitNe(@Nullable final Object value) @Nullable final Object resolvedValue = resolveValue(value); return fieldName -> thing -> !getThingField(fieldName, thing) - .flatMap(ThingPredicatePredicateVisitor::mapJsonValueToJava) - .filter(obj -> { + .map(ThingPredicatePredicateVisitor::mapJsonValueToJava) + .filter(stream -> stream.anyMatch(obj -> { // special NULL handling if (NULL_LITERAL == obj && null == resolvedValue) { return true; @@ -132,7 +133,7 @@ public Function> visitNe(@Nullable final Object value) return compare((Comparable) resolvedValue, (Comparable) obj) == 0; } return false; - }) + })) .isPresent(); } @@ -141,10 +142,11 @@ public Function> visitGe(@Nullable final Object value) @Nullable final Object resolvedValue = resolveValue(value); return fieldName -> thing -> getThingField(fieldName, thing) - .flatMap(ThingPredicatePredicateVisitor::mapJsonValueToJava) - .filter(obj -> obj instanceof Comparable && resolvedValue instanceof Comparable) - .map(Comparable.class::cast) - .filter(obj -> compare((Comparable) resolvedValue, obj) >= 0) + .map(ThingPredicatePredicateVisitor::mapJsonValueToJava) + .filter(stream -> stream.anyMatch(obj -> + obj instanceof Comparable && resolvedValue instanceof Comparable && + compare((Comparable) resolvedValue, (Comparable) obj) >= 0) + ) .isPresent(); } @@ -153,10 +155,11 @@ public Function> visitGt(@Nullable final Object value) @Nullable final Object resolvedValue = resolveValue(value); return fieldName -> thing -> getThingField(fieldName, thing) - .flatMap(ThingPredicatePredicateVisitor::mapJsonValueToJava) - .filter(obj -> obj instanceof Comparable && resolvedValue instanceof Comparable) - .map(Comparable.class::cast) - .filter(obj -> compare((Comparable) resolvedValue, obj) > 0) + .map(ThingPredicatePredicateVisitor::mapJsonValueToJava) + .filter(stream -> stream.anyMatch(obj -> + obj instanceof Comparable && resolvedValue instanceof Comparable && + compare((Comparable) resolvedValue, (Comparable) obj) > 0) + ) .isPresent(); } @@ -165,10 +168,11 @@ public Function> visitLe(@Nullable final Object value) @Nullable final Object resolvedValue = resolveValue(value); return fieldName -> thing -> getThingField(fieldName, thing) - .flatMap(ThingPredicatePredicateVisitor::mapJsonValueToJava) - .filter(obj -> obj instanceof Comparable && resolvedValue instanceof Comparable) - .map(Comparable.class::cast) - .filter(obj -> compare((Comparable) resolvedValue, obj) <= 0) + .map(ThingPredicatePredicateVisitor::mapJsonValueToJava) + .filter(stream -> stream.anyMatch(obj -> + obj instanceof Comparable && resolvedValue instanceof Comparable && + compare((Comparable) resolvedValue, (Comparable) obj) <= 0) + ) .isPresent(); } @@ -177,10 +181,11 @@ public Function> visitLt(@Nullable final Object value) @Nullable final Object resolvedValue = resolveValue(value); return fieldName -> thing -> getThingField(fieldName, thing) - .flatMap(ThingPredicatePredicateVisitor::mapJsonValueToJava) - .filter(obj -> obj instanceof Comparable && resolvedValue instanceof Comparable) - .map(Comparable.class::cast) - .filter(obj -> compare((Comparable) resolvedValue, obj) < 0) + .map(ThingPredicatePredicateVisitor::mapJsonValueToJava) + .filter(stream -> stream.anyMatch(obj -> + obj instanceof Comparable && resolvedValue instanceof Comparable && + compare((Comparable) resolvedValue, (Comparable) obj) < 0) + ) .isPresent(); } @@ -223,11 +228,11 @@ private static Comparable asNumber(final Comparable comparable) { public Function> visitIn(final List values) { return fieldName -> thing -> getThingField(fieldName, thing) - .flatMap(ThingPredicatePredicateVisitor::mapJsonValueToJava) - .filter(Comparable.class::isInstance) - .map(Comparable.class::cast) - .filter(obj -> values.stream().map(this::resolveValue) - .anyMatch(v -> compare((Comparable) v, obj) == 0)) + .map(ThingPredicatePredicateVisitor::mapJsonValueToJava) + .filter(stream -> stream.anyMatch(obj -> + obj instanceof Comparable && values.stream().map(this::resolveValue) + .anyMatch(v -> compare((Comparable) v, (Comparable) obj) == 0)) + ) .isPresent(); } @@ -235,9 +240,10 @@ public Function> visitIn(final List values) { public Function> visitLike(@Nullable final String value) { return fieldName -> thing -> getThingField(fieldName, thing) - .filter(JsonValue::isString) - .map(JsonValue::asString) - .filter(str -> null != value && Pattern.compile(value).matcher(str).matches()) + .map(ThingPredicatePredicateVisitor::mapJsonValueToJava) + .filter(stream -> stream.anyMatch(obj -> null != value && + Pattern.compile(value).matcher(String.valueOf(obj)).matches() + )) .isPresent(); } @@ -245,12 +251,13 @@ public Function> visitLike(@Nullable final String value public Function> visitILike(@Nullable final String value) { return fieldName -> thing -> getThingField(fieldName, thing) - .filter(JsonValue::isString) - .map(JsonValue::asString) - .filter(str -> null != value && Pattern.compile(value, Pattern.CASE_INSENSITIVE).matcher(str).matches()) + .map(ThingPredicatePredicateVisitor::mapJsonValueToJava) + .filter(stream -> stream.anyMatch(obj -> null != value && + Pattern.compile(value, Pattern.CASE_INSENSITIVE).matcher(String.valueOf(obj)).matches() + )) .isPresent(); } - + @Nullable private Object resolveValue(@Nullable final Object value) { if (value instanceof ParsedPlaceholder) { @@ -269,7 +276,7 @@ private Object resolveValue(@Nullable final Object value) { private Optional getThingField(final CharSequence fieldName, final Thing thing) { return Optional.ofNullable( thing.toJson(p -> true) - .getValue(fieldName) // first, try resolving via the thing + .getValueFlatteningArrays(fieldName) // first, try resolving via the thing .orElseGet(() -> {// if that returns nothing, try resolving using the placeholder resolvers: final String[] fieldNameSplit = fieldName.toString().split(Expression.SEPARATOR, 2); if (fieldNameSplit.length > 1) { @@ -289,29 +296,31 @@ private Optional getThingField(final CharSequence fieldName, final Th ); } - private static Optional mapJsonValueToJava(final JsonValue jsonValue) { - final Optional result; + private static Stream mapJsonValueToJava(final JsonValue jsonValue) { + final Stream result; if (jsonValue.isString()) { - result = Optional.of(jsonValue.asString()); + result = Stream.of(jsonValue.asString()); } else if (jsonValue.isBoolean()) { - result = Optional.of(jsonValue.asBoolean()); + result = Stream.of(jsonValue.asBoolean()); } else if (jsonValue.isNull()) { - result = Optional.of(NULL_LITERAL); + result = Stream.of(NULL_LITERAL); } else if (jsonValue.isNumber()) { if (jsonValue.isInt()) { - result = Optional.of(jsonValue.asInt()); + result = Stream.of(jsonValue.asInt()); } else if (jsonValue.isLong()) { - result = Optional.of(jsonValue.asLong()); + result = Stream.of(jsonValue.asLong()); } else { - result = Optional.of(jsonValue.asDouble()); + result = Stream.of(jsonValue.asDouble()); } } else if (jsonValue.isArray()) { - result = Optional.empty(); // filtering arrays is not supported + result = jsonValue.asArray() + .stream() + .flatMap(ThingPredicatePredicateVisitor::mapJsonValueToJava); // recurse! } else if (jsonValue.isObject()) { - result = Optional.empty(); // filtering objects is not supported + result = Stream.empty(); // filtering objects is not supported } else { - result = Optional.empty(); + result = Stream.empty(); } return result; diff --git a/rql/query/src/test/java/org/eclipse/ditto/rql/query/things/ThingPredicatePredicateVisitorTest.java b/rql/query/src/test/java/org/eclipse/ditto/rql/query/things/ThingPredicatePredicateVisitorTest.java index c27423ae57..afc5a6c100 100644 --- a/rql/query/src/test/java/org/eclipse/ditto/rql/query/things/ThingPredicatePredicateVisitorTest.java +++ b/rql/query/src/test/java/org/eclipse/ditto/rql/query/things/ThingPredicatePredicateVisitorTest.java @@ -330,6 +330,45 @@ public void comparingJsonObjectNeverEvaluatesToTrue() { .isFalse(); } + @Test + public void matchingIntegerArrayEq() { + doTest(sut.visitEq(7), + JsonArray.of(JsonValue.of(1), JsonValue.of(3), JsonValue.of(7)) + ).isTrue(); + } + + @Test + public void matchingStringArrayIn() { + doTest(sut.visitIn(Collections.singletonList("this-is-some-content")), + JsonArray.of( + JsonValue.of("hello"), + JsonValue.of("world"), + JsonValue.of("this-is-some-content") + ) + ).isTrue(); + } + + @Test + public void matchingStringArrayNe() { + doTest(sut.visitNe("orl"), + JsonArray.of( + JsonValue.of("hello"), + JsonValue.of("world") + ) + ).isTrue(); + } + + @Test + public void matchingStringArrayILike() { + // the sut already works on regex Pattern - the translation from "*" to ".*" followed by case insensitivity is done in LikePredicateImpl + doTest(sut.visitILike(".*or.?d"), + JsonArray.of( + JsonValue.of("hello"), + JsonValue.of("world") + ) + ).isTrue(); + } + private static AbstractBooleanAssert doTest(final Function> functionToTest, final JsonValue actualValue) { return doTest(functionToTest, "attributes/some-attr", actualValue); diff --git a/things/model/src/main/java/org/eclipse/ditto/things/model/ImmutableAttributes.java b/things/model/src/main/java/org/eclipse/ditto/things/model/ImmutableAttributes.java index e94a7a7d24..0cca326945 100755 --- a/things/model/src/main/java/org/eclipse/ditto/things/model/ImmutableAttributes.java +++ b/things/model/src/main/java/org/eclipse/ditto/things/model/ImmutableAttributes.java @@ -13,8 +13,8 @@ package org.eclipse.ditto.things.model; -import static org.eclipse.ditto.json.JsonFactory.newValue; import static org.eclipse.ditto.base.model.common.ConditionChecker.checkNotNull; +import static org.eclipse.ditto.json.JsonFactory.newValue; import java.io.IOException; import java.util.Iterator; @@ -28,6 +28,7 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import org.eclipse.ditto.base.model.json.JsonSchemaVersion; import org.eclipse.ditto.json.JsonArray; import org.eclipse.ditto.json.JsonCollectors; import org.eclipse.ditto.json.JsonFactory; @@ -40,7 +41,6 @@ import org.eclipse.ditto.json.JsonPointer; import org.eclipse.ditto.json.JsonValue; import org.eclipse.ditto.json.SerializationContext; -import org.eclipse.ditto.base.model.json.JsonSchemaVersion; /** * An immutable implementation of {@link Attributes}. @@ -219,6 +219,11 @@ public boolean contains(final CharSequence key) { return wrapped.contains(key); } + @Override + public boolean containsFlatteningArrays(final CharSequence key) { + return wrapped.containsFlatteningArrays(key); + } + @Override public JsonObject get(final JsonPointer pointer) { return wrapped.get(pointer); @@ -249,6 +254,11 @@ public Optional getValue(final CharSequence key) { return wrapped.getValue(key); } + @Override + public Optional getValueFlatteningArrays(final CharSequence key) { + return wrapped.getValueFlatteningArrays(key); + } + @Override public List getKeys() { return wrapped.getKeys(); diff --git a/things/model/src/main/java/org/eclipse/ditto/things/model/ImmutableFeatureProperties.java b/things/model/src/main/java/org/eclipse/ditto/things/model/ImmutableFeatureProperties.java index 92569086d6..28ca131916 100755 --- a/things/model/src/main/java/org/eclipse/ditto/things/model/ImmutableFeatureProperties.java +++ b/things/model/src/main/java/org/eclipse/ditto/things/model/ImmutableFeatureProperties.java @@ -26,6 +26,7 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import org.eclipse.ditto.base.model.json.JsonSchemaVersion; import org.eclipse.ditto.json.JsonArray; import org.eclipse.ditto.json.JsonCollectors; import org.eclipse.ditto.json.JsonFactory; @@ -38,7 +39,6 @@ import org.eclipse.ditto.json.JsonPointer; import org.eclipse.ditto.json.JsonValue; import org.eclipse.ditto.json.SerializationContext; -import org.eclipse.ditto.base.model.json.JsonSchemaVersion; /** * An immutable implementation of {@link FeatureProperties}. @@ -212,11 +212,21 @@ public boolean contains(final CharSequence key) { return wrapped.contains(key); } + @Override + public boolean containsFlatteningArrays(final CharSequence key) { + return wrapped.containsFlatteningArrays(key); + } + @Override public Optional getValue(final CharSequence key) { return wrapped.getValue(key); } + @Override + public Optional getValueFlatteningArrays(final CharSequence key) { + return wrapped.getValueFlatteningArrays(key); + } + @Override public JsonObject get(final JsonPointer pointer) { return wrapped.get(pointer); diff --git a/things/model/src/main/java/org/eclipse/ditto/things/model/NullAttributes.java b/things/model/src/main/java/org/eclipse/ditto/things/model/NullAttributes.java index 67c9364d87..c9ad57d24d 100755 --- a/things/model/src/main/java/org/eclipse/ditto/things/model/NullAttributes.java +++ b/things/model/src/main/java/org/eclipse/ditto/things/model/NullAttributes.java @@ -24,6 +24,7 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import org.eclipse.ditto.base.model.json.JsonSchemaVersion; import org.eclipse.ditto.json.JsonArray; import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonField; @@ -35,7 +36,6 @@ import org.eclipse.ditto.json.JsonPointer; import org.eclipse.ditto.json.JsonValue; import org.eclipse.ditto.json.SerializationContext; -import org.eclipse.ditto.base.model.json.JsonSchemaVersion; /** * JSON NULL value version of {@link Attributes}. @@ -190,11 +190,21 @@ public boolean contains(final CharSequence key) { return false; } + @Override + public boolean containsFlatteningArrays(final CharSequence key) { + return false; + } + @Override public Optional getValue(final CharSequence name) { return Optional.empty(); } + @Override + public Optional getValueFlatteningArrays(final CharSequence key) { + return Optional.empty(); + } + @Override public JsonObject get(final JsonPointer pointer) { return this; diff --git a/things/model/src/main/java/org/eclipse/ditto/things/model/NullFeatureProperties.java b/things/model/src/main/java/org/eclipse/ditto/things/model/NullFeatureProperties.java index f1b8ddd8da..e264d32d5c 100755 --- a/things/model/src/main/java/org/eclipse/ditto/things/model/NullFeatureProperties.java +++ b/things/model/src/main/java/org/eclipse/ditto/things/model/NullFeatureProperties.java @@ -24,6 +24,7 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import org.eclipse.ditto.base.model.json.JsonSchemaVersion; import org.eclipse.ditto.json.JsonArray; import org.eclipse.ditto.json.JsonFactory; import org.eclipse.ditto.json.JsonField; @@ -35,7 +36,6 @@ import org.eclipse.ditto.json.JsonPointer; import org.eclipse.ditto.json.JsonValue; import org.eclipse.ditto.json.SerializationContext; -import org.eclipse.ditto.base.model.json.JsonSchemaVersion; /** * A null implementation of {@link FeatureProperties}. @@ -206,6 +206,11 @@ public boolean contains(final CharSequence key) { return false; } + @Override + public boolean containsFlatteningArrays(final CharSequence key) { + return false; + } + @Override public JsonObject get(final JsonPointer pointer) { return this; @@ -221,6 +226,11 @@ public Optional getValue(final CharSequence name) { return Optional.empty(); } + @Override + public Optional getValueFlatteningArrays(final CharSequence key) { + return Optional.empty(); + } + @Override public Optional getValue(final JsonFieldDefinition fieldDefinition) { return Optional.empty(); diff --git a/wot/model/src/main/java/org/eclipse/ditto/wot/model/TypedJsonObject.java b/wot/model/src/main/java/org/eclipse/ditto/wot/model/TypedJsonObject.java index c8c1db2488..bcf0a42d44 100644 --- a/wot/model/src/main/java/org/eclipse/ditto/wot/model/TypedJsonObject.java +++ b/wot/model/src/main/java/org/eclipse/ditto/wot/model/TypedJsonObject.java @@ -103,6 +103,11 @@ default boolean contains(final CharSequence key) { return getWrappedObject().contains(key); } + @Override + default boolean containsFlatteningArrays(final CharSequence key) { + return getWrappedObject().containsFlatteningArrays(key); + } + @Override default JsonObject get(final JsonPointer pointer) { return getWrappedObject().get(pointer); @@ -123,6 +128,11 @@ default Optional getValue(final CharSequence key) { return getWrappedObject().getValue(key); } + @Override + default Optional getValueFlatteningArrays(final CharSequence key) { + return getWrappedObject().getValueFlatteningArrays(key); + } + @Override default Optional getValue(final JsonFieldDefinition fieldDefinition) { return getWrappedObject().getValue(fieldDefinition);