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

[unomi] 01/01: UNOMI-445 Implement support for multiple profile IDs on Unomi profiles

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

asi pushed a commit to branch UNOMI-445
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit 20dd675b293e906c8c8d7d533d4ae47103c94c78
Author: Anatol Sialitski <as...@enonic.com>
AuthorDate: Wed Jun 2 17:10:10 2021 +0300

    UNOMI-445 Implement support for multiple profile IDs on Unomi profiles
---
 .../java/org/apache/unomi/api/ContextRequest.java  | 10 ++++
 api/src/main/java/org/apache/unomi/api/Event.java  |  5 ++
 .../java/org/apache/unomi/api/ProfileAlias.java    | 68 ++++++++++++++++++++++
 .../apache/unomi/api/services/ProfileService.java  |  2 +
 .../fetchers/profile/ProfileDataFetcher.java       |  3 +
 .../org/apache/unomi/itests/ProfileMergeIT.java    | 61 +++++++++++++++++++
 .../org/apache/unomi/itests/ProfileServiceIT.java  | 55 ++++++++++++++---
 .../unomi/persistence/spi/CustomObjectMapper.java  |  1 +
 .../actions/MergeProfilesOnPropertyAction.java     | 40 ++++++-------
 .../unomi/rest/endpoints/ContextJsonEndpoint.java  | 17 +++++-
 .../services/impl/profiles/ProfileServiceImpl.java | 28 +++++++++
 11 files changed, 256 insertions(+), 34 deletions(-)

