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

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

commit 5222604ef1a7eb0b44a159f917ce96ae99345cbe
Author: Kevan <ke...@jahia.com>
AuthorDate: Tue Nov 8 15:26:18 2022 +0100

    UNOMI-690, UNOMI-696: refactor control group
---
 .../java/org/apache/unomi/api/ContextResponse.java |  32 +++++-
 .../apache/unomi/api/PersonalizationResult.java    |  65 +++++++++++-
 .../apache/unomi/api/PersonalizationStrategy.java  |   4 +-
 .../main/java/org/apache/unomi/api/Profile.java    |   2 +-
 .../main/java/org/apache/unomi/api/Session.java    |   2 +-
 ...zationResult.java => SystemPropertiesItem.java} |  29 ++----
 .../unomi/rest/endpoints/ContextJsonEndpoint.java  |   5 +
 .../impl/personalization/ControlGroup.java         | 101 ------------------
 .../PersonalizationServiceImpl.java                |  88 ++++------------
 .../sorts/ControlGroupPersonalizationStrategy.java | 115 +++++++++++++++++++++
 .../sorts/FilterPersonalizationStrategy.java       |   5 +-
 .../sorts/RandomPersonalizationStrategy.java       |   8 +-
 .../sorts/ScorePersonalizationStrategy.java        |  15 ++-
 13 files changed, 253 insertions(+), 218 deletions(-)

diff --git a/api/src/main/java/org/apache/unomi/api/ContextResponse.java 
b/api/src/main/java/org/apache/unomi/api/ContextResponse.java
index b7467e018..cb0ee4ab8 100644
--- a/api/src/main/java/org/apache/unomi/api/ContextResponse.java
+++ b/api/src/main/java/org/apache/unomi/api/ContextResponse.java
@@ -21,10 +21,7 @@ import org.apache.unomi.api.conditions.Condition;
 import org.apache.unomi.api.services.RulesService;
 
 import java.io.Serializable;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 
 /**
  * A context server response resulting from the evaluation of a client's 
context request. Note that all returned values result of the evaluation of the 
data provided in the
@@ -60,6 +57,8 @@ public class ContextResponse implements Serializable {
 
     private Map<String, Consent> consents = new LinkedHashMap<>();
 
+    private Map<String, Object> additionalResponseData = new HashMap<>();
+
     /**
      * Retrieves the profile identifier associated with the profile of the 
user on behalf of which the client performed the context request.
      *
@@ -198,10 +197,20 @@ public class ContextResponse implements Serializable {
         this.processedEvents = processedEvents;
     }
 
+    /**
+     * @deprecated personalizations results are more complex since 2.1.0 and 
they are now available under:
+     *             additionalResponseData.personalizationResults
+     */
+    @Deprecated
     public Map<String, List<String>> getPersonalizations() {
         return personalizations;
     }
 
+    /**
+     * @deprecated personalizations results are more complex since 2.1.0 and 
they are now available under:
+     *             additionalResponseData.personalizationResults
+     */
+    @Deprecated
     public void setPersonalizations(Map<String, List<String>> 
personalizations) {
         this.personalizations = personalizations;
     }
@@ -266,4 +275,19 @@ public class ContextResponse implements Serializable {
     public void setConsents(Map<String, Consent> consents) {
         this.consents = consents;
     }
+
+    /**
+     * Useful open map for additional information that could be returned by 
Unomi during the context request.
+     * @return additional properties
+     */
+    public Map<String, Object> getAdditionalResponseData() {
+        return additionalResponseData;
+    }
+
+    /**
+     * Useful open map for additional information that could be returned by 
Unomi during the context request.
+     */
+    public void setAdditionalResponseData(Map<String, Object> 
additionalResponseData) {
+        this.additionalResponseData = additionalResponseData;
+    }
 }
diff --git a/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java 
b/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
index 446189a9a..ceb2e2ade 100644
--- a/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
+++ b/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
@@ -16,27 +16,82 @@
  */
 package org.apache.unomi.api;
 
