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

[unomi] branch unomi-1.5.x updated: UNOMI-448 : Create generic action to copy properties to profile (#268)

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

jkevan pushed a commit to branch unomi-1.5.x
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/unomi-1.5.x by this push:
     new 5e2c2f4  UNOMI-448 : Create generic action to copy properties to profile (#268)
5e2c2f4 is described below

commit 5e2c2f4c07a8066050d18ef3e3c249fd9bf37489
Author: jsinovassin <58...@users.noreply.github.com>
AuthorDate: Fri Mar 26 14:41:31 2021 +0100

    UNOMI-448 : Create generic action to copy properties to profile (#268)
    
    * UNOMI-448 : Create generic action to copy properties to profile
    
    * handle feedback
    
    * handle feedback
---
 .../test/java/org/apache/unomi/itests/AllITs.java  |   1 +
 .../test/java/org/apache/unomi/itests/BaseIT.java  |   8 +-
 .../unomi/itests/CopyPropertiesActionIT.java       | 275 +++++++++++++++++++++
 itests/src/test/resources/testCopyProperties.json  |  26 ++
 .../testCopyPropertiesWithoutSystemTags.json       |  23 ++
 itests/src/test/resources/testLogin.json           |   2 +-
 .../baseplugin/actions/CopyPropertiesAction.java   | 115 +++++++++
 .../actions/allEventToProfilePropertiesAction.json |   5 +-
 .../META-INF/cxs/actions/copyPropertiesAction.json |  30 +++
 .../resources/OSGI-INF/blueprint/blueprint.xml     |  10 +
 10 files changed, 489 insertions(+), 6 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 284b04e..b4ecd56 100644
--- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java
+++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java
@@ -41,6 +41,7 @@ import org.junit.runners.Suite.SuiteClasses;
 	ProfileMergeIT.class,
 	EventServiceIT.class,
     PropertiesUpdateActionIT.class,
+	CopyPropertiesActionIT.class,
 	ModifyConsentIT.class,
 	PatchIT.class,
 	ContextServletIT.class,
diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
index a3d1d27..42e1318 100644
--- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
@@ -59,7 +59,7 @@ import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.*;
 
 /**
  * Base class for integration tests.
- * 
+ *
  * @author kevan
  */
 @RunWith(PaxExam.class)
@@ -67,7 +67,7 @@ import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.*;
 public abstract class BaseIT {
 
     private final static Logger LOGGER = LoggerFactory.getLogger(BaseIT.class);
-    
+
     protected static final String HTTP_PORT = "8181";
     protected static final String URL = "http://localhost:" + HTTP_PORT;
     protected static final String KARAF_DIR = "target/exam";
@@ -140,6 +140,10 @@ public abstract class BaseIT {
                         "src/test/resources/6-actors-test.csv")),
                 replaceConfigurationFile("data/tmp/testLogin.json", new File(
                         "src/test/resources/testLogin.json")),
+                replaceConfigurationFile("data/tmp/testCopyProperties.json", new File(
+                        "src/test/resources/testCopyProperties.json")),
+                replaceConfigurationFile("data/tmp/testCopyPropertiesWithoutSystemTags.json", new File(
+                        "src/test/resources/testCopyPropertiesWithoutSystemTags.json")),
                 replaceConfigurationFile("data/tmp/testLoginEventCondition.json", new File(
                         "src/test/resources/testLoginEventCondition.json")),
                 keepRuntimeFolder(),
diff --git a/itests/src/test/java/org/apache/unomi/itests/CopyPropertiesActionIT.java b/itests/src/test/java/org/apache/unomi/itests/CopyPropertiesActionIT.java
new file mode 100644
index 0000000..3be376d
--- /dev/null
+++ b/itests/src/test/java/org/apache/unomi/itests/CopyPropertiesActionIT.java
@@ -0,0 +1,275 @@
+/*
+ * 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 org.apache.unomi.api.Event;
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.PropertyType;
+import org.apache.unomi.api.rules.Rule;
+import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.api.services.RulesService;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
+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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Created by amidani on 12/10/2017.
+ */
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerSuite.class)
+public class CopyPropertiesActionIT extends BaseIT {
+    private final static Logger LOGGER = LoggerFactory.getLogger(CopyPropertiesActionIT.class);
+
+    private final static String EMPTY_PROFILE = "empty-profile";
+    private final static String PROFILE_WITH_PROPERTIES = "profile-with-properties";
+    private final static String ARRAY_PARAM_NAME = "arrayParam";
+    public static final String SINGLE_PARAM_NAME = "singleParam";
+
+    @Inject
+    @Filter(timeout = 600000)
+    protected RulesService rulesService;
+    @Inject
+    @Filter(timeout = 600000)
+    protected ProfileService profileService;
+    @Inject
+    @Filter(timeout = 600000)
+    protected EventService eventService;
+
+    @Before
+    public void setUp() throws IOException, InterruptedException {
+        Profile profile = new Profile();
+        profile.setItemId(PROFILE_WITH_PROPERTIES);
+        profile.setProperties(new HashMap<>());
+        profile.setProperty("lastName", "Jose"); // property that have a propertyType registered in the system
+        profile.setProperty("singleValue", "A single value");
+        profile.setProperty("existingArray", Arrays.asList("element1", "element2"));
+        profileService.save(profile);
+        LOGGER.info("Profile saved with ID [{}].", profile.getItemId());
+
+        Profile profileTarget = new Profile();
+        profileTarget.setItemId(EMPTY_PROFILE);
+        profileService.save(profileTarget);
+        LOGGER.info("Profile saved with ID [{}].", profileTarget.getItemId());
+
+        refreshPersistence();
+    }
+
+    @After
+    public void cleanUp() throws IOException, InterruptedException {
+        profileService.delete(PROFILE_WITH_PROPERTIES, false);
+        profileService.delete(EMPTY_PROFILE, false);
+        profileService.deletePropertyType(ARRAY_PARAM_NAME);
+        profileService.deletePropertyType(SINGLE_PARAM_NAME);
+        refreshPersistence();
+    }
+
+    private void initializePropertyType() {
+        Metadata metadata = new Metadata();
+        metadata.setSystemTags(new HashSet<>(Arrays.asList("urlParameters")));
+        metadata.setId(ARRAY_PARAM_NAME);
+        metadata.setName("Array parameter");
+
+        PropertyType propertyType1 = new PropertyType();
+        propertyType1.setItemId(ARRAY_PARAM_NAME);
+        propertyType1.setMetadata(metadata);
+        propertyType1.setTarget("profiles");
+        propertyType1.setValueTypeId("string");
+        propertyType1.setMultivalued(true);
+
+        Metadata metadata2 = new Metadata();
+        metadata2.setSystemTags(new HashSet<>(Arrays.asList("urlParameters")));
+        metadata2.setId(SINGLE_PARAM_NAME);
+        metadata2.setName("Single parameters");
+
+        PropertyType propertyType2 = new PropertyType();
+        propertyType2.setItemId(SINGLE_PARAM_NAME);
+        propertyType2.setMetadata(metadata2);
+        propertyType2.setTarget("profiles");
+        propertyType2.setValueTypeId("string");
+        propertyType2.setMultivalued(false);
+
+        profileService.setPropertyType(propertyType1);
+        profileService.setPropertyType(propertyType2);
+    }
+
+    private void initializePropertyTypeWithDifferentSystemTag() {
+        Metadata metadata = new Metadata();
+        metadata.setSystemTags(new HashSet<>(Arrays.asList("shouldBeAbsent")));
+        metadata.setId(ARRAY_PARAM_NAME);
+        metadata.setName("Array parameter");
+
+        PropertyType propertyType1 = new PropertyType();
+        propertyType1.setItemId(ARRAY_PARAM_NAME);
+        propertyType1.setMetadata(metadata);
+        propertyType1.setTarget("profiles");
+        propertyType1.setValueTypeId("string");
+        propertyType1.setMultivalued(true);
+
+        profileService.setPropertyType(propertyType1);
+    }
+
+    private void createRule(String filename) throws IOException, InterruptedException {
+        Rule rule = CustomObjectMapper.getObjectMapper().readValue(new File(filename).toURI().toURL(), Rule.class);
+        rulesService.setRule(rule);
+        Thread.sleep(2000);
+    }
+
+    private Event sendCopyPropertyEvent(Map<String, Object> properties, String profileType) {
+        Profile profile = profileService.load(profileType);
+
+        Event event = new Event("copyProperties", null, profile, null, null, profile, new Date());
+        event.setPersistent(false);
+
+        event.setProperty("urlParameters", properties);
+
+        eventService.send(event);
+        return event;
+    }
+
+    @Test
+    public void testCopyProperties_copyMultipleValueWithoutExistingPropertyTypeAndWithoutExistingValue()
+            throws IOException, InterruptedException {
+        createRule("data/tmp/testCopyPropertiesWithoutSystemTags.json");
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ARRAY_PARAM_NAME, Arrays.asList("valueA", "valueB"));
+
+        Event event = sendCopyPropertyEvent(properties, EMPTY_PROFILE);
+
+        Assert.assertTrue(((List<String>) event.getProfile().getProperty(ARRAY_PARAM_NAME)).contains("valueA"));
+        Assert.assertTrue(((List<String>) event.getProfile().getProperty(ARRAY_PARAM_NAME)).contains("valueB"));
+    }
+
+    @Test
+    public void testCopyProperties_tryCopyArrayOnExistingSingleValue() throws IOException, InterruptedException {
+        createRule("data/tmp/testCopyPropertiesWithoutSystemTags.json");
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put("singleValue", Arrays.asList("valueA", "valueB"));
+
+        Event event = sendCopyPropertyEvent(properties, PROFILE_WITH_PROPERTIES);
+
+        Assert.assertTrue(((String) event.getProfile().getProperty("singleValue")).equals("A single value"));
+    }
+
+    @Test
+    public void testCopyProperties_replaceSingleValue() throws IOException, InterruptedException {
+        createRule("data/tmp/testCopyPropertiesWithoutSystemTags.json");
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put("singleValue", "New value");
+
+        Event event = sendCopyPropertyEvent(properties, PROFILE_WITH_PROPERTIES);
+
+        Assert.assertTrue(((String) event.getProfile().getProperty("singleValue")).equals("New value"));
+    }
+
+    @Test
+    public void testCopyProperties_copyArrayIntoExistingArray() throws IOException, InterruptedException {
+        createRule("data/tmp/testCopyPropertiesWithoutSystemTags.json");
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put("existingArray", Arrays.asList("valueA", "valueB"));
+
+        Event event = sendCopyPropertyEvent(properties, PROFILE_WITH_PROPERTIES);
+
+        Assert.assertTrue(((List<String>) event.getProfile().getProperty("existingArray")).contains("element1"));
+        Assert.assertTrue(((List<String>) event.getProfile().getProperty("existingArray")).contains("element2"));
+        Assert.assertTrue(((List<String>) event.getProfile().getProperty("existingArray")).contains("valueA"));
+        Assert.assertTrue(((List<String>) event.getProfile().getProperty("existingArray")).contains("valueB"));
+    }
+
+    @Test
+    public void testCopyProperties_copyArrayWithPropertyType() throws IOException, InterruptedException {
+        createRule("data/tmp/testCopyPropertiesWithoutSystemTags.json");
+
+        initializePropertyType();
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ARRAY_PARAM_NAME, Arrays.asList("valueA", "valueB"));
+
+        Event event = sendCopyPropertyEvent(properties, EMPTY_PROFILE);
+
+        Assert.assertTrue(((List<String>) event.getProfile().getProperty(ARRAY_PARAM_NAME)).contains("valueA"));
+        Assert.assertTrue(((List<String>) event.getProfile().getProperty(ARRAY_PARAM_NAME)).contains("valueB"));
+    }
+
+    @Test
+    public void testCopyProperties_tryCopyArrayWithPropertyTypeIntoSingleValue() throws IOException, InterruptedException {
+        createRule("data/tmp/testCopyProperties.json");
+
+        initializePropertyType();
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(SINGLE_PARAM_NAME, Arrays.asList("valueA", "valueB"));
+
+        Event event = sendCopyPropertyEvent(properties, EMPTY_PROFILE);
+
+        Assert.assertNull(event.getProfile().getProperty(SINGLE_PARAM_NAME));
+    }
+
+    @Test
+    public void testCopyProperties_replaceSingleValueWithPropertyType() throws IOException, InterruptedException {
+        createRule("data/tmp/testCopyProperties.json");
+
+        initializePropertyType();
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(SINGLE_PARAM_NAME, "New value");
+
+        Event event = sendCopyPropertyEvent(properties, EMPTY_PROFILE);
+
+        Assert.assertTrue(((String) event.getProfile().getProperty(SINGLE_PARAM_NAME)).equals("New value"));
+    }
+
+    @Test
+    public void testCopyProperties_mandatorySystemTagsNotPresent() throws IOException, InterruptedException {
+        createRule("data/tmp/testCopyProperties.json");
+
+        initializePropertyTypeWithDifferentSystemTag();
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ARRAY_PARAM_NAME, Arrays.asList("New value"));
+
+        Event event = sendCopyPropertyEvent(properties, EMPTY_PROFILE);
+
+        Assert.assertTrue(event.getProfile().getProperty(ARRAY_PARAM_NAME) == null);
+    }
+}
diff --git a/itests/src/test/resources/testCopyProperties.json b/itests/src/test/resources/testCopyProperties.json
new file mode 100644
index 0000000..44354e9
--- /dev/null
+++ b/itests/src/test/resources/testCopyProperties.json
@@ -0,0 +1,26 @@
+{
+  "metadata": {
+    "id": "copyProperties",
+    "name": "Copy properties to profile",
+    "description": "Copy properties to profile",
+    "readOnly": true
+  },
+  "condition": {
+    "type": "eventTypeCondition",
+    "parameterValues": {
+      "eventTypeId": "copyProperties"
+    }
+  },
+  "actions": [
+    {
+      "type": "copyPropertiesAction",
+      "parameterValues": {
+        "rootProperty": "properties.urlParameters",
+        "mandatoryPropTypeSystemTag": [
+          "urlParameters"
+        ],
+        "singleValueStrategy": "alwaysSet"
+      }
+    }
+  ]
+}
diff --git a/itests/src/test/resources/testCopyPropertiesWithoutSystemTags.json b/itests/src/test/resources/testCopyPropertiesWithoutSystemTags.json
new file mode 100644
index 0000000..548dd20
--- /dev/null
+++ b/itests/src/test/resources/testCopyPropertiesWithoutSystemTags.json
@@ -0,0 +1,23 @@
+{
+  "metadata": {
+    "id": "copyProperties",
+    "name": "Copy properties to profile",
+    "description": "Copy properties to profile",
+    "readOnly": true
+  },
+  "condition": {
+    "type": "eventTypeCondition",
+    "parameterValues": {
+      "eventTypeId": "copyProperties"
+    }
+  },
+  "actions": [
+    {
+      "type": "copyPropertiesAction",
+      "parameterValues": {
+        "rootProperty": "properties.urlParameters",
+        "singleValueStrategy": "alwaysSet"
+      }
+    }
+  ]
+}
diff --git a/itests/src/test/resources/testLogin.json b/itests/src/test/resources/testLogin.json
index e5a3511..fce5a3d 100644
--- a/itests/src/test/resources/testLogin.json
+++ b/itests/src/test/resources/testLogin.json
@@ -31,4 +31,4 @@
       "type": "allEventToProfilePropertiesAction"
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/CopyPropertiesAction.java b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/CopyPropertiesAction.java
new file mode 100644
index 0000000..7d0b9cf
--- /dev/null
+++ b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/CopyPropertiesAction.java
@@ -0,0 +1,115 @@
+/*
+ * 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.BeanUtilsBean;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.Event;
+import org.apache.unomi.api.PropertyType;
+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.api.services.ProfileService;
+import org.apache.unomi.persistence.spi.PropertyHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class CopyPropertiesAction implements ActionExecutor {
+
+    private static final Logger logger = LoggerFactory.getLogger(CopyPropertiesAction.class);
+    private ProfileService profileService;
+
+    public void setProfileService(ProfileService profileService) {
+        this.profileService = profileService;
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public int execute(Action action, Event event) {
+        boolean atLeastOnechanged = false;
+        List<String> mandatoryPropTypeSystemTags = (List<String>) action.getParameterValues().get("mandatoryPropTypeSystemTag");
+        String singleValueStrategy = (String) action.getParameterValues().get("singleValueStrategy");
+        for (Map.Entry<String, Object> entry : getEventPropsToCopy(action, event).entrySet()) {
+            // propType Check
+            PropertyType propertyType = profileService.getPropertyType(entry.getKey());
+            Object previousValue = event.getProfile().getProperty(entry.getKey());
+            if (mandatoryPropTypeSystemTags != null && mandatoryPropTypeSystemTags.size() > 0) {
+                if (propertyType == null || propertyType.getMetadata() == null || propertyType.getMetadata().getSystemTags() == null
+                        || !propertyType.getMetadata().getSystemTags().containsAll(mandatoryPropTypeSystemTags)) {
+                    continue;
+                }
+            }
+            String propertyName = "properties." + entry.getKey();
+            boolean changed = false;
+            if (previousValue == null && propertyType == null) {
+                changed = PropertyHelper.setProperty(event.getProfile(), propertyName, entry.getValue(), "alwaysSet");
+            } else {
+                boolean propertyTypeIsMultiValued =
+                        propertyType != null && propertyType.isMultivalued() != null && propertyType.isMultivalued();
+                boolean multipleIsExpected = previousValue != null ? previousValue instanceof List : propertyTypeIsMultiValued;
+
+                if (multipleIsExpected) {
+                    changed = PropertyHelper.setProperty(event.getProfile(), propertyName, entry.getValue(), "addValues");
+                } else if (entry.getValue() instanceof List) {
+                    logger.error(
+                            "Impossible to copy the property of type List to the profile, either a single value already exist on the profile or the property type is declared as a single value property. Enable debug log level for more information");
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("cannot copy property {}, because it's a List", entry.getKey());
+                    }
+                } else {
+                    changed = PropertyHelper.setProperty(event.getProfile(), propertyName, entry.getValue(), singleValueStrategy);
+                }
+            }
+            atLeastOnechanged = atLeastOnechanged || changed;
+        }
+        return atLeastOnechanged ? EventService.PROFILE_UPDATED : EventService.NO_CHANGE;
+    }
+
+    private Map<String, Object> getEventPropsToCopy(Action action, Event event) {
+        Map<String, Object> propsToCopy = new HashMap<String, Object>();
+
+        String rootProperty = (String) action.getParameterValues().get("rootProperty");
+        boolean copyEventProps = false;
+
+        if (StringUtils.isEmpty(rootProperty)) {
+            copyEventProps = true;
+            rootProperty = "target.properties";
+        }
+
+        // copy props from the event.properties
+        if (copyEventProps && event.getProperties() != null) {
+            propsToCopy.putAll(event.getProperties());
+        }
+
+        // copy props from the specified level (default is: target.properties)
+        try {
+            Object targetProperties = BeanUtilsBean.getInstance().getPropertyUtils().getProperty(event, rootProperty);
+            if (targetProperties instanceof Map) {
+                propsToCopy.putAll((Map) targetProperties);
+            }
+        } catch (Exception e) {
+            logger.error("Unable to extract properties to be copied from the event to the profile using root property: {}", rootProperty,
+                    e);
+        }
+
+        return propsToCopy;
+    }
+}
diff --git a/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/allEventToProfilePropertiesAction.json b/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/allEventToProfilePropertiesAction.json
index 35d4ade..abf1d29 100644
--- a/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/allEventToProfilePropertiesAction.json
+++ b/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/allEventToProfilePropertiesAction.json
@@ -10,6 +10,5 @@
     "readOnly": true
   },
   "actionExecutor": "allEventToProfileProperties",
-  "parameters": [
-  ]
-}
\ No newline at end of file
+  "parameters": []
+}
diff --git a/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/copyPropertiesAction.json b/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/copyPropertiesAction.json
new file mode 100644
index 0000000..7c1a834
--- /dev/null
+++ b/plugins/baseplugin/src/main/resources/META-INF/cxs/actions/copyPropertiesAction.json
@@ -0,0 +1,30 @@
+{
+  "metadata": {
+    "id": "copyPropertiesAction",
+    "name": "copyPropertiesAction",
+    "description": "",
+    "systemTags": [
+      "profileTags",
+      "event"
+    ],
+    "readOnly": true
+  },
+  "actionExecutor": "copyProperties",
+  "parameters": [
+    {
+      "id": "setPropertyStrategy",
+      "type": "string",
+      "multivalued": false
+    },
+    {
+      "id": "rootProperty",
+      "type": "string",
+      "multivalued": false
+    },
+    {
+      "id": "mandatoryPropTypeSystemTag",
+      "type": "string",
+      "multivalued": true
+    }
+  ]
+}
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 9f7169a..22f01d7 100644
--- a/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -171,6 +171,7 @@
         </bean>
     </service>
 
+
     <!-- Action executors -->
 
     <service interface="org.apache.unomi.api.actions.ActionExecutor">
@@ -184,6 +185,15 @@
 
     <service interface="org.apache.unomi.api.actions.ActionExecutor">
         <service-properties>
+            <entry key="actionExecutorId" value="copyProperties"/>
+        </service-properties>
+        <bean class="org.apache.unomi.plugins.baseplugin.actions.CopyPropertiesAction">
+            <property name="profileService" ref="profileService"/>
+        </bean>
+    </service>
+
+    <service interface="org.apache.unomi.api.actions.ActionExecutor">
+        <service-properties>
             <entry key="actionExecutorId" value="eventToProfileProperty"/>
         </service-properties>
         <bean class="org.apache.unomi.plugins.baseplugin.actions.EventToProfilePropertyAction"/>