diff --git a/crud/src/main/java/com/redhat/lightblue/eval/SetExpressionEvaluator.java b/crud/src/main/java/com/redhat/lightblue/eval/SetExpressionEvaluator.java index 34708b68..fdbb997c 100644 --- a/crud/src/main/java/com/redhat/lightblue/eval/SetExpressionEvaluator.java +++ b/crud/src/main/java/com/redhat/lightblue/eval/SetExpressionEvaluator.java @@ -19,16 +19,18 @@ package com.redhat.lightblue.eval; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; +import java.util.Map.Entry; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.databind.node.ArrayNode; import com.redhat.lightblue.crud.CrudConstants; import com.redhat.lightblue.metadata.ArrayField; import com.redhat.lightblue.metadata.FieldTreeNode; @@ -39,6 +41,9 @@ import com.redhat.lightblue.metadata.Type; import com.redhat.lightblue.metadata.types.Arith; import com.redhat.lightblue.query.FieldAndRValue; +import com.redhat.lightblue.query.MaskedSetExpression; +import com.redhat.lightblue.query.Projection; +import com.redhat.lightblue.query.ProjectionList; import com.redhat.lightblue.query.RValueExpression; import com.redhat.lightblue.query.SetExpression; import com.redhat.lightblue.query.UpdateOperator; @@ -56,6 +61,8 @@ public class SetExpressionEvaluator extends Updater { private final List setValues = new ArrayList<>(); private final UpdateOperator op; private final JsonNodeFactory factory; + private JsonDoc project; + private boolean masked; private static final class FieldData { /** @@ -73,14 +80,12 @@ private static final class FieldData { private final Type fieldType; /** - * If the field is to be set from another field, the referenced relative - * path to the source field + * If the field is to be set from another field, the referenced relative path to the source field */ private final Path refPath; /** - * If the field is to be set from another field, the type of the source - * field + * If the field is to be set from another field, the type of the source field */ private final Type refType; @@ -112,9 +117,15 @@ public FieldData(Path field, Type t, Path refPath, Type refType, RValueExpressio public SetExpressionEvaluator(JsonNodeFactory factory, FieldTreeNode context, SetExpression expr) { this.factory = factory; op = expr.getOp(); + List docs = new ArrayList(); + Projector projector = null; + if (expr instanceof MaskedSetExpression) { + MaskedSetExpression mExpr = (MaskedSetExpression) expr; + masked = true; + projector = Projector.getInstance(new ProjectionList(mExpr.getMaskFields()), Path.EMPTY, context); + } for (FieldAndRValue fld : expr.getFields()) { Path field = fld.getField(); - LOGGER.debug("Parsing setter for {}", field); RValueExpression rvalue = fld.getRValue(); Path refPath = null; FieldTreeNode refMdNode = null; @@ -127,6 +138,10 @@ public SetExpressionEvaluator(JsonNodeFactory factory, FieldTreeNode context, Se } LOGGER.debug("Refpath {}", refPath); } + if(rvalue.getType() == RValueExpression.RValueType._value && rvalue.getValue().getValue() instanceof ObjectNode && projector != null){ + ObjectNode node = (ObjectNode) rvalue.getValue().getValue(); + docs.add(projector.project(new JsonDoc(node), factory)); + } FieldTreeNode mdNode = context.resolve(field); if (mdNode == null) { throw new EvaluationError(CrudConstants.ERR_CANT_ACCESS + field); @@ -141,6 +156,7 @@ public SetExpressionEvaluator(JsonNodeFactory factory, FieldTreeNode context, Se } setValues.add(data); } + project = new JsonDoc(JsonDoc.listToDoc(docs, factory)); } private FieldData initializeSimple(RValueExpression rvalue, FieldTreeNode refMdNode, FieldTreeNode mdNode, Path field, Path refPath) { @@ -196,6 +212,7 @@ public void getUpdateFields(Set fields) { @Override public boolean update(JsonDoc doc, FieldTreeNode contextMd, Path contextPath) { boolean ret = false; + LOGGER.debug("Starting"); for (FieldData df : setValues) { LOGGER.debug("Set field {} in ctx: {} to {}/{}", df.field, contextPath, df.value, df.value.getType()); @@ -240,11 +257,23 @@ private JsonNode setOrAdd(JsonDoc doc, Path contextPath, FieldData df, JsonNode oldValueNode = doc.get(fieldPath); if (newValueNode != null && oldValueNode != null) { newValueNode = df.fieldType.toJson(factory, Arith.add(df.fieldType.fromJson(oldValueNode), newValue, Arith.promote(df.fieldType, newValueType))); - doc.modify(fieldPath, newValueNode, false); + if (masked) { + copyProjection(project, doc, fieldPath); + } else { + doc.modify(fieldPath, newValueNode, false); + } } } return oldValueNode; } + + private void copyProjection(JsonDoc projection, JsonDoc docToModify, Path fieldPath){ + Iterator> fieldsIt = project.getRoot().fields(); + while (fieldsIt.hasNext()) { + Entry next = fieldsIt.next(); + docToModify.modify(fieldPath.add(new Path(next.getKey())), next.getValue(), false); + } + } private boolean oldAndNewAreDifferent(JsonNode oldValueNode, JsonNode newValueNode) { if (oldValueNode == null && newValueNode != null) { diff --git a/crud/src/test/java/com/redhat/lightblue/eval/UpdaterTest.java b/crud/src/test/java/com/redhat/lightblue/eval/UpdaterTest.java index ddf95d11..48a3a1c2 100644 --- a/crud/src/test/java/com/redhat/lightblue/eval/UpdaterTest.java +++ b/crud/src/test/java/com/redhat/lightblue/eval/UpdaterTest.java @@ -23,6 +23,7 @@ import org.junit.Test; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import com.redhat.lightblue.metadata.EntityMetadata; import com.redhat.lightblue.query.UpdateExpression; @@ -168,6 +169,75 @@ public void array_insert() throws Exception { Assert.assertEquals(7, jsonDoc.get(new Path("field6.nf6#")).asInt()); Assert.assertEquals(7, jsonDoc.get(new Path("field6.nf6")).size()); } + + @Test + public void array_foreach_set_this() throws Exception { + UpdateExpression expr = EvalTestContext.updateExpressionFromJson("{ '$foreach' : { 'field7' : '$all' , '$update' : {'$set': { '$this': {} } } } }"); + Updater updater = Updater.getInstance(JSON_NODE_FACTORY, md, expr); + Assert.assertTrue(updater.update(jsonDoc, md.getFieldTreeRoot(), new Path())); + + Assert.assertEquals(4, jsonDoc.get(new Path("field7")).size()); + for (JsonNode node : jsonDoc.get(new Path("field7"))) { + Assert.assertEquals(JsonNodeFactory.instance.objectNode(), node); + Assert.assertEquals(0, node.size()); + Assert.assertTrue(!node.fields().hasNext()); + } + } + + @Test + public void array_foreach_set_partial_this() throws Exception { + UpdateExpression expr = EvalTestContext.updateExpressionFromJson( + "{ '$foreach' : { 'field7' : '$all' , '$update' : {'$set': { '$this': {'elemf1': 'NA', 'elemf2': 'NA', 'elemf3': -1 } }, 'fields': [ { 'field': 'elemf2' }, { 'field': 'elemf3' } ] } } }"); + Updater updater = Updater.getInstance(JSON_NODE_FACTORY, md, expr); + Assert.assertTrue(updater.update(jsonDoc, md.getFieldTreeRoot(), new Path())); + + Assert.assertEquals(4, jsonDoc.get(new Path("field7")).size()); + int i = 0; + for (JsonNode node : jsonDoc.get(new Path("field7"))) { + Assert.assertEquals("elvalue" + i + "_1", node.get("elemf1").asText()); + Assert.assertEquals("NA", node.get("elemf2").asText()); + Assert.assertEquals(-1, node.get("elemf3").asInt()); + Assert.assertEquals(3, node.size()); + i++; + } + } + + @Test + public void array_foreach_set_partial_this_no_fields() throws Exception { + UpdateExpression expr = EvalTestContext.updateExpressionFromJson( + "{ '$foreach' : { 'field7' : '$all' , '$update' : {'$set': { '$this': {'elemf1': 'NA', 'elemf2': 'NA', 'elemf3': -1 } }, 'fields': [ ] } } }"); + Updater updater = Updater.getInstance(JSON_NODE_FACTORY, md, expr); + Assert.assertTrue(updater.update(jsonDoc, md.getFieldTreeRoot(), new Path())); + + Assert.assertEquals(4, jsonDoc.get(new Path("field7")).size()); + int i = 0; + for (JsonNode node : jsonDoc.get(new Path("field7"))) { + Assert.assertEquals("elvalue" + i + "_1", node.get("elemf1").asText()); + Assert.assertEquals("elvalue" + i + "_2", node.get("elemf2").asText()); + Assert.assertEquals(3 + i, node.get("elemf3").asInt()); + Assert.assertEquals(3, node.size()); + i++; + } + } + + @Test + public void array_foreach_set_partial_this_invalid_fields() throws Exception { + UpdateExpression expr = EvalTestContext.updateExpressionFromJson( + "{ '$foreach' : { 'field7' : '$all' , '$update' : {'$set': { '$this': {'elemf1': 'NA', 'elemf2': 'NA', 'elemf3': -1 } }, 'fields': [ { 'field': 'elemf4' } ] } } }"); + // should do nothing + Updater updater = Updater.getInstance(JSON_NODE_FACTORY, md, expr); + Assert.assertTrue(updater.update(jsonDoc, md.getFieldTreeRoot(), new Path())); + + Assert.assertEquals(4, jsonDoc.get(new Path("field7")).size()); + int i = 0; + for (JsonNode node : jsonDoc.get(new Path("field7"))) { + Assert.assertEquals("elvalue" + i + "_1", node.get("elemf1").asText()); + Assert.assertEquals("elvalue" + i + "_2", node.get("elemf2").asText()); + Assert.assertEquals(3 + i, node.get("elemf3").asInt()); + Assert.assertEquals(3, node.size()); + i++; + } + } @Test public void array_foreach_removeall() throws Exception { diff --git a/metadata/src/main/java/com/redhat/lightblue/metadata/Fields.java b/metadata/src/main/java/com/redhat/lightblue/metadata/Fields.java index b75b071d..c9746eb9 100644 --- a/metadata/src/main/java/com/redhat/lightblue/metadata/Fields.java +++ b/metadata/src/main/java/com/redhat/lightblue/metadata/Fields.java @@ -113,6 +113,9 @@ protected FieldTreeNode resolve(Path p, int level) { } else if (name.equals(Path.ANY)) { throw Error.get(MetadataConstants.ERR_INVALID_ARRAY_REFERENCE, name + " in " + p.toString()); } else if (name.equals(Path.THIS)) { + if (level + 1 >= p.numSegments()) { + return this.parent; + } return this.resolve(p, level + 1); } else if (name.equals(Path.PARENT)) { if (parent != null && !(parent instanceof EntitySchema.RootNode)) { diff --git a/query-api/src/main/java/com/redhat/lightblue/query/MaskedSetExpression.java b/query-api/src/main/java/com/redhat/lightblue/query/MaskedSetExpression.java new file mode 100644 index 00000000..5591b52f --- /dev/null +++ b/query-api/src/main/java/com/redhat/lightblue/query/MaskedSetExpression.java @@ -0,0 +1,60 @@ +package com.redhat.lightblue.query; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.redhat.lightblue.util.Path; + +public class MaskedSetExpression extends SetExpression { + private static final long serialVersionUID = 1L; + private List maskFields; + + public MaskedSetExpression(UpdateOperator op, List list, List maskFields) { + super(op, list); + this.maskFields = maskFields; + } + + public List getMaskFields() { + return maskFields; + } + + @Override + public JsonNode toJson() { + ArrayNode node = getFactory().arrayNode(); + for (FieldProjection x : maskFields) { + node.add(x.toJson()); + } + + ObjectNode objectNode = getFactory().objectNode(); + JsonNode setJson = super.toJson(); + if (setJson.has(UpdateOperator._set.toString())) { + objectNode.set(UpdateOperator._set.toString(), setJson.get(UpdateOperator._set.toString())); + } else if (setJson.has(UpdateOperator._add.toString())) { + objectNode.set(UpdateOperator._add.toString(), setJson.get(UpdateOperator._add.toString())); + } + objectNode.set("fields", node); + return objectNode; + } + + public static MaskedSetExpression fromJson(ObjectNode node) { + ObjectNode setNode = getFactory().objectNode(); + if (node.has(UpdateOperator._set.toString())) { + setNode.set(UpdateOperator._set.toString(), node.get(UpdateOperator._set.toString())); + } else if (node.has(UpdateOperator._add.toString())) { + setNode.set(UpdateOperator._add.toString(), node.get(UpdateOperator._add.toString())); + } + SetExpression fromJson = SetExpression.fromJson(setNode); + List mf = new ArrayList(); + Iterator nodeIt = node.get("fields").elements(); + while (nodeIt.hasNext()) { + JsonNode n = nodeIt.next(); + mf.add((FieldProjection) FieldProjection.fromJson(n)); + } + return new MaskedSetExpression(fromJson.getOp(), fromJson.getFields(), mf); + } +} diff --git a/query-api/src/main/java/com/redhat/lightblue/query/PrimitiveUpdateExpression.java b/query-api/src/main/java/com/redhat/lightblue/query/PrimitiveUpdateExpression.java index f84415f0..bb0a9370 100644 --- a/query-api/src/main/java/com/redhat/lightblue/query/PrimitiveUpdateExpression.java +++ b/query-api/src/main/java/com/redhat/lightblue/query/PrimitiveUpdateExpression.java @@ -18,6 +18,8 @@ */ package com.redhat.lightblue.query; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import com.redhat.lightblue.util.Error; @@ -39,6 +41,9 @@ public abstract class PrimitiveUpdateExpression extends PartialUpdateExpression */ public static PrimitiveUpdateExpression fromJson(ObjectNode node) { if (node.has(UpdateOperator._add.toString()) || node.has(UpdateOperator._set.toString())) { + if (node.has("fields")) { + return MaskedSetExpression.fromJson(node); + } return SetExpression.fromJson(node); } else if (node.has(UpdateOperator._unset.toString())) { return UnsetExpression.fromJson(node);