+import org.apache.unomi.api.services.EventService;
+
+import javax.xml.bind.annotation.XmlTransient;
+import java.io.Serializable;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * A class to contain the result of a personalization, containing the list of 
content IDs as well as a changeType to
- * indicate if a profile and/or a session was modified (to store control group 
information).
+ * indicate if a profile and/or a session was modified.
  */
-public class PersonalizationResult {
+public class PersonalizationResult implements Serializable  {
+
+    public final static String ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP = 
"inControlGroup";
 
     List<String> contentIds;
-    int changeType;
 
-    public PersonalizationResult(List<String> contentIds, int changeType) {
+    Map<String, Object> additionalResultInfos = new HashMap<>();
+
+    int changeType = EventService.NO_CHANGE;
+
+    public PersonalizationResult(List<String> contentIds) {
         this.contentIds = contentIds;
-        this.changeType = changeType;
     }
 
+    /**
+     * List of matching ids for current personalization
+     * @return the list of matching ids
+     */
     public List<String> getContentIds() {
         return contentIds;
     }
 
+    /**
+     * Useful open map to return additional result information to the client
+     * @return map of key/value pair for additional information, like: 
inControlGroup
+     */
+    public Map<String, Object> getAdditionalResultInfos() {
+        return additionalResultInfos;
+    }
+
+    public void setAdditionalResultInfos(Map<String, Object> 
additionalResultInfos) {
+        this.additionalResultInfos = additionalResultInfos;
+    }
+
+    /**
+     * Is the current personalization result in a control group ?
+     * Control group are used to identify a profile or a session that should 
not get personalized results,
+     * instead the current profile/session should get a specific result 
(usually the same for all peoples falling in control group)
+     * Note: it's for now the responsibility of the client to decide what to 
do when the current personalization is under control group.
+     *
+     * @return true in case current profile or session is in control group for 
the personalization.
+     */
+    @XmlTransient
+    public boolean isInControlGroup() {
+        return 
additionalResultInfos.containsKey(ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP) &&
+                (Boolean) 
additionalResultInfos.get(ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP);
+    }
+
+    public void setInControlGroup(boolean inControlGroup) {
+        
this.additionalResultInfos.put(ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP, 
inControlGroup);
+    }
+
+    /**
+     * Change code in case the personalization resolution modified the profile 
or the session
+     * Only used internally, and will not be serialized either for storage or 
response payload.
+     *
+     * @return change code
+     */
+    @XmlTransient
     public int getChangeType() {
         return changeType;
     }
+
+    public void addChanges(int changes) {
+        this.changeType |= changes;
+    }
 }
diff --git 
a/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java 
b/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java
index 152625e3f..5ceb941f7 100644
--- a/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java
+++ b/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java
@@ -32,7 +32,7 @@ public interface PersonalizationStrategy {
      * @param session the session to use for the personalization
      * @param personalizationRequest the request contains the contents to 
personalizes as well as the parameters for the
      *                               strategy (options)
-     * @return a list of content IDs resulting from the filtering/re-ordering
+     * @return the personalization result that contains the list of content 
IDs resulting from the filtering/re-ordering
      */
-    List<String> personalizeList(Profile profile, Session session, 
PersonalizationService.PersonalizationRequest personalizationRequest);
+    PersonalizationResult personalizeList(Profile profile, Session session, 
PersonalizationService.PersonalizationRequest personalizationRequest);
 }
diff --git a/api/src/main/java/org/apache/unomi/api/Profile.java 
b/api/src/main/java/org/apache/unomi/api/Profile.java
index ea9583850..133fc7599 100644
--- a/api/src/main/java/org/apache/unomi/api/Profile.java
+++ b/api/src/main/java/org/apache/unomi/api/Profile.java
@@ -38,7 +38,7 @@ import java.util.*;
  *
  * @see Segment
  */
-public class Profile extends Item {
+public class Profile extends Item implements SystemPropertiesItem {
 
     /**
      * The Profile ITEM_TYPE
diff --git a/api/src/main/java/org/apache/unomi/api/Session.java 
b/api/src/main/java/org/apache/unomi/api/Session.java
index 986323c12..8ca067720 100644
--- a/api/src/main/java/org/apache/unomi/api/Session.java
+++ b/api/src/main/java/org/apache/unomi/api/Session.java
@@ -27,7 +27,7 @@ import java.util.Map;
  * A time-bounded interaction between a user (via their associated {@link 
Profile}) and a unomi-enabled application. A session represents a sequence of 
operations the user
  * performed during its duration. In the context of web applications, sessions 
are usually linked to HTTP sessions.
  */
-public class Session extends Item implements TimestampedItem {
+public class Session extends Item implements TimestampedItem, 
SystemPropertiesItem {
 
     /**
      * The Session ITEM_TYPE.
diff --git a/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java 
b/api/src/main/java/org/apache/unomi/api/SystemPropertiesItem.java
similarity index 57%
copy from api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
copy to api/src/main/java/org/apache/unomi/api/SystemPropertiesItem.java
index 446189a9a..e191562cc 100644
--- a/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
+++ b/api/src/main/java/org/apache/unomi/api/SystemPropertiesItem.java
@@ -16,27 +16,18 @@
  */
 package org.apache.unomi.api;
 
-import java.util.List;
+import java.util.Map;
 
 /**
- * A class to contain the result of a personalization, containing the list of 
content IDs as well as a changeType to
- * indicate if a profile and/or a session was modified (to store control group 
information).
+ * An Item that is holding system properties.
  */
-public class PersonalizationResult {
+public interface SystemPropertiesItem {
 
-    List<String> contentIds;
-    int changeType;
-
-    public PersonalizationResult(List<String> contentIds, int changeType) {
-        this.contentIds = contentIds;
-        this.changeType = changeType;
-    }
-
-    public List<String> getContentIds() {
-        return contentIds;
-    }
-
-    public int getChangeType() {
-        return changeType;
-    }
+    /**
+     * Retrieves a Map of system property name - value pairs for this item. 
System properties can be used by implementations to store non-user visible 
properties needed for
+     * internal purposes.
+     *
+     * @return a Map of system property name - value pairs for this item
+     */
+    Map<String, Object> getSystemProperties();
 }
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 6207845f8..49b1c535b 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
@@ -204,11 +204,16 @@ public class ContextJsonEndpoint {
         List<PersonalizationService.PersonalizationRequest> personalizations = 
contextRequest.getPersonalizations();
         if (personalizations != null) {
             data.setPersonalizations(new HashMap<>());
+            Map<String, PersonalizationResult> personalizationResults = new 
HashMap<>();
             for (PersonalizationService.PersonalizationRequest personalization 
: sanitizePersonalizations(personalizations)) {
                 PersonalizationResult personalizationResult = 
personalizationService.personalizeList(eventsRequestContext.getProfile(), 
eventsRequestContext.getSession(), personalization);
                 
eventsRequestContext.addChanges(personalizationResult.getChangeType());
+
+                // Support for old personalization result in response
                 data.getPersonalizations().put(personalization.getId(), 
personalizationResult.getContentIds());
+                personalizationResults.put(personalization.getId(), 
personalizationResult);
             }
+            data.getAdditionalResponseData().put("personalizationResults", 
personalizationResults);
         }
 
         if (contextRequest.isRequireSegments()) {
diff --git 
a/services/src/main/java/org/apache/unomi/services/impl/personalization/ControlGroup.java
 
b/services/src/main/java/org/apache/unomi/services/impl/personalization/ControlGroup.java
deleted file mode 100644
index 795080e87..000000000
--- 
a/services/src/main/java/org/apache/unomi/services/impl/personalization/ControlGroup.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * 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.services.impl.personalization;
-
-import org.apache.unomi.persistence.spi.CustomObjectMapper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.text.ParseException;
-import java.util.Date;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * Represents a personalization control group, stored in a profile and/or a 
session
- */
-public class ControlGroup {
-
-    private static final Logger logger = 
LoggerFactory.getLogger(ControlGroup.class.getName());
-
-    String id;
-    String displayName;
-    String path;
-    Date timeStamp;
-
-    public ControlGroup(String id, String displayName, String path, Date 
timeStamp) {
-        this.id = id;
-        this.displayName = displayName;
-        this.path = path;
-        this.timeStamp = timeStamp;
-    }
-
-    public static ControlGroup fromMap(Map<String,Object> map) {
-        String id = (String) map.get("id");
-        String displayName = (String) map.get("displayName");
-        String path = (String) map.get("path");
-        String dateStr = (String) map.get("timeStamp");
-        Date date = null;
-        try {
-            date = 
CustomObjectMapper.getObjectMapper().getDateFormat().parse(dateStr);
-        } catch (ParseException e) {
-            logger.error("Error parsing control group date", e);
-        }
-        return new ControlGroup(id, displayName, path, date);
-    }
-
-    public String getId() {
-        return id;
-    }
-
-    public void setId(String id) {
-        this.id = id;
-    }
-
-    public String getDisplayName() {
-        return displayName;
-    }
-
-    public void setDisplayName(String displayName) {
-        this.displayName = displayName;
-    }
-
-    public String getPath() {
-        return path;
-    }
-
-    public void setPath(String path) {
-        this.path = path;
-    }
-
-    public Date getTimeStamp() {
-        return timeStamp;
-    }
-
-    public void setTimeStamp(Date timeStamp) {
-        this.timeStamp = timeStamp;
-    }
-
-    public Map<String,Object> toMap() {
-        Map<String,Object> result = new LinkedHashMap<>();
-        result.put("id", id);
-        result.put("displayName", displayName);
-        result.put("path", path);
-        result.put("timeStamp", 
CustomObjectMapper.getObjectMapper().getDateFormat().format(timeStamp));
-        return result;
-    }
-}
diff --git 
a/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java
 
b/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java
index 1d2cfd760..9fef8ba2c 100644
--- 
a/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java
+++ 
b/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java
@@ -17,30 +17,27 @@
 
 package org.apache.unomi.services.impl.personalization;
 
-import org.apache.unomi.api.PersonalizationResult;
-import org.apache.unomi.api.PersonalizationStrategy;
-import org.apache.unomi.api.Profile;
-import org.apache.unomi.api.Session;
+import org.apache.unomi.api.*;
 import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.services.EventService;
 import org.apache.unomi.api.services.PersonalizationService;
 import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.services.sorts.ControlGroupPersonalizationStrategy;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.ServiceReference;
 
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
+
+import static 
org.apache.unomi.services.sorts.ControlGroupPersonalizationStrategy.CONTROL_GROUP_CONFIG;
 
 public class PersonalizationServiceImpl implements PersonalizationService {
 
-    public static final String CONTROL_GROUPS_PROPERTY_NAME = 
"unomiControlGroups";
     private BundleContext bundleContext;
     private ProfileService profileService;
 
     private Map<String, PersonalizationStrategy> personalizationStrategies = 
new ConcurrentHashMap<>();
+    private final PersonalizationStrategy controlGroupStrategy = new 
ControlGroupPersonalizationStrategy();
 
-    private Random controlGroupRandom = new Random();
 
     public void setProfileService(ProfileService profileService) {
         this.profileService = profileService;
@@ -88,73 +85,24 @@ public class PersonalizationServiceImpl implements 
PersonalizationService {
     @Override
     public PersonalizationResult personalizeList(Profile profile, Session 
session, PersonalizationRequest personalizationRequest) {
         PersonalizationStrategy strategy = 
personalizationStrategies.get(personalizationRequest.getStrategy());
-        int changeType = EventService.NO_CHANGE;
 
         if (strategy != null) {
-            if (personalizationRequest.getStrategyOptions() != null && 
personalizationRequest.getStrategyOptions().containsKey("controlGroup")) {
-                Map<String,Object> controlGroupMap = (Map<String,Object>) 
personalizationRequest.getStrategyOptions().get("controlGroup");
-
-                boolean storeInSession = false;
-                if (controlGroupMap.containsKey("storeInSession")) {
-                    storeInSession = (Boolean) 
controlGroupMap.get("storeInSession");
+            // hook on control group if necessary
+            PersonalizationResult controlGroupStrategyResult = null;
+            if (personalizationRequest.getStrategyOptions() != null && 
personalizationRequest.getStrategyOptions().containsKey(CONTROL_GROUP_CONFIG)) {
+                controlGroupStrategyResult = 
controlGroupStrategy.personalizeList(profile, session, personalizationRequest);
+                if (controlGroupStrategyResult.isInControlGroup()) {
+                    return controlGroupStrategyResult;
                 }
+            }
 
-                boolean profileInControlGroup = false;
-                Optional<ControlGroup> currentControlGroup;
-
-                List<ControlGroup> controlGroups = null;
-                if (storeInSession) {
-                    if (session.getProperty(CONTROL_GROUPS_PROPERTY_NAME) != 
null) {
-                        controlGroups = ((List<Map<String, Object>>) 
session.getProperty(CONTROL_GROUPS_PROPERTY_NAME)).stream().map(ControlGroup::fromMap).collect(Collectors.toList());
-                    }
-                } else {
-                    if (profile.getProperty(CONTROL_GROUPS_PROPERTY_NAME) != 
null) {
-                        controlGroups = ((List<Map<String, Object>>) 
profile.getProperty(CONTROL_GROUPS_PROPERTY_NAME)).stream().map(ControlGroup::fromMap).collect(Collectors.toList());
-                    }
-                }
-                if (controlGroups == null) {
-                    controlGroups = new ArrayList<>();
-                }
-                currentControlGroup = 
controlGroups.stream().filter(controlGroup -> 
controlGroup.id.equals(personalizationRequest.getId())).findFirst();
-                if (currentControlGroup.isPresent()) {
-                    // we already have an entry for this personalization so 
this means the profile is in the control group
-                    profileInControlGroup = true;
-                } else {
-                    double randomDouble = controlGroupRandom.nextDouble() * 
100.0;
-                    Object percentageObject = 
controlGroupMap.get("percentage");
-                    Double controlGroupPercentage = null;
-                    if (percentageObject != null) {
-                        if (percentageObject instanceof Double) {
-                            controlGroupPercentage = (Double) percentageObject;
-                        } else if (percentageObject instanceof Integer) {
-                            controlGroupPercentage = ((Integer) 
percentageObject).doubleValue();
-                        }
-                    }
-
-                    if (randomDouble <= controlGroupPercentage) {
-                        // Profile is elected to be in control group
-                        profileInControlGroup = true;
-                        ControlGroup controlGroup = new 
ControlGroup(personalizationRequest.getId(),
-                                (String) controlGroupMap.get("displayName"),
-                                (String) controlGroupMap.get("path"),
-                                new Date());
-                        controlGroups.add(controlGroup);
-                        List<Map<String,Object>> controlGroupsMap = 
controlGroups.stream().map(ControlGroup::toMap).collect(Collectors.toList());
-                        if (storeInSession) {
-                            session.setProperty(CONTROL_GROUPS_PROPERTY_NAME, 
controlGroupsMap);
-                            changeType = EventService.SESSION_UPDATED;
-                        } else {
-                            profile.setProperty(CONTROL_GROUPS_PROPERTY_NAME, 
controlGroupsMap);
-                            changeType = EventService.PROFILE_UPDATED;
-                        }
-                    }
-                }
-                if (profileInControlGroup) {
-                    // if profile is in control group we return the unmodified 
list.
-                    return new 
PersonalizationResult(personalizationRequest.getContents().stream().map(PersonalizedContent::getId).collect(Collectors.toList()),
 changeType);
-                }
+            // Execute the original strategy
+            PersonalizationResult originalStrategyResult = 
strategy.personalizeList(profile, session, personalizationRequest);
+            // even if control group is false, profile or session could have 
been modified
+            if (controlGroupStrategyResult != null) {
+                
originalStrategyResult.addChanges(controlGroupStrategyResult.getChangeType());
             }
-            return new PersonalizationResult(strategy.personalizeList(profile, 
session, personalizationRequest), changeType);
+            return originalStrategyResult;
         }
 
         throw new IllegalArgumentException("Unknown strategy : "+ 
personalizationRequest.getStrategy());
diff --git 
a/services/src/main/java/org/apache/unomi/services/sorts/ControlGroupPersonalizationStrategy.java
 
b/services/src/main/java/org/apache/unomi/services/sorts/ControlGroupPersonalizationStrategy.java
new file mode 100644
index 000000000..3361d5182
--- /dev/null
+++ 
b/services/src/main/java/org/apache/unomi/services/sorts/ControlGroupPersonalizationStrategy.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.services.sorts;
+
+import org.apache.unomi.api.*;
+import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.api.services.PersonalizationService;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * System strategy to calculate control group on personalization that would 
use such configuration
+ * The status is then stored in the current profile/session under: 
systemProperties.personalizationStrategyStatus
+ * A control group is used with a percentage to decide if the current 
profile/session should have personalized results or not.
+ * - in case of control group is true: the variants will be return untouched 
like received and an information will be added to the personalized results to 
warn the client
+ * - in case of control group is false: we will execute the personalization 
normally
+ */
+public class ControlGroupPersonalizationStrategy implements 
PersonalizationStrategy {
+    public static final String PERSONALIZATION_STRATEGY_STATUS = 
"personalizationStrategyStatus";
+    public static final String PERSONALIZATION_STRATEGY_STATUS_ID = 
"personalizationId";
+    public static final String PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP = 
"inControlGroup";
+    public static final String PERSONALIZATION_STRATEGY_STATUS_DATE = 
"timeStamp";
+
+    public static final String CONTROL_GROUP_CONFIG_STORE_IN_SESSION = 
"storeInSession";
+    public static final String CONTROL_GROUP_CONFIG_PERCENTAGE = "percentage";
+    public static final String CONTROL_GROUP_CONFIG = "controlGroup";
+
+    private final Random controlGroupRandom = new Random();
+
+    @Override
+    public PersonalizationResult personalizeList(Profile profile, Session 
session, PersonalizationService.PersonalizationRequest personalizationRequest) {
+        if (personalizationRequest.getStrategyOptions() != null && 
personalizationRequest.getStrategyOptions().containsKey(CONTROL_GROUP_CONFIG)) {
+            Map<String, Object> controlGroupMap = (Map<String, Object>) 
personalizationRequest.getStrategyOptions().get(CONTROL_GROUP_CONFIG);
+
+            return 
controlGroupMap.containsKey(CONTROL_GROUP_CONFIG_STORE_IN_SESSION) ?
+                    
getPersonalizationResultForControlGroup(personalizationRequest, session, 
controlGroupMap, EventService.SESSION_UPDATED) :
+                    
getPersonalizationResultForControlGroup(personalizationRequest, profile, 
controlGroupMap, EventService.PROFILE_UPDATED);
+        }
+
+        throw new IllegalArgumentException("Not possible to perform control 
group strategy without control group config");
+    }
+
+    private PersonalizationResult 
getPersonalizationResultForControlGroup(PersonalizationService.PersonalizationRequest
 personalizationRequest, SystemPropertiesItem systemPropertiesItem, Map<String, 
Object> controlGroupConfig, int changeType) {
+        // Control group will return the same untouched list of received 
content ids
+        PersonalizationResult personalizationResult = new 
PersonalizationResult(
+                
personalizationRequest.getContents().stream().map(PersonalizationService.PersonalizedContent::getId).collect(Collectors.toList()));
+
+        // get the list of existing personalization strategy status
+        List<Map<String, Object>> strategyStatuses;
+        if 
(systemPropertiesItem.getSystemProperties().get(PERSONALIZATION_STRATEGY_STATUS)
 == null) {
+            strategyStatuses = new ArrayList<>();
+            
systemPropertiesItem.getSystemProperties().put(PERSONALIZATION_STRATEGY_STATUS, 
strategyStatuses);
+        } else {
+            strategyStatuses = (List<Map<String, Object>>) 
systemPropertiesItem.getSystemProperties().get(PERSONALIZATION_STRATEGY_STATUS);
+        }
+
+        // Check if we need to update an old status that would not contains 
control group info
+        boolean inControlGroup;
+        for (Map<String, Object> oldStrategyStatus : strategyStatuses) {
+            if 
(personalizationRequest.getId().equals(oldStrategyStatus.get(PERSONALIZATION_STRATEGY_STATUS_ID)))
 {
+                // Check if we have to update the strategy status or not ?
+                if 
(!oldStrategyStatus.containsKey(PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP)) 
{
+
+                    // Old status doesn't contain any control group check, we 
need to calculate it and update the old status with the result.
+                    inControlGroup = calculateControlGroup(controlGroupConfig);
+                    
oldStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP, 
inControlGroup);
+                    
oldStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_DATE, new Date());
+                    personalizationResult.addChanges(changeType);
+
+                } else {
+                    // Just read existing status about the control group
+                    inControlGroup = (boolean) 
oldStrategyStatus.get(PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP);
+                }
+
+                personalizationResult.setInControlGroup(inControlGroup);
+                return personalizationResult;
+            }
+        }
+
+        // We didn't found any existing status for the current perso, we need 
to create a new one.
+        inControlGroup = calculateControlGroup(controlGroupConfig);
+        Map<String, Object> newStrategyStatus = new HashMap<>();
+        newStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_ID, 
personalizationRequest.getId());
+        newStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_DATE, new 
Date());
+        newStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP, 
inControlGroup);
+        strategyStatuses.add(newStrategyStatus);
+
+        personalizationResult.addChanges(changeType);
+        personalizationResult.setInControlGroup(inControlGroup);
+        return personalizationResult;
+    }
+
+    private boolean calculateControlGroup(Map<String,Object> 
controlGroupConfig) {
+        double percentage = 
(controlGroupConfig.get(CONTROL_GROUP_CONFIG_PERCENTAGE) != null && 
controlGroupConfig.get(CONTROL_GROUP_CONFIG_PERCENTAGE) instanceof Number) ?
+                ((Number) 
controlGroupConfig.get(CONTROL_GROUP_CONFIG_PERCENTAGE)).doubleValue() : 0;
+
+        double random = controlGroupRandom.nextDouble() * 100.0;
+        return random <= percentage;
+    }
+}
diff --git 
a/services/src/main/java/org/apache/unomi/services/sorts/FilterPersonalizationStrategy.java
 
b/services/src/main/java/org/apache/unomi/services/sorts/FilterPersonalizationStrategy.java
index 809a724d6..a79bc62bf 100644
--- 
a/services/src/main/java/org/apache/unomi/services/sorts/FilterPersonalizationStrategy.java
+++ 
b/services/src/main/java/org/apache/unomi/services/sorts/FilterPersonalizationStrategy.java
@@ -17,6 +17,7 @@
 
 package org.apache.unomi.services.sorts;
 
+import org.apache.unomi.api.PersonalizationResult;
 import org.apache.unomi.api.Profile;
 import org.apache.unomi.api.Session;
 import org.apache.unomi.api.PersonalizationStrategy;
@@ -39,7 +40,7 @@ public class FilterPersonalizationStrategy implements 
PersonalizationStrategy {
     }
 
     @Override
-    public List<String> personalizeList(Profile profile, Session session, 
PersonalizationService.PersonalizationRequest personalizationRequest) {
+    public PersonalizationResult personalizeList(Profile profile, Session 
session, PersonalizationService.PersonalizationRequest personalizationRequest) {
         List<String> sortedContent = new ArrayList<>();
         for (PersonalizationService.PersonalizedContent personalizedContent : 
personalizationRequest.getContents()) {
             boolean result = true;
@@ -59,6 +60,6 @@ public class FilterPersonalizationStrategy implements 
PersonalizationStrategy {
             sortedContent.add(fallback);
         }
 
-        return sortedContent;
+        return new PersonalizationResult(sortedContent);
     }
 }
diff --git 
a/services/src/main/java/org/apache/unomi/services/sorts/RandomPersonalizationStrategy.java
 
b/services/src/main/java/org/apache/unomi/services/sorts/RandomPersonalizationStrategy.java
index 3dd31f78b..76bb168af 100644
--- 
a/services/src/main/java/org/apache/unomi/services/sorts/RandomPersonalizationStrategy.java
+++ 
b/services/src/main/java/org/apache/unomi/services/sorts/RandomPersonalizationStrategy.java
@@ -17,19 +17,19 @@
 
 package org.apache.unomi.services.sorts;
 
+import org.apache.unomi.api.PersonalizationResult;
 import org.apache.unomi.api.Profile;
 import org.apache.unomi.api.Session;
 import org.apache.unomi.api.services.PersonalizationService;
 
 import java.util.Collections;
-import java.util.List;
 
 public class RandomPersonalizationStrategy extends 
FilterPersonalizationStrategy {
 
     @Override
-    public List<String> personalizeList(Profile profile, Session session, 
PersonalizationService.PersonalizationRequest personalizationRequest) {
-        List<String> r = super.personalizeList(profile, session, 
personalizationRequest);
-        Collections.shuffle(r);
+    public PersonalizationResult personalizeList(Profile profile, Session 
session, PersonalizationService.PersonalizationRequest personalizationRequest) {
+        PersonalizationResult r = super.personalizeList(profile, session, 
personalizationRequest);
+        Collections.shuffle(r.getContentIds());
         return r;
     }
 }
diff --git 
a/services/src/main/java/org/apache/unomi/services/sorts/ScorePersonalizationStrategy.java
 
b/services/src/main/java/org/apache/unomi/services/sorts/ScorePersonalizationStrategy.java
index 82bcc165b..93d6c0ebe 100644
--- 
a/services/src/main/java/org/apache/unomi/services/sorts/ScorePersonalizationStrategy.java
+++ 
b/services/src/main/java/org/apache/unomi/services/sorts/ScorePersonalizationStrategy.java
@@ -17,6 +17,7 @@
 
 package org.apache.unomi.services.sorts;
 
+import org.apache.unomi.api.PersonalizationResult;
 import org.apache.unomi.api.Profile;
 import org.apache.unomi.api.Session;
 import org.apache.unomi.api.PersonalizationStrategy;
@@ -35,7 +36,7 @@ public class ScorePersonalizationStrategy implements 
PersonalizationStrategy {
     }
 
     @Override
-    public List<String> personalizeList(Profile profile, Session session, 
PersonalizationService.PersonalizationRequest personalizationRequest) {
+    public PersonalizationResult personalizeList(Profile profile, Session 
session, PersonalizationService.PersonalizationRequest personalizationRequest) {
         List<String> sortedContent = new ArrayList<>();
         final Map<String,Integer> t = new HashMap<>();
 
@@ -64,7 +65,7 @@ public class ScorePersonalizationStrategy implements 
PersonalizationStrategy {
 
             String scoringPlanList = (String) 
(personalizedContent.getProperties() != null ? 
personalizedContent.getProperties().get("scoringPlans") : null);
             if (scoringPlanList != null) {
-                Map<String,Integer> scoreValues = (Map<String, Integer>) 
profile.getScores();
+                Map<String,Integer> scoreValues = profile.getScores();
                 for (String scoringPlan : scoringPlanList.split(" ")) {
                     if (scoreValues.get(scoringPlan) != null) {
                         score += scoreValues.get(scoringPlan);
@@ -93,18 +94,14 @@ public class ScorePersonalizationStrategy implements 
PersonalizationStrategy {
                 sortedContent.add(personalizedContent.getId());
             }
         }
-        Collections.sort(sortedContent, new Comparator<String>() {
-            @Override
-            public int compare(String o1, String o2) {
-                return t.get(o2) - t.get(o1);
-            }
-        });
+
+        sortedContent.sort((o1, o2) -> t.get(o2) - t.get(o1));
 
         String fallback = (String) 
personalizationRequest.getStrategyOptions().get("fallback");
         if (fallback != null && !sortedContent.contains(fallback)) {
             sortedContent.add(fallback);
         }
 
-        return sortedContent;
+        return new PersonalizationResult(sortedContent);
     }
 }


Reply via email to