You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by ta...@apache.org on 2021/03/29 14:02:08 UTC

[unomi] branch master updated: UNOMI-446 improve increment action and make it generic (#265)

This is an automated email from the ASF dual-hosted git repository.

taybou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/master by this push:
     new 486aff8  UNOMI-446 improve increment action and make it generic (#265)
486aff8 is described below

commit 486aff8571694a9b57118fcb3425fa4f97cb0da3
Author: MT BENTERKI <be...@gmail.com>
AuthorDate: Mon Mar 29 16:00:20 2021 +0200

    UNOMI-446 improve increment action and make it generic (#265)
---
 .../test/java/org/apache/unomi/itests/AllITs.java  |   2 +
 .../test/java/org/apache/unomi/itests/BasicIT.java |   2 +-
 .../apache/unomi/itests/IncrementPropertyIT.java   | 424 +++++++++++++++++++++
 .../actions/IncrementPropertyAction.java           | 137 +++++++
 .../META-INF/cxs/actions/incrementProperty.json    |  29 ++
 .../resources/OSGI-INF/blueprint/blueprint.xml     |   6 +
 .../services/actions/ActionExecutorDispatcher.java |  43 ++-
 7 files changed, 633 insertions(+), 10 deletions(-)

diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
index bd36ac0..ea3312a 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -43,6 +43,8 @@ import org.junit.runners.Suite.SuiteClasses;
         EventServiceIT.class,
         PropertiesUpdateActionIT.class,
         CopyPropertiesActionIT.class,
+        IncrementPropertyIT.class,
+        IncrementInterestsIT.class,
         ModifyConsentIT.class,
         PatchIT.class,
         ContextServletIT.class,
diff --git a/itests/src/test/java/org/apache/unomi/itests/BasicIT.java b/itests/src/test/java/org/apache/unomi/itests/BasicIT.java
index 1f7299e..64286b8 100644
--- a/itests/src/test/java/org/apache/unomi/itests/BasicIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/BasicIT.java
@@ -72,7 +72,7 @@ public class BasicIT extends BaseIT {
     private static final String ITEM_TYPE_SITE = "site";
     private static final String ITEM_ID_SITE = "/test/site";
     private static final String ITEM_TYPE_VISITOR = "VISITOR";
-    private static final String ITEM_ID_PAGE_1 = "/test/site/page1";
+    protected static final String ITEM_ID_PAGE_1 = "/test/site/page1";
     protected static final String ITEM_TYPE_PAGE = "page";
 
     private static final String FIRST_NAME = "firstName";
diff --git a/itests/src/test/java/org/apache/unomi/itests/IncrementPropertyIT.java b/itests/src/test/java/org/apache/unomi/itests/IncrementPropertyIT.java
new file mode 100644
index 0000000..4488fb9
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/IncrementPropertyIT.java
@@ -0,0 +1,424 @@
+/*
+ * 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.itests;
+
+import java.util.*;
+
+import javax.inject.Inject;
+
+import org.apache.unomi.api.*;
+import org.apache.unomi.api.actions.Action;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.rules.Rule;
+import org.apache.unomi.api.services.DefinitionsService;
+import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.api.services.RulesService;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerSuite;
+import org.ops4j.pax.exam.util.Filter;
+
+import static org.apache.unomi.itests.BasicIT.ITEM_TYPE_PAGE;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
+public class IncrementPropertyIT extends BaseIT {
+    private Profile profile;
+    private Rule rule;
+    private Event event;
+
+    @Inject
+    @Filter(timeout = 600000)
+    protected ProfileService profileService;
+
+    @Inject
+    @Filter(timeout = 600000)
+    protected EventService eventService;
+
+    @Inject
+    @Filter(timeout = 600000)
+    protected RulesService rulesService;
+
+    @Inject
+    @Filter(timeout = 600000)
+    protected DefinitionsService definitionsService;
+
+    @Before
+    public void setup() throws Exception {
+        profile = createProfile();
+        rule = new Rule();
+    }
+
+    @After
+    public void tearDown() {
+        rulesService.removeRule(rule.getItemId());
+        profileService.delete(profile.getItemId(), false);
+    }
+
+    @Test
+    public void testIncrementNotExistingPropertyWithDynamicName() throws InterruptedException {
+        int eventCode = buildActionAndSendEvent("pageView.${eventProperty::target.scope}", null, null, null);
+
+        if (eventCode == EventService.PROFILE_UPDATED) {
+            Profile updatedProfile = profileService.save(event.getProfile());
+            refreshPersistence();
+
+            int value = ((Map<String, Integer>) updatedProfile.getProperty("pageView")).get("acme-space");
+            Assert.assertEquals(1, value, 0.0);
+        } else {
+            Assert.fail("Profile was not updated");
+        }
+    }
+
+    @Test
+    public void testIncrementExistingPropertyWithDynamicName() throws InterruptedException {
+        Map<String, Object> properties = new HashMap<>();
+        Map<String, Integer> propertyValue = new HashMap<>();
+        propertyValue.put("acme-space", 24);
+        properties.put("pageView", propertyValue);
+
+        int eventCode = buildActionAndSendEvent("pageView.${eventProperty::target.scope}", null, properties, null);
+
+        if (eventCode == EventService.PROFILE_UPDATED) {
+            Profile updatedProfile = profileService.save(event.getProfile());
+
+            int value = ((Map<String, Integer>) updatedProfile.getProperty("pageView")).get("acme-space");
+            Assert.assertEquals(25, value, 0.0);
+        } else {
+            Assert.fail("Profile was not updated");
+        }
+    }
+
+    @Test
+    public void testIncrementNotExistingProperty() throws InterruptedException {
+        int eventCode = buildActionAndSendEvent("pageView.acme", null, null, null);
+
+        if (eventCode == EventService.PROFILE_UPDATED) {
+            Profile updatedProfile = profileService.save(event.getProfile());
+
+            int value = ((Map<String, Integer>) updatedProfile.getProperty("pageView")).get("acme");
+            Assert.assertEquals(1, value, 0.0);
+        } else {
+            Assert.fail("Profile was not updated");
+        }
+    }
+
+    @Test
+    public void testIncrementExistingProperty() throws InterruptedException {
+        Map<String, Object> properties = new HashMap<>();
+        Map<String, Integer> propertyValue = new HashMap<>();
+        propertyValue.put("acme", 49);
+        properties.put("pageView", propertyValue);
+
+        int eventCode = buildActionAndSendEvent("pageView.acme", null, properties, null);
+
+        if (eventCode == EventService.PROFILE_UPDATED) {
+            Profile updatedProfile = profileService.save(event.getProfile());
+
+            int value = ((Map<String, Integer>) updatedProfile.getProperty("pageView")).get("acme");
+            Assert.assertEquals(50, value, 0.0);
+        } else {
+            Assert.fail("Profile was not updated");
+        }
+    }
+
+    @Test
+    public void testIncrementExistingPropertyWithExistingEventProperty() throws InterruptedException {
+        Map<String, Object> properties = new HashMap<>();
+        Map<String, Integer> propertyValue = new HashMap<>();
+        propertyValue.put("acme", 49);
+        properties.put("pageView", propertyValue);
+
+        Map<String, Object> targetProperties = new HashMap<>();
+        propertyValue = new HashMap<>();
+        propertyValue.put("nasa", 19);
+        targetProperties.put("project", propertyValue);
+
+        int eventCode = buildActionAndSendEvent("pageView.acme", "project.nasa", properties, targetProperties);
+
+        if (eventCode == EventService.PROFILE_UPDATED) {
+            Profile updatedProfile = profileService.save(event.getProfile());
+
+            int value = ((Map<String, Integer>) updatedProfile.getProperty("pageView")).get("acme");
+            Assert.assertEquals(68, value, 0.0);
+        } else {
+            Assert.fail("Profile was not updated");
+        }
+    }
+
+    @Test
+    public void testIncrementNotExistingObjectPropertyWithExistingEventObjectProperty() throws InterruptedException {
+        Map<String, Object> targetProperties = new HashMap<>();
+        Map<String, Integer> propertyValue = new HashMap<>();
+        propertyValue.put("acme", 49);
+        propertyValue.put("health", 18);
+        propertyValue.put("sport", 99);
+        targetProperties.put("pageView", propertyValue);
+
+        int eventCode = buildActionAndSendEvent("pageView", "pageView", null, targetProperties);
+
+        if (eventCode == EventService.PROFILE_UPDATED) {
+            Profile updatedProfile = profileService.save(event.getProfile());
+
+            Map<String, Integer> property = ((Map<String, Integer>) updatedProfile.getProperty("pageView"));
+            Assert.assertEquals(49, property.get("acme"), 0.0);
+            Assert.assertEquals(18, property.get("health"), 0.0);
+            Assert.assertEquals(99, property.get("sport"), 0.0);
+        } else {
+            Assert.fail("Profile was not updated");
+        }
+    }
+
+    @Test
+    public void testIncrementExistingObjectProperty() throws InterruptedException {
+        Map<String, Object> properties = new HashMap<>();
+        Map<String, Integer> propertyValue = new HashMap<>();
+        propertyValue.put("acme", 49);
+        propertyValue.put("nasa", 5);
+        properties.put("pageView", propertyValue);
+
+        int eventCode = buildActionAndSendEvent("pageView", null, properties, null);
+
+        if (eventCode == EventService.PROFILE_UPDATED) {
+            Profile updatedProfile = profileService.save(event.getProfile());
+
+            Map<String, Integer> property = ((Map<String, Integer>) updatedProfile.getProperty("pageView"));
+            Assert.assertEquals(50, property.get("acme"), 0.0);
+            Assert.assertEquals(6, property.get("nasa"), 0.0);
+        } else {
+            Assert.fail("Profile was not updated");
+        }
+    }
+
+    @Test
+    public void testIncrementExistingObjectPropertyWithExistingEventObjectProperty() throws InterruptedException {
+        Map<String, Object> properties = new HashMap<>();
+        Map<String, Integer> propertyValue = new HashMap<>();
+        propertyValue.put("acme", 49);
+        properties.put("pageView", propertyValue);
+
+        Map<String, Object> targetProperties = new HashMap<>();
+        Map<String, Integer> propertyValue1 = new HashMap<>();
+        propertyValue1.put("acme", 31);
+        propertyValue1.put("health", 88);
+        propertyValue1.put("sport", 9);
+        targetProperties.put("pageView", propertyValue1);
+
+        int eventCode = buildActionAndSendEvent("pageView", "pageView", properties, targetProperties);
+
+        if (eventCode == EventService.PROFILE_UPDATED) {
+            Profile updatedProfile = profileService.save(event.getProfile());
+
+            Map<String, Integer> property = ((Map<String, Integer>) updatedProfile.getProperty("pageView"));
+            Assert.assertEquals(80, property.get("acme"), 0.0);
+            Assert.assertEquals(88, property.get("health"), 0.0);
+            Assert.assertEquals(9, property.get("sport"), 0.0);
+        } else {
+            Assert.fail("Profile was not updated");
+        }
+    }
+
+    @Test
+    public void testIncrementExistingPropertyNested() throws InterruptedException {
+        Map<String, Object> properties = new HashMap<>();
+        Map<String, Object> properties1 = new HashMap<>();
+        Map<String, Object> properties2 = new HashMap<>();
+        Map<String, Integer> propertyValue = new HashMap<>();
+        propertyValue.put("city", 13);
+        properties2.put("state", propertyValue);
+        properties1.put("country", properties2);
+        properties.put("continent", properties1);
+
+        int eventCode = buildActionAndSendEvent("continent.country.state.city", null, properties, null);
+
+        if (eventCode == EventService.PROFILE_UPDATED) {
+            Profile updatedProfile = profileService.save(event.getProfile());
+
+            Map<String, Integer> property = (Map<String, Integer>) ((Map<String, Object>) ((Map<String, Object>) updatedProfile.getProperty("continent")).get("country")).get("state");
+            Assert.assertEquals(14, property.get("city"), 0.0);
+        } else {
+            Assert.fail("Profile was not updated");
+        }
+    }
+
+    @Test
+    public void testIncrementNotExistingPropertyNested() throws InterruptedException {
+        int eventCode = buildActionAndSendEvent("continent.country.state.city", null, null, null);
+
+        if (eventCode == EventService.PROFILE_UPDATED) {
+            Profile updatedProfile = profileService.save(event.getProfile());
+
+            Map<String, Integer> property = (Map<String, Integer>) ((Map<String, Object>) ((Map<String, Object>) updatedProfile.getProperty("continent")).get("country")).get("state");
+            Assert.assertEquals(1, property.get("city"), 0.0);
+        } else {
+            Assert.fail("Profile was not updated");
+        }
+    }
+
+    @Test
+    public void testIncrementExistingPropertyNestedWithExistingEventProperty() throws InterruptedException {
+        Map<String, Object> properties = new HashMap<>();
+        Map<String, Object> properties1 = new HashMap<>();
+        Map<String, Object> properties2 = new HashMap<>();
+        Map<String, Integer> propertyValue = new HashMap<>();
+        propertyValue.put("city", 13);
+        properties2.put("state", propertyValue);
+        properties1.put("country", properties2);
+        properties.put("continent", properties1);
+
+        Map<String, Object> targetProperties = new HashMap<>();
+        Map<String, Object> properties3 = new HashMap<>();
+        Map<String, Object> propertyValue1 = new HashMap<>();
+        propertyValue1.put("zone", 107);
+        properties3.put("mars", propertyValue1);
+        targetProperties.put("planet", properties3);
+
+        int eventCode = buildActionAndSendEvent("continent.country.state.city", "planet.mars.zone", properties, targetProperties);
+
+        if (eventCode == EventService.PROFILE_UPDATED) {
+            Profile updatedProfile = profileService.save(event.getProfile());
+
+            Map<String, Integer> property = (Map<String, Integer>) ((Map<String, Object>) ((Map<String, Object>) updatedProfile.getProperty("continent")).get("country")).get("state");
+            Assert.assertEquals(120, property.get("city"), 0.0);
+        } else {
+            Assert.fail("Profile was not updated");
+        }
+    }
+
+    @Test
+    public void testIncrementObjectPropertyContainsStringValue() throws InterruptedException {
+        Map<String, Object> properties = new HashMap<>();
+        Map<String, Object> propertyValue = new HashMap<>();
+        propertyValue.put("books", 59);
+        propertyValue.put("chapters", 1001);
+        propertyValue.put("featured", "The forty rules");
+        properties.put("library", propertyValue);
+
+        int eventCode = buildActionAndSendEvent("library", null, properties, null);
+
+        if (eventCode == EventService.PROFILE_UPDATED) {
+            Profile updatedProfile = profileService.save(event.getProfile());
+
+            Map<String, Object> property = ((Map<String, Object>) updatedProfile.getProperty("library"));
+            Assert.assertEquals(60, (int) property.get("books"), 0.0);
+            Assert.assertEquals(1002, (int) property.get("chapters"), 0.0);
+            Assert.assertEquals("The forty rules", property.get("featured"));
+        } else {
+            Assert.fail("Profile was not updated");
+        }
+    }
+
+    @Test
+    public void testIncrementObjectPropertyContainsStringValueWithExistingEventProperty() throws InterruptedException {
+        Map<String, Object> properties = new HashMap<>();
+        Map<String, Object> propertyValue = new HashMap<>();
+        propertyValue.put("books", 59);
+        propertyValue.put("chapters", 1001);
+        propertyValue.put("featured", "The forty rules");
+        properties.put("library", propertyValue);
+
+        Map<String, Object> targetProperties = new HashMap<>();
+        Map<String, Object> properties1 = new HashMap<>();
+        Map<String, Object> propertyValue1 = new HashMap<>();
+        propertyValue1.put("books", 222);
+        propertyValue1.put("chapters", 2048);
+        propertyValue1.put("featured", "Bible");
+        properties1.put("library", propertyValue1);
+        targetProperties.put("main", properties1);
+
+        int eventCode = buildActionAndSendEvent("library", "main.library", properties, targetProperties);
+
+        if (eventCode == EventService.PROFILE_UPDATED) {
+            Profile updatedProfile = profileService.save(event.getProfile());
+
+            Map<String, Object> property = ((Map<String, Object>) updatedProfile.getProperty("library"));
+            Assert.assertEquals(281, (int) property.get("books"), 0.0);
+            Assert.assertEquals(3049, (int) property.get("chapters"), 0.0);
+            Assert.assertEquals("The forty rules", property.get("featured"));
+        } else {
+            Assert.fail("Profile was not updated");
+        }
+    }
+
+    private void createRule(Action incrementPropertyAction) throws InterruptedException {
+        Condition condition = createCondition();
+        Metadata metadata = createMetadata();
+
+        List<Action> actions = new ArrayList<>();
+        actions.add(incrementPropertyAction);
+
+        rule.setCondition(condition);
+        rule.setActions(actions);
+        rule.setMetadata(metadata);
+        rulesService.setRule(rule);
+        refreshPersistence();
+    }
+
+    private int buildActionAndSendEvent(String propertyName, String propertyTargetName, Map<String, Object> properties, Map<String, Object> targetProperties) throws InterruptedException {
+        Action incrementPropertyAction = new Action(definitionsService.getActionType("incrementPropertyAction"));
+        incrementPropertyAction.setParameter("propertyName", propertyName);
+        if (propertyTargetName != null) incrementPropertyAction.setParameter("propertyTarget", propertyTargetName);
+
+        createRule(incrementPropertyAction);
+
+        if (properties != null) profile.setProperties(properties);
+
+        CustomItem target = new CustomItem("ITEM_ID_PAGE", ITEM_TYPE_PAGE);
+        target.setScope("acme-space");
+        if (targetProperties != null) target.setProperties(targetProperties);
+
+        event = new Event("view", null, profile, null, null, target, new Date());
+        event.setPersistent(false);
+
+        int eventCode = eventService.send(event);
+        refreshPersistence();
+
+        return eventCode;
+    }
+
+    private Metadata createMetadata() {
+        String itemId = UUID.randomUUID().toString();
+        Metadata metadata = new Metadata();
+        metadata.setId(itemId);
+        metadata.setName(itemId);
+        metadata.setDescription(itemId);
+        metadata.setEnabled(true);
+        metadata.setScope("systemscope");
+        return metadata;
+    }
+
+    private Condition createCondition() {
+        Condition condition = new Condition(definitionsService.getConditionType("eventTypeCondition"));
+        condition.setParameter("eventTypeId", "view");
+        return condition;
+    }
+
+    private Profile createProfile() throws InterruptedException {
+        Profile profile = new Profile(UUID.randomUUID().toString());
+
+        profileService.save(profile);
+        refreshPersistence();
+
+        return profile;
+    }
+}
diff --git a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/IncrementPropertyAction.java b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/IncrementPropertyAction.java
new file mode 100644
index 0000000..3651ece
--- /dev/null
+++ b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/IncrementPropertyAction.java
@@ -0,0 +1,137 @@
+/*
+ * 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.actions;
+
+import org.apache.commons.beanutils.PropertyUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.*;
+import org.apache.unomi.api.actions.Action;
+import org.apache.unomi.api.actions.ActionExecutor;
+import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.persistence.spi.PropertyHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Map;
+
+public class IncrementPropertyAction implements ActionExecutor {
+    private static final Logger logger = LoggerFactory.getLogger(IncrementPropertyAction.class.getName());
+
+    @Override
+    public int execute(final Action action, final Event event) {
+        boolean storeInSession = Boolean.TRUE.equals(action.getParameterValues().get("storeInSession"));
+        if (storeInSession && event.getSession() == null) {
+            return EventService.NO_CHANGE;
+        }
+
+        String propertyName = (String) action.getParameterValues().get("propertyName");
+        Profile profile = event.getProfile();
+        Session session = event.getSession();
+
+        try {
+            Map<String, Object> properties = storeInSession ? session.getProperties() : profile.getProperties();
+            Object propertyValue = getPropertyValue(action, event, propertyName, properties);
+            if (PropertyHelper.setProperty(properties, propertyName, propertyValue, "alwaysSet")) {
+                return storeInSession ? EventService.SESSION_UPDATED : EventService.PROFILE_UPDATED;
+            }
+        } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
+            logger.warn("Error resolving nested property of object. See debug log level for more information");
+            if (logger.isDebugEnabled()) {
+                logger.debug("Error resolving nested property of item: {}", storeInSession ? session : profile, e);
+            }
+        } catch (IllegalStateException ee) {
+            logger.warn("Error increment existing property, because existing property doesn't have expected type. See debug log level for more information");
+            if (logger.isDebugEnabled()) {
+                logger.debug(ee.getMessage(), ee);
+            }
+        }
+
+        return EventService.NO_CHANGE;
+    }
+
+    private Object getPropertyValue(Action action, Event event, String propertyName, Map<String, Object> properties)
+            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
+        String propertyTarget = (String) action.getParameterValues().get("propertyTarget");
+        String rootPropertyName = propertyName.split("\\.")[0];
+        Object propertyValue = 1;
+
+        Object propertyTargetValue = null;
+
+        if (StringUtils.isNotEmpty(propertyTarget)) {
+            propertyTargetValue = PropertyUtils.getNestedProperty(((CustomItem) event.getTarget()).getProperties(), propertyTarget);
+        }
+
+        if (propertyTargetValue != null) {
+            if (propertyTargetValue instanceof Integer) {
+                if (properties.containsKey(rootPropertyName)) {
+                    Object nestedProperty = PropertyUtils.getNestedProperty(properties, propertyName);
+                    if (nestedProperty == null) {
+                        propertyValue = propertyTargetValue;
+                    } else if (nestedProperty instanceof Integer) {
+                        propertyValue = (int) propertyTargetValue + (int) nestedProperty;
+                    } else {
+                        throw new IllegalStateException("Property: " + propertyName + " already exist, can not increment the property because the exiting property is not integer");
+                    }
+                } else {
+                    propertyValue = propertyTargetValue;
+                }
+            } else if (propertyTargetValue instanceof Map) {
+                if (properties.containsKey(rootPropertyName)) {
+                    Object nestedPropertyValue = PropertyUtils.getNestedProperty(properties, propertyName);
+                    if (nestedPropertyValue == null) {
+                        propertyValue = propertyTargetValue;
+                    } else if (nestedPropertyValue instanceof Map) {
+                        Map<String, Object> nestedProperty = (Map<String, Object>) nestedPropertyValue;
+
+                        ((Map<String, Object>) propertyTargetValue).forEach((key, targetValue) -> {
+                            if ((targetValue instanceof Integer && (nestedProperty.containsKey(key) && nestedProperty.get(key) instanceof Integer)) ||
+                                    (targetValue instanceof Integer && !nestedProperty.containsKey(key))) {
+                                nestedProperty.put(key, nestedProperty.containsKey(key) ? (int) nestedProperty.get(key) + (int) targetValue : targetValue);
+                            }
+                        });
+                        propertyValue = nestedProperty;
+                    } else {
+                        throw new IllegalStateException("Property: " + propertyName + " already exist, can not increment the properties from the map because the exiting property is not map");
+                    }
+                } else {
+                    propertyValue = propertyTargetValue;
+                }
+            }
+        } else {
+            if (properties.containsKey(rootPropertyName)) {
+                Object nestedProperty = PropertyUtils.getNestedProperty(properties, propertyName);
+                if (nestedProperty == null) {
+                    propertyValue = 1;
+                } else if (nestedProperty instanceof Integer) {
+                    propertyValue = (int) nestedProperty + 1;
+                } else if (nestedProperty instanceof Map) {
+                    ((Map<String, Object>) nestedProperty).forEach((key, propValue) -> {
+                        if (propValue instanceof Integer) {
+                            ((Map<String, Integer>) nestedProperty).merge(key, 1, Integer::sum);
+                        }
+                    });
+                    propertyValue = nestedProperty;
+                } else {
+                    throw new IllegalStateException("Property: " + propertyName + " already exist, can not increment the property because the exiting property is not integer or map");
+                }
+            }
+        }
+
+        return propertyValue;
+    }
+}
diff --git a/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/incrementProperty.json b/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/incrementProperty.json
new file mode 100644
index 0000000..3ba8828
--- /dev/null
+++ b/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/incrementProperty.json
@@ -0,0 +1,29 @@
+{
+  "metadata": {
+    "id": "incrementPropertyAction",
+    "name": "incrementPropertyAction",
+    "description": "",
+    "systemTags": [
+      "profileTags"
+    ],
+    "readOnly": true
+  },
+  "actionExecutor": "incrementProperty",
+  "parameters": [
+    {
+      "id": "propertyName",
+      "type": "string",
+      "multivalued": false
+    },
+    {
+      "id": "propertyTarget",
+      "type": "string",
+      "multivalued": false
+    },
+    {
+      "id": "storeInSession",
+      "type": "boolean",
+      "multivalued": false
+    }
+  ]
+}
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 48c4b46..61d0027 100644
--- a/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -298,4 +298,10 @@
         </bean>
     </service>
 
+    <service interface="org.apache.unomi.api.actions.ActionExecutor">
+        <service-properties>
+            <entry key="actionExecutorId" value="incrementProperty"/>
+        </service-properties>
+        <bean class="org.apache.unomi.plugins.baseplugin.actions.IncrementPropertyAction"/>
+    </service>
 </blueprint>
diff --git a/services/src/main/java/org/apache/unomi/services/actions/ActionExecutorDispatcher.java b/services/src/main/java/org/apache/unomi/services/actions/ActionExecutorDispatcher.java
index e0c29f5..99aefa7 100644
--- a/services/src/main/java/org/apache/unomi/services/actions/ActionExecutorDispatcher.java
+++ b/services/src/main/java/org/apache/unomi/services/actions/ActionExecutorDispatcher.java
@@ -40,6 +40,8 @@ import java.util.concurrent.ConcurrentHashMap;
 public class ActionExecutorDispatcher {
     private static final Logger logger = LoggerFactory.getLogger(ActionExecutorDispatcher.class.getName());
     private static final String VALUE_NAME_SEPARATOR = "::";
+    private static final String PLACEHOLDER_PREFIX = "${";
+    private static final String PLACEHOLDER_SUFFIX = "}";
     private final Map<String, ValueExtractor> valueExtractors = new HashMap<>(11);
     private Map<String, ActionExecutor> executors = new ConcurrentHashMap<>();
     private MetricsService metricsService;
@@ -124,13 +126,21 @@ public class ActionExecutorDispatcher {
             if (value instanceof String) {
                 String s = (String) value;
                 try {
-                    // check if we have special values
-                    if (s.contains(VALUE_NAME_SEPARATOR)) {
-                        final String valueType = StringUtils.substringBefore(s, VALUE_NAME_SEPARATOR);
-                        final String valueAsString = StringUtils.substringAfter(s, VALUE_NAME_SEPARATOR);
-                        final ValueExtractor extractor = valueExtractors.get(valueType);
-                        if (extractor != null) {
-                            value = extractor.extract(valueAsString, event);
+                    if (s.contains(PLACEHOLDER_PREFIX)) {
+                        while (s.contains(PLACEHOLDER_PREFIX)) {
+                            String substring = s.substring(s.indexOf(PLACEHOLDER_PREFIX) + 2, s.indexOf(PLACEHOLDER_SUFFIX));
+                            Object v = extractValue(substring, event);
+                            if (v != null) {
+                                s = s.replace(PLACEHOLDER_PREFIX + substring + PLACEHOLDER_SUFFIX, v.toString());
+                            } else {
+                                break;
+                            }
+                        }
+                        value = s;
+                    } else {
+                        // check if we have special values
+                        if (s.contains(VALUE_NAME_SEPARATOR)) {
+                            value = extractValue(s, event);
                         }
                     }
                 } catch (UnsupportedOperationException e) {
@@ -146,13 +156,28 @@ public class ActionExecutorDispatcher {
         return values;
     }
 
+    private Object extractValue(String s, Event event) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
+        Object value = null;
+
+        String valueType = StringUtils.substringBefore(s, VALUE_NAME_SEPARATOR);
+        String valueAsString = StringUtils.substringAfter(s, VALUE_NAME_SEPARATOR);
+        ValueExtractor extractor = valueExtractors.get(valueType);
+        if (extractor != null) {
+            value = extractor.extract(valueAsString, event);
+        }
+
+        return value;
+    }
+
     @SuppressWarnings("unchecked")
     private boolean hasContextualParameter(Map<String, Object> values) {
         for (Map.Entry<String, Object> entry : values.entrySet()) {
             Object value = entry.getValue();
             if (value instanceof String) {
                 String s = (String) value;
-                if (s.contains(VALUE_NAME_SEPARATOR) && valueExtractors.containsKey(StringUtils.substringBefore(s, VALUE_NAME_SEPARATOR))) {
+                String str = s.contains(PLACEHOLDER_PREFIX) ? s.substring(s.indexOf(PLACEHOLDER_PREFIX) + 2, s.indexOf(PLACEHOLDER_SUFFIX)) : s;
+
+                if (str.contains(VALUE_NAME_SEPARATOR) && valueExtractors.containsKey(StringUtils.substringBefore(str, VALUE_NAME_SEPARATOR))) {
                     return true;
                 }
             } else if (value instanceof Map) {
@@ -173,7 +198,7 @@ public class ActionExecutorDispatcher {
         int colonPos = actionKey.indexOf(":");
         if (colonPos > 0) {
             String actionPrefix = actionKey.substring(0, colonPos);
-            String actionName = actionKey.substring(colonPos+1);
+            String actionName = actionKey.substring(colonPos + 1);
             ActionDispatcher actionDispatcher = actionDispatchers.get(actionPrefix);
             if (actionDispatcher == null) {
                 logger.warn("Couldn't find any action dispatcher for prefix '{}', action {} won't execute !", actionPrefix, actionKey);