UNOMI-585: introduce new nestedCondition and set profile.properties.interests as nested (#442)

* UNOMI-585: introduce new nestedCondition and set profile.properties.interests as nested

* UNOMI-585: introduce new nestedCondition and set profile.properties.interests as nested
diff --git a/itests/src/test/java/org/apache/unomi/itests/ConditionESQueryBuilderIT.java b/itests/src/test/java/org/apache/unomi/itests/ConditionESQueryBuilderIT.java
index 3e4333c..da8dabe 100644
--- a/itests/src/test/java/org/apache/unomi/itests/ConditionESQueryBuilderIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/ConditionESQueryBuilderIT.java
@@ -44,16 +44,24 @@
         return list.contains(item);
     }
 
+    @Override
+    protected boolean evalEmpty(Condition c) {
+        @SuppressWarnings("unchecked")
+        List<Item> list = persistenceService.query(c,null,(Class<Item>) emptyItem.getClass());
+        return list.contains(emptyItem);
+    }
+
     @Before
     public void setUp() {
         super.setUp();
         persistenceService.save(item);
+        persistenceService.save(emptyItem);
         persistenceService.refresh();
     }
 
     @After
     public void tearDown() {
         persistenceService.remove(item.getItemId(), item.getClass());
+        persistenceService.remove(emptyItem.getItemId(), emptyItem.getClass());
     }
-
 }
diff --git a/itests/src/test/java/org/apache/unomi/itests/ConditionEvaluatorIT.java b/itests/src/test/java/org/apache/unomi/itests/ConditionEvaluatorIT.java
index 574c340..c9eb401 100644
--- a/itests/src/test/java/org/apache/unomi/itests/ConditionEvaluatorIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/ConditionEvaluatorIT.java
@@ -47,6 +47,7 @@
 public class ConditionEvaluatorIT extends BaseIT {
     protected ConditionBuilder builder;
     protected Item item;
+    protected Item emptyItem;
     protected Date lastVisit;
 
     @Inject @Filter(timeout = 600000)
@@ -58,6 +59,10 @@
         return persistenceService.testMatch(c, item);
     }
 
+    protected boolean evalEmpty(Condition c) {
+        return persistenceService.testMatch(c, emptyItem);
+    }
+
     @Before
     public void setUp() {
         assertNotNull("Definition service should be available", definitionsService);
@@ -72,8 +77,21 @@
         profile.setProperty("gender", "female");
         profile.setProperty("lastVisit", lastVisit);
         profile.setProperty("randomStats", 0.15);
+
+        List<Map<String, Object>> interests = new ArrayList<>();
+        Map<String, Object> interest1 = new HashMap<>();
+        interest1.put("key", "cars");
+        interest1.put("value", 50);
+        interests.add(interest1);
+        Map<String, Object> interest2 = new HashMap<>();
+        interest2.put("key", "football");
+        interest2.put("value", 15);
+        interests.add(interest2);
+        profile.setProperty("interests", interests);
         profile.setSegments(new HashSet<>(Arrays.asList("s1", "s2", "s3")));
         item = profile;
+
+        emptyItem = new Profile("profile-" + UUID.randomUUID().toString());
     }
 
     @Test
@@ -219,4 +237,55 @@
         ).build();
         assertTrue(eval(condition));
     }
