This is an automated email from the ASF dual-hosted git repository.
jkevan pushed a commit to branch unomi-1.x
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to refs/heads/unomi-1.x by this push:
new cb06fe397 UNOMI-690, UNOMI-696: Backport control group refactoring
(#534)
cb06fe397 is described below
commit cb06fe3977c2a6135af688ca4dd1df216ad58c7c
Author: kevan Jahanshahi <[email protected]>
AuthorDate: Thu Nov 10 15:48:47 2022 +0100
UNOMI-690, UNOMI-696: Backport control group refactoring (#534)
* UNOMI-690, UNOMI-696: refactor control group (#531)
* UNOMI-690, UNOMI-696: refactor control group
* UNOMI-690, UNOMI-696: Fix itests
* UNOMI-690, UNOMI-696: Add new control group integration test and cover
the feature with strong integration tests
* UNOMI-690, UNOMI-696: Fix java doc generation (#533)
* UNOMI-690, UNOMI-696: Adapt original code to 1_x branch
---
.../java/org/apache/unomi/api/ContextResponse.java | 40 +-
.../apache/unomi/api/PersonalizationResult.java | 72 +-
.../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 +-
.../test/java/org/apache/unomi/itests/BaseIT.java | 11 +-
.../org/apache/unomi/itests/ContextServletIT.java | 305 ++++--
.../resources/personalization-control-group.json | 49 +
.../resources/personalization-controlgroup.json | 1031 --------------------
.../personalization-no-control-group.json | 45 +
.../unomi/rest/endpoints/ContextJsonEndpoint.java | 5 +-
.../impl/personalization/ControlGroup.java | 101 --
.../PersonalizationServiceImpl.java | 90 +-
.../sorts/ControlGroupPersonalizationStrategy.java | 117 +++
.../sorts/FilterPersonalizationStrategy.java | 5 +-
.../sorts/RandomPersonalizationStrategy.java | 8 +-
.../sorts/ScorePersonalizationStrategy.java | 15 +-
18 files changed, 591 insertions(+), 1340 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..78382af03 100644
--- a/api/src/main/java/org/apache/unomi/api/ContextResponse.java
+++ b/api/src/main/java/org/apache/unomi/api/ContextResponse.java
@@ -20,11 +20,10 @@ package org.apache.unomi.api;
import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.api.services.RulesService;
+import javax.xml.bind.annotation.XmlTransient;
import java.io.Serializable;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
+import java.util.stream.Collectors;
/**
* 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
@@ -52,7 +51,7 @@ public class ContextResponse implements Serializable {
private int processedEvents;
- private Map<String, List<String>> personalizations;
+ private Map<String, PersonalizationResult> personalizationResults;
private Set<Condition> trackedConditions;
@@ -198,12 +197,39 @@ public class ContextResponse implements Serializable {
this.processedEvents = processedEvents;
}
+ /**
+ * @deprecated personalizations results are more complex since 2.1.0 and
they are now available under: getPersonalizationResults()
+ */
+ @Deprecated
+ @XmlTransient
public Map<String, List<String>> getPersonalizations() {
- return personalizations;
+ if (personalizationResults != null) {
+ return personalizationResults.entrySet()
+ .stream()
+ .collect(Collectors.toMap(Map.Entry::getKey,
+ entry -> entry.getValue().getContentIds()));
+ }
+ return null;
}
+ /**
+ * @deprecated personalizations results are more complex since 2.1.0 and
they are now available under: setPersonalizationResults()
+ */
+ @Deprecated
public void setPersonalizations(Map<String, List<String>>
personalizations) {
- this.personalizations = personalizations;
+ // do nothing, use setPersonalizationResults() instead
+ }
+
+ /**
+ * Get the result of the personalization resolutions done during the
context request.
+ * @return a Map key/value pair (key:personalization id, value:the result
that contains the matching content ids and additional information)
+ */
+ public Map<String, PersonalizationResult> getPersonalizationResults() {
+ return personalizationResults;
+ }
+
+ public void setPersonalizationResults(Map<String, PersonalizationResult>
personalizationResults) {
+ this.personalizationResults = personalizationResults;
}
/**
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..15c6ab5a5 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,89 @@
*/
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() {
+ }
+
+ 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;
}
+ public void setContentIds(List<String> contentIds) {
+ this.contentIds = 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 7115bd563..9c07928bd 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 7260e9ec3..9d7209072 100644
--- a/api/src/main/java/org/apache/unomi/api/Session.java
+++ b/api/src/main/java/org/apache/unomi/api/Session.java
@@ -25,7 +25,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/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
index e75bee6b9..584d7e2fb 100644
--- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
@@ -243,7 +243,8 @@ public abstract class BaseIT {
return options.toArray(new Option[0]);
}
- protected <T> T keepTrying(String failMessage, Supplier<T> call,
Predicate<T> predicate, int timeout, int retries) throws InterruptedException {
+ protected <T> T keepTrying(String failMessage, Supplier<T> call,
Predicate<T> predicate, int timeout, int retries)
+ throws InterruptedException {
int count = 0;
T value = null;
while (value == null || !predicate.test(value)) {
@@ -265,10 +266,10 @@ public abstract class BaseIT {
}
}
- protected String getValidatedBundleJSON(final String resourcePath,
Map<String,String> parameters) throws IOException {
+ protected String getValidatedBundleJSON(final String resourcePath,
Map<String, String> parameters) throws IOException {
String jsonString = bundleResourceAsString(resourcePath);
if (parameters != null && parameters.size() > 0) {
- for (Map.Entry<String,String> parameterEntry :
parameters.entrySet()) {
+ for (Map.Entry<String, String> parameterEntry :
parameters.entrySet()) {
jsonString = jsonString.replace("###" +
parameterEntry.getKey() + "###", parameterEntry.getValue());
}
}
@@ -347,4 +348,8 @@ public abstract class BaseIT {
100);
rulesService.refreshRules();
}
+
+ public String getFullUrl(String url) throws Exception {
+ return URL + url;
+ }
}
diff --git a/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
index 1147b906e..9d0fa26bc 100644
--- a/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
@@ -66,6 +66,10 @@ public class ContextServletIT extends BaseIT {
private final static String SEGMENT_ID = "test-segment-id";
private final static int SEGMENT_NUMBER_OF_DAYS = 30;
+ private final static String TEST_SESSION_ID = "dummy-session";
+ private final static String TEST_PROFILE_ID = "test-profile-id";
+ private final static String TEST_PROFILE_FIRST_NAME =
"contextServletIT_profile";
+
private ObjectMapper objectMapper = new ObjectMapper();
@Inject
@@ -106,6 +110,7 @@ public class ContextServletIT extends BaseIT {
String profileId = "test-profile-id";
profile = new Profile(profileId);
+ profile.setProperty("firstName", TEST_PROFILE_FIRST_NAME);
profileService.save(profile);
refreshPersistence();
@@ -400,88 +405,6 @@ public class ContextServletIT extends BaseIT {
}
- @Test
- public void testPersonalizationWithControlGroup() throws IOException,
InterruptedException {
-
- Map<String,String> parameters = new HashMap<>();
- parameters.put("storeInSession", "false");
- HttpPost request = new HttpPost(URL + CONTEXT_URL);
- request.setEntity(new
StringEntity(getValidatedBundleJSON("personalization-controlgroup.json",
parameters), ContentType.create("application/json")));
- TestUtils.RequestResponse response =
TestUtils.executeContextJSONRequest(request);
- assertEquals("Invalid response code", 200, response.getStatusCode());
- refreshPersistence();
- Thread.sleep(2000); //Making sure event is updated in DB
- ContextResponse contextResponse = response.getContextResponse();
-
- Map<String,List<String>> personalizations =
contextResponse.getPersonalizations();
-
- validatePersonalizations(personalizations);
-
- // let's check that the persisted profile has the control groups;
- Map<String,Object> profileProperties =
contextResponse.getProfileProperties();
- List<Map<String,Object>> profileControlGroups =
(List<Map<String,Object>>) profileProperties.get("unomiControlGroups");
- assertControlGroups(profileControlGroups);
-
- Profile updatedProfile =
profileService.load(contextResponse.getProfileId());
- profileControlGroups = (List<Map<String,Object>>)
updatedProfile.getProperty("unomiControlGroups");
- assertNotNull("Profile control groups not found in persisted profile",
profileControlGroups);
- assertControlGroups(profileControlGroups);
-
- // now let's test with session storage
- parameters.put("storeInSession", "true");
- request = new HttpPost(URL + CONTEXT_URL);
- request.setEntity(new
StringEntity(getValidatedBundleJSON("personalization-controlgroup.json",
parameters), ContentType.create("application/json")));
- response = TestUtils.executeContextJSONRequest(request);
- assertEquals("Invalid response code", 200, response.getStatusCode());
- refreshPersistence();
- Thread.sleep(2000); //Making sure event is updated in DB
- contextResponse = response.getContextResponse();
-
- personalizations = contextResponse.getPersonalizations();
-
- validatePersonalizations(personalizations);
-
- Map<String,Object> sessionProperties =
contextResponse.getSessionProperties();
- List<Map<String,Object>> sessionControlGroups =
(List<Map<String,Object>>) sessionProperties.get("unomiControlGroups");
- assertControlGroups(sessionControlGroups);
-
- Session updatedSession =
profileService.loadSession(contextResponse.getSessionId(), new Date());
- sessionControlGroups = (List<Map<String,Object>>)
updatedSession.getProperty("unomiControlGroups");
- assertNotNull("Session control groups not found in persisted session",
sessionControlGroups);
- assertControlGroups(sessionControlGroups);
-
- }
-
- private void validatePersonalizations(Map<String, List<String>>
personalizations) {
- assertEquals("Personalizations don't have expected size", 2,
personalizations.size());
-
- List<String> perso1Contents = personalizations.get("perso1");
- assertEquals("Perso 1 content list size doesn't match", 10,
perso1Contents.size());
- List<String> expectedPerso1Contents = new ArrayList<>();
- expectedPerso1Contents.add("perso1content1");
- expectedPerso1Contents.add("perso1content2");
- expectedPerso1Contents.add("perso1content3");
- expectedPerso1Contents.add("perso1content4");
- expectedPerso1Contents.add("perso1content5");
- expectedPerso1Contents.add("perso1content6");
- expectedPerso1Contents.add("perso1content7");
- expectedPerso1Contents.add("perso1content8");
- expectedPerso1Contents.add("perso1content9");
- expectedPerso1Contents.add("perso1content10");
- assertEquals("Perso1 contents do not match", expectedPerso1Contents,
perso1Contents);
- }
-
- private void assertControlGroups(List<Map<String, Object>>
profileControlGroups) {
- assertNotNull("Couldn't find control groups for profile",
profileControlGroups);
- assertTrue("Control group size should be 1",
profileControlGroups.size() == 1);
- Map<String,Object> controlGroup = profileControlGroups.get(0);
- assertEquals("Invalid ID for control group", "perso1",
controlGroup.get("id"));
- assertEquals("Invalid path for control group", "/home/perso1.html",
controlGroup.get("path"));
- assertEquals("Invalid displayName for control group", "First perso",
controlGroup.get("displayName"));
- assertNotNull("Null timestamp for control group",
controlGroup.get("timeStamp"));
- }
-
-
@Test
public void testRequireScoring() throws IOException, InterruptedException {
@@ -522,4 +445,222 @@ public class ContextServletIT extends BaseIT {
segmentService.removeScoringDefinition(scoring.getItemId(), false);
}
+ @Test
+ public void test_no_ControlGroup() throws Exception {
+ performPersonalizationWithControlGroup(
+ null,
+ Collections.singletonList("no-condition"),
+ false,
+ false,
+ null,
+ null);
+ }
+
+ @Test
+ public void test_in_ControlGroup_profile_stored() throws Exception {
+ performPersonalizationWithControlGroup(
+ generateControlGroupConfig("false", "100.0"),
+ Arrays.asList("first-name-missing", "no-condition"),
+ true,
+ true,
+ true,
+ null);
+
+ performPersonalizationWithControlGroup(
+ generateControlGroupConfig("false", "0.0"),
+ Arrays.asList("first-name-missing", "no-condition"),
+ true,
+ true,
+ true,
+ null);
+ }
+
+ @Test
+ public void test_in_ControlGroup_session_stored() throws Exception {
+ performPersonalizationWithControlGroup(
+ generateControlGroupConfig("true", "100.0"),
+ Arrays.asList("first-name-missing", "no-condition"),
+ true,
+ true,
+ null,
+ true);
+
+ performPersonalizationWithControlGroup(
+ generateControlGroupConfig("true", "0.0"),
+ Arrays.asList("first-name-missing", "no-condition"),
+ true,
+ true,
+ null,
+ true);
+ }
+
+ @Test
+ public void test_out_ControlGroup_profile_stored() throws Exception {
+ performPersonalizationWithControlGroup(
+ generateControlGroupConfig("false", "0.0"),
+ Collections.singletonList("no-condition"),
+ true,
+ false,
+ false,
+ null);
+
+ performPersonalizationWithControlGroup(
+ generateControlGroupConfig("false", "100.0"),
+ Collections.singletonList("no-condition"),
+ true,
+ false,
+ false,
+ null);
+ }
+
+ @Test
+ public void test_out_ControlGroup_session_stored() throws Exception {
+ performPersonalizationWithControlGroup(
+ generateControlGroupConfig("true", "0.0"),
+ Collections.singletonList("no-condition"),
+ true,
+ false,
+ null,
+ false);
+
+ performPersonalizationWithControlGroup(
+ generateControlGroupConfig("true", "100.0"),
+ Collections.singletonList("no-condition"),
+ true,
+ false,
+ null,
+ false);
+ }
+
+ @Test
+ public void test_advanced_ControlGroup_test() throws Exception {
+ // STEP 1: start with no control group
+ performPersonalizationWithControlGroup(
+ null,
+ Collections.singletonList("no-condition"),
+ false,
+ false,
+ null,
+ null);
+
+ // STEP 2: then enable control group stored in profile
+ performPersonalizationWithControlGroup(
+ generateControlGroupConfig("false", "100.0"),
+ Arrays.asList("first-name-missing", "no-condition"),
+ true,
+ true,
+ true,
+ null);
+
+ // STEP 3: then re disable control group
+ performPersonalizationWithControlGroup(
+ null,
+ Collections.singletonList("no-condition"),
+ false,
+ false,
+ /* We can see we still have old control group check stored in
the profile */ true,
+ null);
+
+ // STEP 4: then re-enable control group, but session scoped this time,
with a 0 percentage
+ performPersonalizationWithControlGroup(
+ generateControlGroupConfig("true", "0.0"),
+ Collections.singletonList("no-condition"),
+ true,
+ false,
+ /* We can see we still have old control group check stored in
the profile */ true,
+ /* And now we also have a status saved in the session */
false);
+
+ // STEP 5: then re-enable control group, but profile scoped this time,
with a 0 percentage
+ // We should be in control group because of the STEP 2, the
current profile already contains a persisted status for the perso.
+ // So even if current config is 0, old check already flagged
current profile to be in the control group.
+ performPersonalizationWithControlGroup(
+ generateControlGroupConfig("false", "0.0"),
+ Arrays.asList("first-name-missing", "no-condition"),
+ true,
+ true,
+ /* We can see we still have old control group check stored in
the profile */ true,
+ /* We can see we still have old control group check stored in
the session too */ false);
+
+ // STEP 6: then re-enable control group, but session scoped this time,
with a 100 percentage
+ // We should not be in control group because of the STEP 4,
the current session already contains a persisted status for the perso.
+ // So even if current config is 100, old check already flagged
current profile to not be in the control group.
+ performPersonalizationWithControlGroup(
+ generateControlGroupConfig("true", "100.0"),
+ Collections.singletonList("no-condition"),
+ true,
+ false,
+ /* We can see we still have old control group check stored in
the profile */ true,
+ /* We can see we still have old control group check stored in
the session too */ false);
+
+ // STEP 7: then re disable control group
+ performPersonalizationWithControlGroup(
+ null,
+ Collections.singletonList("no-condition"),
+ false,
+ false,
+ /* We can see we still have old control group check stored in
the profile */ true,
+ /* We can see we still have old control group check stored in
the session too */ false);
+ }
+
+ private void performPersonalizationWithControlGroup(Map<String, String>
controlGroupConfig, List<String> expectedVariants,
+ boolean
expectedControlGroupInfoInPersoResult, boolean
expectedControlGroupValueInPersoResult,
+ Boolean
expectedControlGroupValueInProfile, Boolean expectedControlGroupValueInSession)
throws Exception {
+ // Test normal personalization should not have control group info in
response
+
+ HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL));
+ if (controlGroupConfig != null) {
+ request.setEntity(new
StringEntity(getValidatedBundleJSON("personalization-control-group.json",
controlGroupConfig), ContentType.APPLICATION_JSON));
+ } else {
+ request.setEntity(new
StringEntity(getValidatedBundleJSON("personalization-no-control-group.json",
null), ContentType.APPLICATION_JSON));
+ }
+
+ TestUtils.RequestResponse response =
TestUtils.executeContextJSONRequest(request);
+ ContextResponse contextResponse = response.getContextResponse();
+
+ // Check variants
+ List<String> variants =
contextResponse.getPersonalizations().get("perso-control-group");
+ assertEquals("Invalid response code", 200, response.getStatusCode());
+ assertEquals("Perso should contains the good number of variants",
expectedVariants.size(), variants.size());
+ for (int i = 0; i < expectedVariants.size(); i++) {
+ assertEquals("Variant is not the expected one",
expectedVariants.get(i), variants.get(i));
+ }
+ PersonalizationResult personalizationResult =
contextResponse.getPersonalizationResults().get("perso-control-group");
+ variants = personalizationResult.getContentIds();
+ assertEquals("Perso should contains the good number of variants",
expectedVariants.size(), variants.size());
+ for (int i = 0; i < expectedVariants.size(); i++) {
+ assertEquals("Variant is not the expected one",
expectedVariants.get(i), variants.get(i));
+ }
+ // Check control group info
+ assertEquals("Perso result should contains control group info",
expectedControlGroupInfoInPersoResult,
personalizationResult.getAdditionalResultInfos().containsKey(PersonalizationResult.ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP));
+ assertEquals("Perso should not be in control group then",
expectedControlGroupValueInPersoResult,
contextResponse.getPersonalizationResults().get("perso-control-group").isInControlGroup());
+
+ // Check control group state on profile
+ keepTrying("Incorrect control group on profile",
+ () -> profileService.load(TEST_PROFILE_ID), storedProfile ->
expectedControlGroupValueInProfile ==
getPersistedControlGroupStatus(storedProfile, "perso-control-group"),
+ 1000, 10);
+
+ // Check control group state on session
+ keepTrying("Incorrect control group status on session",
+ () -> persistenceService.load(TEST_SESSION_ID, Session.class),
storedSession -> expectedControlGroupValueInSession ==
getPersistedControlGroupStatus(storedSession, "perso-control-group"),
+ 1000, 10);
+ }
+
+ private Boolean getPersistedControlGroupStatus(SystemPropertiesItem
systemPropertiesItem, String personalizationId) {
+ if(systemPropertiesItem.getSystemProperties() != null &&
systemPropertiesItem.getSystemProperties().containsKey("personalizationStrategyStatus"))
{
+ List<Map<String, Object>> personalizationStrategyStatus =
(List<Map<String, Object>>)
systemPropertiesItem.getSystemProperties().get("personalizationStrategyStatus");
+ for (Map<String, Object> strategyStatus :
personalizationStrategyStatus) {
+ if
(personalizationId.equals(strategyStatus.get("personalizationId"))) {
+ return strategyStatus.containsKey("inControlGroup") &&
((boolean) strategyStatus.get("inControlGroup"));
+ }
+ }
+ }
+ return null;
+ }
+
+ private Map<String, String> generateControlGroupConfig(String
storeInSession, String percentage) {
+ Map<String, String> controlGroupConfig = new HashMap<>();
+ controlGroupConfig.put("storeInSession", storeInSession);
+ controlGroupConfig.put("percentage", percentage);
+ return controlGroupConfig;
+ }
}
diff --git a/itests/src/test/resources/personalization-control-group.json
b/itests/src/test/resources/personalization-control-group.json
new file mode 100644
index 000000000..e7b07eb2b
--- /dev/null
+++ b/itests/src/test/resources/personalization-control-group.json
@@ -0,0 +1,49 @@
+{
+ "source": {
+ "itemId": "CMSServer",
+ "itemType": "custom",
+ "scope": "acme",
+ "properties": {}
+ },
+ "requireSegments": true,
+ "requiredProfileProperties": [
+ "*"
+ ],
+ "requiredSessionProperties": [
+ "*"
+ ],
+ "personalizations": [
+ {
+ "id": "perso-control-group",
+ "strategy": "matching-first",
+ "strategyOptions": {
+ "controlGroup": {
+ "storeInSession": ###storeInSession###,
+ "percentage": ###percentage###
+ }
+ },
+ "contents": [
+ {
+ "id": "first-name-missing",
+ "filters": [
+ {
+ "condition": {
+ "type": "profilePropertyCondition",
+ "parameterValues": {
+ "propertyName": "properties.firstName",
+ "comparisonOperator": "missing"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "id": "no-condition",
+ "filters": null
+ }
+ ]
+ }
+ ],
+ "sessionId": "dummy-session",
+ "profileId": "test-profile-id"
+}
\ No newline at end of file
diff --git a/itests/src/test/resources/personalization-controlgroup.json
b/itests/src/test/resources/personalization-controlgroup.json
deleted file mode 100644
index 8b8c74fbc..000000000
--- a/itests/src/test/resources/personalization-controlgroup.json
+++ /dev/null
@@ -1,1031 +0,0 @@
-{
- "source": {
- "itemId": "CMSServer",
- "itemType": "custom",
- "scope": "acme",
- "version": null,
- "properties": {}
- },
- "requireSegments": true,
- "requiredProfileProperties": [
- "unomiControlGroups"
- ],
- "requiredSessionProperties": [
- "unomiControlGroups"
- ],
- "events": null,
- "filters": null,
- "personalizations": [
- {
- "id": "perso1",
- "strategy": "score-sorted",
- "strategyOptions": {
- "threshold": -1,
- "controlGroup" : {
- "percentage" : 100.0,
- "displayName" : "First perso",
- "path" : "/home/perso1.html",
- "storeInSession" : ###storeInSession###
- }
- },
- "contents": [
- {
- "id": "perso1content1",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": {
- "interests": "health food"
- }
- },
- {
- "id": "perso1content2",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/contactus.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso1content3",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/documentation.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso1content4",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/aboutus.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso1content5",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/products.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso1content6",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/services.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso1content7",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/community.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso1content8",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/projects.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso1content9",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/home.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso1content10",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/theend.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- }
- ]
- },
- {
- "id": "perso2",
- "strategy": "score-sorted",
- "strategyOptions": {
- "threshold": -1
- },
- "contents": [
- {
- "id": "perso2content1",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": {
- "interests": "health food"
- }
- },
- {
- "id": "perso2content2",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/contactus.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso1content3",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/documentation.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso2content4",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/aboutus.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso2content5",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/products.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso2content6",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/services.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso2content7",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/community.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso2content8",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/projects.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso2content9",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/home.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- },
- {
- "id": "perso2content10",
- "filters": [
- {
- "appliesOn": null,
- "condition": {
- "parameterValues": {
- "minimumEventCount": 1,
- "eventCondition": {
- "type": "booleanCondition",
- "parameterValues": {
- "operator": "and",
- "subConditions" : [
- {
- "type": "eventTypeCondition",
- "parameterValues": {
- "eventTypeId": "view"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.pagePath",
- "propertyValue": "/theend.html",
- "comparisonOperator": "equals"
- }
- },
- {
- "type": "eventPropertyCondition",
- "parameterValues": {
- "propertyName":
"target.properties.pageInfo.language",
- "propertyValue": "en",
- "comparisonOperator": "equals"
- }
- }
- ]
- }
- },
- "numberOfDays": 30
- },
- "type": "pastEventCondition"
- },
- "properties": {
- "score": -1000
- }
- }
- ],
- "properties": null
- }
- ]
- }
- ],
- "profileOverrides": null,
- "sessionPropertiesOverrides": null,
- "sessionId": "test-session-id"
-}
\ No newline at end of file
diff --git a/itests/src/test/resources/personalization-no-control-group.json
b/itests/src/test/resources/personalization-no-control-group.json
new file mode 100644
index 000000000..790dbe71b
--- /dev/null
+++ b/itests/src/test/resources/personalization-no-control-group.json
@@ -0,0 +1,45 @@
+{
+ "source": {
+ "itemId": "CMSServer",
+ "itemType": "custom",
+ "scope": "acme",
+ "properties": {}
+ },
+ "requireSegments": true,
+ "requiredProfileProperties": [
+ "*"
+ ],
+ "requiredSessionProperties": [
+ "*"
+ ],
+ "personalizations": [
+ {
+ "id": "perso-control-group",
+ "strategy": "matching-first",
+ "strategyOptions": {
+ },
+ "contents": [
+ {
+ "id": "first-name-missing",
+ "filters": [
+ {
+ "condition": {
+ "type": "profilePropertyCondition",
+ "parameterValues": {
+ "propertyName": "properties.firstName",
+ "comparisonOperator": "missing"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "id": "no-condition",
+ "filters": null
+ }
+ ]
+ }
+ ],
+ "sessionId": "dummy-session",
+ "profileId": "test-profile-id"
+}
\ No newline at end of file
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 c64275fc3..5fa8b8685 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
@@ -375,12 +375,11 @@ public class ContextJsonEndpoint {
List<PersonalizationService.PersonalizationRequest> personalizations =
contextRequest.getPersonalizations();
if (personalizations != null) {
- data.setPersonalizations(new HashMap<>());
+ data.setPersonalizationResults(new HashMap<>());
for (PersonalizationService.PersonalizationRequest personalization
: sanitizePersonalizations(personalizations)) {
PersonalizationResult personalizationResult =
personalizationService.personalizeList(profile, session, personalization);
changes.setChangeType(changes.getChangeType() |
personalizationResult.getChangeType());
- data.getPersonalizations()
- .put(personalization.getId(),
personalizationResult.getContentIds());
+ data.getPersonalizationResults().put(personalization.getId(),
personalizationResult);
}
}
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..494b8db66 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,26 @@ 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);
+ // merge original strategy result with previous controlGroup hook
in case it's needed
+ if (controlGroupStrategyResult != null) {
+
originalStrategyResult.addChanges(controlGroupStrategyResult.getChangeType());
+
originalStrategyResult.getAdditionalResultInfos().putAll(controlGroupStrategyResult.getAdditionalResultInfos());
}
- 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..3297b334c
--- /dev/null
+++
b/services/src/main/java/org/apache/unomi/services/sorts/ControlGroupPersonalizationStrategy.java
@@ -0,0 +1,117 @@
+/*
+ * 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) &&
+ controlGroupMap.get(CONTROL_GROUP_CONFIG_STORE_IN_SESSION)
instanceof Boolean &&
+ ((Boolean)
controlGroupMap.get(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 7e636d32f..90f85b6a6 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<>();
@@ -59,7 +60,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);
@@ -88,18 +89,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);
}
}