diff --git a/api/src/main/java/org/apache/unomi/api/ContextRequest.java b/api/src/main/java/org/apache/unomi/api/ContextRequest.java
index 7a67c06..78c4720 100644
--- a/api/src/main/java/org/apache/unomi/api/ContextRequest.java
+++ b/api/src/main/java/org/apache/unomi/api/ContextRequest.java
@@ -73,6 +73,8 @@ public class ContextRequest {
     @Pattern(regexp = ValidationPattern.TEXT_VALID_CHARACTERS_PATTERN)
     private String profileId;
 
+    private String clientId;
+
     /**
      * Retrieves the source of the context request.
      *
@@ -271,4 +273,12 @@ public class ContextRequest {
     public void setProfileId(String profileId) {
         this.profileId = profileId;
     }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(String clientId) {
+        this.clientId = clientId;
+    }
 }
diff --git a/api/src/main/java/org/apache/unomi/api/Event.java b/api/src/main/java/org/apache/unomi/api/Event.java
index ab7e1b0..92e8378 100644
--- a/api/src/main/java/org/apache/unomi/api/Event.java
+++ b/api/src/main/java/org/apache/unomi/api/Event.java
@@ -46,6 +46,11 @@ public class Event extends Item implements TimestampedItem {
      * A constant for the name of the attribute that can be used to retrieve the current HTTP response.
      */
     public static final String HTTP_RESPONSE_ATTRIBUTE = "http_response";
+    /**
+     * A constant for the name of the attribute that can be used to retrieve the current clientID.
+     */
+    public static final String CLIENT_ID_ATTRIBUTE = "client_id";
+
     private static final long serialVersionUID = -1096874942838593575L;
     private String eventType;
     private String sessionId = null;
diff --git a/api/src/main/java/org/apache/unomi/api/ProfileAlias.java b/api/src/main/java/org/apache/unomi/api/ProfileAlias.java
new file mode 100644
index 0000000..1434b02
--- /dev/null
+++ b/api/src/main/java/org/apache/unomi/api/ProfileAlias.java
@@ -0,0 +1,68 @@
+/*
+ * 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.api;
+
+import java.util.Date;
+
+public class ProfileAlias extends Item {
+
+    public static final String ITEM_TYPE = "profileAlias";
+
+    private String profileID;
+
+    private String clientID;
+
+    private Date creationTime;
+
+    private Date modifiedTime;
+
+    public ProfileAlias() {
+    }
+
+    public String getProfileID() {
+        return profileID;
+    }
+
+    public void setProfileID(String profileID) {
+        this.profileID = profileID;
+    }
+
+    public String getClientID() {
+        return clientID;
+    }
+
+    public void setClientID(String clientID) {
+        this.clientID = clientID;
+    }
+
+    public Date getCreationTime() {
+        return creationTime;
+    }
+
+    public void setCreationTime(Date creationTime) {
+        this.creationTime = creationTime;
+    }
+
+    public Date getModifiedTime() {
+        return modifiedTime;
+    }
+
+    public void setModifiedTime(Date modifiedTime) {
+        this.modifiedTime = modifiedTime;
+    }
+}
diff --git a/api/src/main/java/org/apache/unomi/api/services/ProfileService.java b/api/src/main/java/org/apache/unomi/api/services/ProfileService.java
index 3b94ed9..b2698c4 100644
--- a/api/src/main/java/org/apache/unomi/api/services/ProfileService.java
+++ b/api/src/main/java/org/apache/unomi/api/services/ProfileService.java
@@ -108,6 +108,8 @@ public interface ProfileService {
      */
     Profile save(Profile profile);
 
+    void addAliasToProfile(String profileID, String alias, String clientID);
+
     /**
      * Merge the specified profile properties in an existing profile,or save new profile if it does not exist yet
      *
diff --git a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/fetchers/profile/ProfileDataFetcher.java b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/fetchers/profile/ProfileDataFetcher.java
index 0a566af..0000065 100644
--- a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/fetchers/profile/ProfileDataFetcher.java
+++ b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/fetchers/profile/ProfileDataFetcher.java
@@ -52,6 +52,9 @@ public class ProfileDataFetcher extends BaseDataFetcher<CDPProfile> {
             profile.setItemType("profile");
 
             profile = profileService.save(profile);
+
+            profileService.addAliasToProfile(profile.getItemId(), profile.getItemId(), profileIDInput.getClient().getId());
+
             return new CDPProfile(profile);
         }
 
diff --git a/itests/src/test/java/org/apache/unomi/itests/ProfileMergeIT.java b/itests/src/test/java/org/apache/unomi/itests/ProfileMergeIT.java
index aa19ae4..c7602d6 100644
--- a/itests/src/test/java/org/apache/unomi/itests/ProfileMergeIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/ProfileMergeIT.java
@@ -19,12 +19,15 @@ 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.ProfileAlias;
 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.apache.unomi.persistence.spi.PersistenceService;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Test;
@@ -38,6 +41,7 @@ import javax.inject.Inject;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.List;
 
 /**
  * Integration test for MergeProfilesOnPropertyAction
@@ -52,6 +56,10 @@ public class ProfileMergeIT extends BaseIT {
     protected RulesService rulesService;
     @Inject @Filter(timeout = 600000)
     protected DefinitionsService definitionsService;
+    @Inject @Filter(timeout = 600000)
+    protected ProfileService profileService;
+    @Inject @Filter(timeout = 600000)
+    protected PersistenceService persistenceService;
 
     private final static String TEST_EVENT_TYPE = "mergeProfileTestEventType";
     private final static String TEST_RULE_ID = "mergeOnPropertyTest";
@@ -81,6 +89,59 @@ public class ProfileMergeIT extends BaseIT {
         Assert.assertEquals(sendEvent().getProfile().getItemId(), TEST_PROFILE_ID);
     }
 
+    @Test
+    public void test() throws InterruptedException {
+        // create rule
+        Condition condition = new Condition(definitionsService.getConditionType("eventTypeCondition"));
+        condition.setParameter("eventTypeId", TEST_EVENT_TYPE);
+
+        final Action action = new Action( definitionsService.getActionType( "mergeProfilesOnPropertyAction"));
+        action.setParameter("mergeProfilePropertyValue", "eventProperty::target.properties(email)");
+        action.setParameter("mergeProfilePropertyName", "mergeIdentifier");
+        action.setParameter("forceEventProfileAsMaster", false);
+
+        Rule rule = new Rule();
+        rule.setMetadata(new Metadata(null, TEST_RULE_ID, TEST_RULE_ID, "Description"));
+        rule.setCondition(condition);
+        rule.setActions(Collections.singletonList(action));
+
+        rulesService.setRule(rule);
+        refreshPersistence();
+
+        // create master profile
+        Profile masterProfile = new Profile();
+        masterProfile.setItemId("masterProfileID");
+        masterProfile.setProperty("email", "username@domain.com");
+        masterProfile.setSystemProperty("mergeIdentifier", "username@domain.com");
+        profileService.save(masterProfile);
+
+        // create event profile
+        Profile eventProfile = new Profile();
+        eventProfile.setItemId("eventProfileID");
+        eventProfile.setProperty("email", "username@domain.com");
+        profileService.save(eventProfile);
+
+        refreshPersistence();
+
+        Event event = new Event(TEST_EVENT_TYPE, null, eventProfile, null, null, eventProfile, new Date());
+        eventService.send(event);
+
+        refreshPersistence();
+
+        Assert.assertNotNull(event.getProfile());
+
+        List<ProfileAlias> profileAliases = persistenceService.getAllItems(ProfileAlias.class);
+
+        Assert.assertFalse(profileAliases.isEmpty());
+
+        List<ProfileAlias> aliases = persistenceService.query("profileID", masterProfile.getItemId(), null, ProfileAlias.class);
+
+        Assert.assertFalse(aliases.isEmpty());
+        Assert.assertEquals(masterProfile.getItemId(), aliases.get(0).getProfileID());
+        Assert.assertEquals(eventProfile.getItemId(), aliases.get(0).getItemId());
+        Assert.assertEquals("defaultClientID", aliases.get(0).getClientID());
+    }
+
     private Event sendEvent() {
         Profile profile = new Profile();
         profile.setProperties(new HashMap<>());
diff --git a/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java b/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java
index 25788bd..437097c 100644
--- a/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java
@@ -16,20 +16,16 @@
  */
 package org.apache.unomi.itests;
 
+import org.apache.unomi.api.PartialList;
 import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.ProfileAlias;
 import org.apache.unomi.api.query.Query;
+import org.apache.unomi.api.services.DefinitionsService;
 import org.apache.unomi.api.services.ProfileService;
 import org.apache.unomi.persistence.spi.PersistenceService;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.persistence.elasticsearch.*;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.Before;
-
 import org.ops4j.pax.exam.junit.PaxExam;
 import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
 import org.ops4j.pax.exam.spi.reactors.PerSuite;
@@ -37,10 +33,14 @@ import org.ops4j.pax.exam.util.Filter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.inject.Inject;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.stream.IntStream;
 
-import javax.inject.Inject;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
 
 /**
  * An integration test for the profile service
@@ -152,5 +152,42 @@ public class ProfileServiceIT extends BaseIT {
         assertEquals(testValue, value);
     }
 
+    @Test
+    public void testLoadProfileByAlias() throws Exception {
+        String profileID = "profileID_testLoadProfileByAlias";
+        try {
+            Profile profile = new Profile();
+            profile.setItemId(profileID);
+            profileService.save(profile);
+
+            refreshPersistence();
+
+            IntStream.range(1, 3).forEach(index -> {
+                final String profileAlias = profileID + "_alias_" + index;
+                profileService.addAliasToProfile(profileID, profileAlias, "clientID");
+            });
+
+            refreshPersistence();
+
+            Profile storedProfile = profileService.load(profileID);
+            assertNotNull(storedProfile);
+            assertEquals(profileID, storedProfile.getItemId());
+
+            storedProfile = profileService.load(profileID + "_alias_1");
+            assertNotNull(storedProfile);
+            assertEquals(profileID, storedProfile.getItemId());
+
+            storedProfile = profileService.load(profileID + "_alias_2");
+            assertNotNull(storedProfile);
+            assertEquals(profileID, storedProfile.getItemId());
+        } finally {
+            profileService.delete(profileID, false);
+
+            IntStream.range(1, 3).forEach(index -> {
+                final String profileAlias = profileID + "_alias_" + index;
+                persistenceService.remove(profileAlias, ProfileAlias.class);
+            });
+        }
+    }
 
 }
diff --git a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/CustomObjectMapper.java b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/CustomObjectMapper.java
index 786bbdc..21dbcbd 100644
--- a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/CustomObjectMapper.java
+++ b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/CustomObjectMapper.java
@@ -79,6 +79,7 @@ public class CustomObjectMapper extends ObjectMapper {
         classes.put(ActionType.ITEM_TYPE, ActionType.class);
         classes.put(Topic.ITEM_TYPE, Topic.class);
         classes.put(SourceItem.ITEM_TYPE, SourceItem.class);
+        classes.put(ProfileAlias.ITEM_TYPE, ProfileAlias.class);
         for (Map.Entry<String, Class<? extends Item>> entry : classes.entrySet()) {
             propertyTypedObjectDeserializer.registerMapping("itemType=" + entry.getKey(), entry.getValue());
             itemDeserializer.registerMapping(entry.getKey(), entry.getValue());
diff --git a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/MergeProfilesOnPropertyAction.java b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/MergeProfilesOnPropertyAction.java
index 24294ff..f1fde71 100644
--- a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/MergeProfilesOnPropertyAction.java
+++ b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/MergeProfilesOnPropertyAction.java
@@ -32,9 +32,11 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.servlet.ServletResponse;
-import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletResponse;
-import java.util.*;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
 
 public class MergeProfilesOnPropertyAction implements ActionExecutor {
     private static final Logger logger = LoggerFactory.getLogger(MergeProfilesOnPropertyAction.class.getName());
@@ -98,10 +100,10 @@ public class MergeProfilesOnPropertyAction implements ActionExecutor {
                 // Take existing profile
                 profile = profiles.get(0);
             } else {
-                // Create a new profile
-                if (forceEventProfileAsMaster)
+                if (forceEventProfileAsMaster) {
                     profile = event.getProfile();
-                else {
+                } else {
+                    // Create a new profile
                     profile = new Profile(UUID.randomUUID().toString());
                     profile.setProperty("firstVisit", event.getTimeStamp());
                 }
@@ -163,7 +165,7 @@ public class MergeProfilesOnPropertyAction implements ActionExecutor {
 
                 final Boolean anonymousBrowsing = privacyService.isRequireAnonymousBrowsing(masterProfileId);
 
-                if (currentSession != null){
+                if (currentSession != null) {
                     currentSession.setProfile(masterProfile);
                     if (privacyService.isRequireAnonymousBrowsing(profile)) {
                         privacyService.setRequireAnonymousBrowsing(masterProfileId, true, event.getSourceId());
@@ -185,13 +187,14 @@ public class MergeProfilesOnPropertyAction implements ActionExecutor {
                             if (!StringUtils.equals(profileId, masterProfileId)) {
                                 if (currentEvent.isPersistent()) {
                                     persistenceService.update(currentEvent, currentEvent.getTimeStamp(), Event.class, "profileId", anonymousBrowsing ? null : masterProfileId);
-                                }                            }
+                                }
+                            }
 
                             for (Profile profile : profiles) {
                                 String profileId = profile.getItemId();
                                 if (!StringUtils.equals(profileId, masterProfileId)) {
                                     List<Session> sessions = persistenceService.query("profileId", profileId, null, Session.class);
-                                    if (currentSession != null){
+                                    if (currentSession != null) {
                                         if (masterProfileId.equals(profileId) && !sessions.contains(currentSession)) {
                                             sessions.add(currentSession);
                                         }
@@ -207,21 +210,14 @@ public class MergeProfilesOnPropertyAction implements ActionExecutor {
                                             persistenceService.update(event, event.getTimeStamp(), Event.class, "profileId", anonymousBrowsing ? null : masterProfileId);
                                         }
                                     }
-                                    // we must mark all the profiles that we merged into the master as merged with the master, and they will
-                                    // be deleted upon next load
-                                    profile.setMergedWith(masterProfileId);
-                                    Map<String,Object> sourceMap = new HashMap<>();
-                                    sourceMap.put("mergedWith", masterProfileId);
-                                    profile.setSystemProperty("lastUpdated", new Date());
-                                    sourceMap.put("systemProperties", profile.getSystemProperties());
 
-                                    boolean isExist  = persistenceService.load(profile.getItemId(), Profile.class) != null;
-
-                                    if (isExist == false) //save the original event profile is it has been changed
-                                        persistenceService.save(profile);
-                                    else
-                                      persistenceService.update(profile, null, Profile.class, sourceMap,true);
+                                    String clientId = Objects.requireNonNullElse((String) event.getAttributes().get(Event.CLIENT_ID_ATTRIBUTE), "defaultClientID");
+                                    profileService.addAliasToProfile(masterProfileId, profile.getItemId(), clientId);
 
+                                    boolean isExist = profileService.load(profile.getItemId()) != null;
+                                    if (isExist) {
+                                        profileService.delete(profileId, false);
+                                    }
                                 }
                             }
                         } catch (Exception e) {
@@ -246,7 +242,7 @@ public class MergeProfilesOnPropertyAction implements ActionExecutor {
                         profileIdCookieName + "=" + profile.getItemId() +
                                 "; Path=/" +
                                 "; Max-Age=" + cookieAgeInSeconds +
-                                (StringUtils.isNotBlank(profileIdCookieDomain) ? ("; Domain=" + profileIdCookieDomain) : "")  +
+                                (StringUtils.isNotBlank(profileIdCookieDomain) ? ("; Domain=" + profileIdCookieDomain) : "") +
                                 "; SameSite=Lax");
             }
         }
diff --git a/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java b/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java
index e2df705..3224484 100644
--- a/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java
+++ b/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java
@@ -64,6 +64,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.Objects;
 
 @WebService
 @Consumes(MediaType.APPLICATION_JSON)
@@ -73,6 +74,8 @@ import java.util.UUID;
 public class ContextJsonEndpoint {
     private static final Logger logger = LoggerFactory.getLogger(ContextJsonEndpoint.class.getName());
 
+    private static final String DEFAULT_CLIENT_ID = "defaultClientId";
+
     private boolean sanitizeConditions = Boolean
             .parseBoolean(System.getProperty("org.apache.unomi.security.personalization.sanitizeConditions", "true"));
 
@@ -204,10 +207,11 @@ public class ContextJsonEndpoint {
         }
 
         int changes = EventService.NO_CHANGE;
-        if (profile == null) {
-            // Not a persona, resolve profile now
-            boolean profileCreated = false;
 
+        // Not a persona, resolve profile now
+        boolean profileCreated = false;
+
+        if (profile == null) {
             if (profileId == null || invalidateProfile) {
                 // no profileId cookie was found or the profile has to be invalidated, we generate a new one and create the profile in the profile service
                 profile = createNewProfile(null, timestamp);
@@ -295,6 +299,7 @@ public class ContextJsonEndpoint {
                 profileUpdated.setPersistent(false);
                 profileUpdated.getAttributes().put(Event.HTTP_REQUEST_ATTRIBUTE, request);
                 profileUpdated.getAttributes().put(Event.HTTP_RESPONSE_ATTRIBUTE, response);
+                profileUpdated.getAttributes().put(Event.CLIENT_ID_ATTRIBUTE, DEFAULT_CLIENT_ID);
 
                 if (logger.isDebugEnabled()) {
                     logger.debug("Received event {} for profile={} {} target={} timestamp={}", profileUpdated.getEventType(),
@@ -322,6 +327,12 @@ public class ContextJsonEndpoint {
         if ((changes & EventService.PROFILE_UPDATED) == EventService.PROFILE_UPDATED) {
             profileService.save(profile);
             contextResponse.setProfileId(profile.getItemId());
+
+            if (profileCreated) {
+                String clientId = Objects.requireNonNullElse(contextRequest.getClientId(), DEFAULT_CLIENT_ID);
+                String profileMasterId = Objects.requireNonNullElse(profile.getMergedWith(), profile.getItemId());
+                profileService.addAliasToProfile(profileMasterId, profile.getItemId(), clientId );
+            }
         }
         if ((changes & EventService.SESSION_UPDATED) == EventService.SESSION_UPDATED && session != null) {
             profileService.saveSession(session);
diff --git a/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
index 38356cf..6f984c6 100644
--- a/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
+++ b/services/src/main/java/org/apache/unomi/services/impl/profiles/ProfileServiceImpl.java
@@ -27,6 +27,7 @@ import org.apache.unomi.api.Persona;
 import org.apache.unomi.api.PersonaSession;
 import org.apache.unomi.api.PersonaWithSessions;
 import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.ProfileAlias;
 import org.apache.unomi.api.PropertyMergeStrategyExecutor;
 import org.apache.unomi.api.PropertyMergeStrategyType;
 import org.apache.unomi.api.PropertyType;
@@ -68,6 +69,7 @@ import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.TimerTask;
 import java.util.TreeSet;
@@ -546,6 +548,10 @@ public class ProfileServiceImpl implements ProfileService, SynchronousBundleList
     }
 
     public Profile load(String profileId) {
+        ProfileAlias profileAlias = persistenceService.load(profileId, ProfileAlias.class);
+        if (profileAlias != null) {
+            profileId = profileAlias.getProfileID();
+        }
         return persistenceService.load(profileId, Profile.class);
     }
 
@@ -553,6 +559,28 @@ public class ProfileServiceImpl implements ProfileService, SynchronousBundleList
         return save(profile, forceRefreshOnSave);
     }
 
+    @Override
+    public void addAliasToProfile(String profileID, String alias, String clientID) {
+        ProfileAlias profileAlias = persistenceService.load(alias, ProfileAlias.class);
+
+        if (profileAlias == null) {
+            profileAlias = new ProfileAlias();
+
+            profileAlias.setItemId(alias);
+            profileAlias.setItemType(ProfileAlias.ITEM_TYPE);
+            profileAlias.setProfileID(profileID);
+            profileAlias.setClientID(clientID);
+
+            Date creationTime = new Date();
+            profileAlias.setCreationTime(creationTime);
+            profileAlias.setModifiedTime(creationTime);
+
+            persistenceService.save(profileAlias);
+        } else if (!Objects.equals(profileAlias.getProfileID(), profileID)) {
+            throw new IllegalArgumentException("Alias \"" + alias + "\" already used by profile with ID = \"" + profileID + "\"");
+        }
+    }
+
     private Profile save(Profile profile, boolean forceRefresh) {
         if (profile.getItemId() == null) {
             return null;