+
+    @Test
+    public void testNestedConditionEvaluator() {
+        // football cars is 50 on the profile
+        assertTrue(eval(buildConditionOnNestedInterests("cars", 45, "greaterThan")));
+        assertTrue(eval(buildConditionOnNestedInterests("cars", 60, "lessThan")));
+        assertFalse(eval(buildConditionOnNestedInterests("cars", 60, "greaterThan")));
+        assertFalse(eval(buildConditionOnNestedInterests("cars", 15, "lessThan")));
+
+        // football interest is 15 on the profile
+        assertTrue(eval(buildConditionOnNestedInterests("football", 15, "equals")));
+        assertTrue(eval(buildConditionOnNestedInterests("football", 50, "notEquals")));
+        assertFalse(eval(buildConditionOnNestedInterests("football", 15, "notEquals")));
+        assertFalse(eval(buildConditionOnNestedInterests("football", 50, "equals")));
+    }
+
+    @Test
+    public void testNestedConditionEvaluator_emptyItem() {
+        // football cars is 50 on the profile
+        assertFalse(evalEmpty(buildConditionOnNestedInterests("cars", 45, "greaterThan")));
+        assertFalse(evalEmpty(buildConditionOnNestedInterests("cars", 60, "lessThan")));
+        assertFalse(evalEmpty(buildConditionOnNestedInterests("cars", 60, "greaterThan")));
+        assertFalse(evalEmpty(buildConditionOnNestedInterests("cars", 15, "lessThan")));
+
+        // football interest is 15 on the profile
+        assertFalse(evalEmpty(buildConditionOnNestedInterests("football", 15, "equals")));
+        assertFalse(evalEmpty(buildConditionOnNestedInterests("football", 50, "notEquals")));
+        assertFalse(evalEmpty(buildConditionOnNestedInterests("football", 15, "notEquals")));
+        assertFalse(evalEmpty(buildConditionOnNestedInterests("football", 50, "equals")));
+    }
+
+    protected Condition buildConditionOnNestedInterests(String interestName, Integer interestValue, String operator) {
+        Condition interestKeyCondition = new Condition(definitionsService.getConditionType("profilePropertyCondition"));
+        interestKeyCondition.setParameter("propertyName","properties.interests.key");
+        interestKeyCondition.setParameter("comparisonOperator","equals");
+        interestKeyCondition.setParameter("propertyValue", interestName);
+
+        Condition interestValueCondition = new Condition(definitionsService.getConditionType("profilePropertyCondition"));
+        interestValueCondition.setParameter("propertyName","properties.interests.value");
+        interestValueCondition.setParameter("comparisonOperator", operator);
+        interestValueCondition.setParameter("propertyValueInteger",interestValue);
+
+        Condition booleanCondition = new Condition(definitionsService.getConditionType("booleanCondition"));
+        booleanCondition.setParameter("operator", "and");
+        booleanCondition.setParameter("subConditions", Arrays.asList(interestKeyCondition, interestValueCondition));
+
+        Condition nestedCondition = new Condition(definitionsService.getConditionType("nestedCondition"));
+        nestedCondition.setParameter("path", "properties.interests");
+        nestedCondition.setParameter("subCondition", booleanCondition);
+        return nestedCondition;
+    }
 }
diff --git a/persistence-elasticsearch/core/pom.xml b/persistence-elasticsearch/core/pom.xml
index 4b7791a..acff540 100644
--- a/persistence-elasticsearch/core/pom.xml
+++ b/persistence-elasticsearch/core/pom.xml
@@ -112,7 +112,7 @@
         <dependency>
             <groupId>org.apache.lucene</groupId>
             <artifactId>lucene-test-framework</artifactId>
-            <version>8.2.0</version>
+            <version>${lucene.version}</version>
             <scope>test</scope>
             <exclusions>
                 <exclusion>
@@ -317,6 +317,7 @@
                         <Export-Package>
                             org.elasticsearch.*;version="${elasticsearch.version}",
                             org.elasticsearch.index.query.*;version="${elasticsearch.version}",
+                            org.apache.lucene.search.join.*;version="${lucene.version}",
                             org.apache.unomi.persistence.elasticsearch.conditions;version="${project.version}"
                         </Export-Package>
                         <Embed-Dependency>*;scope=compile|runtime</Embed-Dependency>
diff --git a/persistence-elasticsearch/core/src/main/resources/META-INF/cxs/mappings/profile.json b/persistence-elasticsearch/core/src/main/resources/META-INF/cxs/mappings/profile.json
index a484d91..81cc14d 100644
--- a/persistence-elasticsearch/core/src/main/resources/META-INF/cxs/mappings/profile.json
+++ b/persistence-elasticsearch/core/src/main/resources/META-INF/cxs/mappings/profile.json
@@ -34,6 +34,9 @@
         },
         "nbOfVisits": {
           "type": "long"
+        },
+        "interests": {
+          "type": "nested"
         }
       }
     },
diff --git a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/NestedConditionESQueryBuilder.java b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/NestedConditionESQueryBuilder.java
new file mode 100644
index 0000000..e8c3701
--- /dev/null
+++ b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/NestedConditionESQueryBuilder.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.unomi.plugins.baseplugin.conditions;
+
+import org.apache.lucene.search.join.ScoreMode;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.persistence.elasticsearch.conditions.ConditionESQueryBuilder;
+import org.apache.unomi.persistence.elasticsearch.conditions.ConditionESQueryBuilderDispatcher;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+
+import java.util.Map;
+
+public class NestedConditionESQueryBuilder implements ConditionESQueryBuilder {
+    @Override
+    public QueryBuilder buildQuery(Condition condition, Map<String, Object> context, ConditionESQueryBuilderDispatcher dispatcher) {
+        String path = (String) condition.getParameter("path");
+        Condition subCondition = (Condition) condition.getParameter("subCondition");
+
+        if (subCondition == null || path == null) {
+            throw new IllegalArgumentException("Impossible to build Nested query, subCondition and path properties should be provided");
+        }
+
+        QueryBuilder nestedQueryBuilder = dispatcher.buildFilter(subCondition, context);
+        if (nestedQueryBuilder != null) {
+            return QueryBuilders.nestedQuery(path, nestedQueryBuilder, ScoreMode.Avg);
+        } else {
+            throw new IllegalArgumentException("Impossible to build Nested query due to subCondition filter null");
+        }
+    }
+}
diff --git a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/NestedConditionEvaluator.java b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/NestedConditionEvaluator.java
new file mode 100644
index 0000000..1bbba63
--- /dev/null
+++ b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/NestedConditionEvaluator.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.unomi.plugins.baseplugin.conditions;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.Item;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.Session;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluator;
+import org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluatorDispatcher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+
+public class NestedConditionEvaluator implements ConditionEvaluator {
+
+    private static final Logger logger = LoggerFactory.getLogger(NestedConditionEvaluator.class.getName());
+
+    PropertyConditionEvaluator propertyConditionEvaluator;
+
+    public void setPropertyConditionEvaluator(PropertyConditionEvaluator propertyConditionEvaluator) {
+        this.propertyConditionEvaluator = propertyConditionEvaluator;
+    }
+
+    @Override
+    public boolean eval(Condition condition, Item item, Map<String, Object> context, ConditionEvaluatorDispatcher dispatcher) {
+        String path = (String) condition.getParameter("path");
+        Condition subCondition = (Condition) condition.getParameter("subCondition");
+
+        if (subCondition == null || path == null) {
+            throw new IllegalArgumentException("Impossible to build Nested evaluator, subCondition and path properties should be provided");
+        }
+
+
+        try {
+            // Get list of nested items to be evaluated
+            Object nestedItems = propertyConditionEvaluator.getPropertyValue(item, path);
+            if (nestedItems instanceof List) {
+
+                // Evaluated each nested items until one match the nested condition
+                for (Object nestedItem : (List<Object>) nestedItems) {
+                    if (nestedItem instanceof Map) {
+                        Map<String, Object> flattenedNestedItem = flattenNestedItem(path, (Map<String, Object>) nestedItem);
+                        Item finalNestedItem = createFinalNestedItemForEvaluation(item, path, flattenedNestedItem);
+                        if (finalNestedItem != null && dispatcher.eval(subCondition, finalNestedItem, context)) {
+                            // We found at least one nested item matching
+                            return true;
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            logger.error("Failed to evaluated nested condition", e);
+            return false;
+        }
+        return false;
+    }
+
+    protected Map<String, Object> flattenNestedItem(String path, Map<String, Object> nestedItem) {
+        Map<String, Object> flattenNestedItem = new HashMap<>();
+
+        // Merge the nested Item in flat properties
+        if (StringUtils.isNotEmpty(path)) {
+            // substring to keep only the part after "properties".
+            // For example in case of "properties.interests", we only want to keep "interests"
+            String propertyPath = StringUtils.substringAfter(path, ".");
+            if (StringUtils.isNotEmpty(propertyPath)) {
+                String[] propertyKeys = propertyPath.split("\\.");
+                Iterator<String> propertyKeysIterator = Arrays.stream(propertyKeys).iterator();
+
+                Map<String, Object> currentPropertiesLevel = flattenNestedItem;
+                while (propertyKeysIterator.hasNext()) {
+                    String propertyKey = propertyKeysIterator.next();
+                    if (!propertyKeysIterator.hasNext()) {
+                        // last property, we set the nested Item
+                        currentPropertiesLevel.put(propertyKey, nestedItem);
+                    } else {
+                        // new level of prop
+                        Map<String, Object> subLevel = new HashMap<>();
+                        currentPropertiesLevel.put(propertyKey, subLevel);
+                        currentPropertiesLevel = subLevel;
+                    }
+                }
+            }
+        }
+
+        return flattenNestedItem;
+    }
+
+    protected Item createFinalNestedItemForEvaluation(Item parentItem, String path, Map<String, Object> flattenedNestedItem) {
+        // Build a basic Item with merged properties
+        if (parentItem instanceof Profile) {
+            Profile profile = new Profile(parentItem.getItemId());
+            if (path.startsWith("properties.")) {
+                profile.setProperties(flattenedNestedItem);
+            } else if (path.startsWith("systemProperties.")) {
+                profile.setSystemProperties(flattenedNestedItem);
+            }
+            return profile;
+        } else if (parentItem instanceof Session) {
+            Session session = new Session();
+            if (path.startsWith("properties.")) {
+                session.setProperties(flattenedNestedItem);
+            } else if (path.startsWith("systemProperties.")) {
+                session.setSystemProperties(flattenedNestedItem);
+            }
+            return session;
+        }
+
+        return null;
+    }
+}
diff --git a/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/nestedCondition.json b/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/nestedCondition.json
new file mode 100644
index 0000000..6fd44ac
--- /dev/null
+++ b/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/nestedCondition.json
@@ -0,0 +1,29 @@
+{
+  "metadata": {
+    "id": "nestedCondition",
+    "name": "nestedCondition",
+    "description": "",
+    "systemTags": [
+      "profileTags",
+      "logical",
+      "condition",
+      "profileCondition",
+      "sessionCondition"
+    ],
+    "readOnly": true
+  },
+  "conditionEvaluator": "nestedConditionEvaluator",
+  "queryBuilder": "nestedConditionESQueryBuilder",
+  "parameters": [
+    {
+      "id": "path",
+      "type": "String",
+      "multivalued": false
+    },
+    {
+      "id": "subCondition",
+      "type": "Condition",
+      "multivalued": false
+    }
+  ]
+}
\ No newline at end of file
diff --git a/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index b80f250..15f6766 100644
--- a/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -131,6 +131,14 @@
         </bean>
     </service>
 
+    <service
+            interface="org.apache.unomi.persistence.elasticsearch.conditions.ConditionESQueryBuilder">
+        <service-properties>
+            <entry key="queryBuilderId" value="nestedConditionESQueryBuilder"/>
+        </service-properties>
+        <bean class="org.apache.unomi.plugins.baseplugin.conditions.NestedConditionESQueryBuilder"/>
+    </service>
+
 
     <!-- Condition evaluators -->
     <service interface="org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluator">
@@ -154,13 +162,13 @@
         <bean class="org.apache.unomi.plugins.baseplugin.conditions.NotConditionEvaluator"/>
     </service>
 
-    <service interface="org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluator">
+    <bean id="propertyConditionEvaluator" class="org.apache.unomi.plugins.baseplugin.conditions.PropertyConditionEvaluator">
+        <property name="usePropertyConditionOptimizations" value="${base.usePropertyConditionOptimizations}"/>
+    </bean>
+    <service  interface="org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluator" ref="propertyConditionEvaluator">
         <service-properties>
             <entry key="conditionEvaluatorId" value="propertyConditionEvaluator"/>
         </service-properties>
-        <bean class="org.apache.unomi.plugins.baseplugin.conditions.PropertyConditionEvaluator">
-            <property name="usePropertyConditionOptimizations" value="${base.usePropertyConditionOptimizations}"/>
-        </bean>
     </service>
 
     <service interface="org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluator">
@@ -198,6 +206,15 @@
         </bean>
     </service>
 
+    <service interface="org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluator">
+        <service-properties>
+            <entry key="conditionEvaluatorId" value="nestedConditionEvaluator"/>
+        </service-properties>
+        <bean class="org.apache.unomi.plugins.baseplugin.conditions.NestedConditionEvaluator">
+            <property name="propertyConditionEvaluator" ref="propertyConditionEvaluator"/>
+        </bean>
+    </service>
+
 
     <!-- Action executors -->
 
diff --git a/plugins/baseplugin/src/test/java/org/apache/unomi/plugins/baseplugin/conditions/NestedConditionEvaluatorTest.java b/plugins/baseplugin/src/test/java/org/apache/unomi/plugins/baseplugin/conditions/NestedConditionEvaluatorTest.java
new file mode 100644
index 0000000..227a460
--- /dev/null
+++ b/plugins/baseplugin/src/test/java/org/apache/unomi/plugins/baseplugin/conditions/NestedConditionEvaluatorTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.unomi.plugins.baseplugin.conditions;
+
+import org.apache.unomi.api.Event;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.Session;
+import org.junit.Test;
+
+import java.util.*;
+
+import static junit.framework.TestCase.*;
+
+public class NestedConditionEvaluatorTest {
+    private final NestedConditionEvaluator nestedConditionEvaluator = new NestedConditionEvaluator();
+
+    @Test
+    public void testFlattenedNestedItem_singleLevel() {
+        Map<String, Object> result = nestedConditionEvaluator.flattenNestedItem("properties.interests", buildNestedInterest("football", 15));
+        Map<String, Object> mergeResultProperties = (Map<String, Object>) result.get("interests");
+
+        assertEquals("The interest should have been flattened in the profile", "football", mergeResultProperties.get("key"));
+        assertEquals("The interest should have been flattened in the profile", 15, mergeResultProperties.get("value"));
+    }
+
+    @Test
+    public void testFlattenedNestedItem_multipleLevel() {
+        Map<String, Object> result = nestedConditionEvaluator.flattenNestedItem("properties.subLevel.subLevel2.interests", buildNestedInterest("football", 15));
+        Map<String, Object> subLevelResult = (Map<String, Object>) result.get("subLevel");
+        Map<String, Object> subLevel2Result = (Map<String, Object>) subLevelResult.get("subLevel2");
+        Map<String, Object> interestsResult = (Map<String, Object>) subLevel2Result.get("interests");
+
+        assertEquals("The interest should have been flattened in the profile", "football", interestsResult.get("key"));
+        assertEquals("The interest should have been flattened in the profile", 15, interestsResult.get("value"));
+    }
+
+    @Test
+    public void testFlattenedNestedItem_invalidPaths() {
+        assertTrue(nestedConditionEvaluator.flattenNestedItem("properties.", buildNestedInterest("football", 15)).isEmpty());
+        assertTrue(nestedConditionEvaluator.flattenNestedItem("", buildNestedInterest("football", 15)).isEmpty());
+        assertTrue(nestedConditionEvaluator.flattenNestedItem(null, buildNestedInterest("football", 15)).isEmpty());
+    }
+
+    @Test
+    public void testCreateFinalNestedItem_Profile() {
+        Map<String, Object> testMap = new HashMap<>();
+        testMap.put("foo", "bar");
+        
+        Profile profile = (Profile) nestedConditionEvaluator.createFinalNestedItemForEvaluation(new Profile(), "properties.test", testMap);
+        assertEquals("bar", profile.getProperties().get("foo"));
+        assertTrue(profile.getSystemProperties().isEmpty());
+
+        profile = (Profile) nestedConditionEvaluator.createFinalNestedItemForEvaluation(new Profile(), "systemProperties.test", testMap);
+        assertEquals("bar", profile.getSystemProperties().get("foo"));
+        assertTrue(profile.getProperties().isEmpty());
+
+        profile = (Profile) nestedConditionEvaluator.createFinalNestedItemForEvaluation(new Profile(), "invalidPath", testMap);
+        assertTrue(profile.getSystemProperties().isEmpty());
+        assertTrue(profile.getProperties().isEmpty());
+    }
+
+    @Test
+    public void testCreateFinalNestedItem_Session() {
+        Map<String, Object> testMap = new HashMap<>();
+        testMap.put("foo", "bar");
+
+        Session session = (Session) nestedConditionEvaluator.createFinalNestedItemForEvaluation(new Session(), "properties.test", testMap);
+        assertEquals("bar", session.getProperties().get("foo"));
+        assertTrue(session.getSystemProperties().isEmpty());
+
+        session = (Session) nestedConditionEvaluator.createFinalNestedItemForEvaluation(new Session(), "systemProperties.test", testMap);
+        assertEquals("bar", session.getSystemProperties().get("foo"));
+        assertTrue(session.getProperties().isEmpty());
+
+        session = (Session) nestedConditionEvaluator.createFinalNestedItemForEvaluation(new Session(), "invalidPath", testMap);
+        assertTrue(session.getSystemProperties().isEmpty());
+        assertTrue(session.getProperties().isEmpty());
+    }
+
+    @Test
+    public void testCreateFinalNestedItem_unsupportedType() {
+        Map<String, Object> testMap = new HashMap<>();
+        testMap.put("foo", "bar");
+
+        Event segment = (Event) nestedConditionEvaluator.createFinalNestedItemForEvaluation(new Event(), "properties.test", testMap);
+        assertNull(segment);
+    }
+
+    private Map<String, Object> buildNestedInterest(String key, Object value) {
+        Map<String, Object> nestedInterest = new HashMap<>();
+        nestedInterest.put("key", key);
+        nestedInterest.put("value", value);
+        return nestedInterest;
+    }
+}
diff --git a/pom.xml b/pom.xml
index ee5abe5..f9d867c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -73,6 +73,7 @@
         <version.pax.exam>4.13.1</version.pax.exam>
         <elasticsearch.version>7.4.2</elasticsearch.version>
         <elasticsearch.test.version>7.11.0</elasticsearch.test.version>
+        <lucene.version>8.2.0</lucene.version>
         <groovy.version>3.0.3</groovy.version>
         <networknt.version>1.0.49</networknt.version>
         <bean.validation.version>1.1.0.Final</bean.validation.version>