blob: f61db79bf3ef9ac17320b5e1d2fd9ad5fa776dcb [file] [log] [blame]
/*
* 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.goals;
import org.apache.unomi.api.Metadata;
import org.apache.unomi.api.PartialList;
import org.apache.unomi.api.Profile;
import org.apache.unomi.api.Session;
import org.apache.unomi.api.actions.Action;
import org.apache.unomi.api.campaigns.Campaign;
import org.apache.unomi.api.campaigns.CampaignDetail;
import org.apache.unomi.api.campaigns.events.CampaignEvent;
import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.api.goals.Goal;
import org.apache.unomi.api.goals.GoalReport;
import org.apache.unomi.api.query.AggregateQuery;
import org.apache.unomi.api.query.Query;
import org.apache.unomi.api.rules.Rule;
import org.apache.unomi.api.services.DefinitionsService;
import org.apache.unomi.api.services.GoalsService;
import org.apache.unomi.api.services.RulesService;
import org.apache.unomi.persistence.spi.CustomObjectMapper;
import org.apache.unomi.persistence.spi.PersistenceService;
import org.apache.unomi.persistence.spi.aggregate.*;
import org.apache.unomi.api.utils.ParserHelper;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.SynchronousBundleListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URL;
import java.util.*;
public class GoalsServiceImpl implements GoalsService, SynchronousBundleListener {
private static final Logger logger = LoggerFactory.getLogger(GoalsServiceImpl.class.getName());
private BundleContext bundleContext;
private PersistenceService persistenceService;
private DefinitionsService definitionsService;
private RulesService rulesService;
public void setBundleContext(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
public void setPersistenceService(PersistenceService persistenceService) {
this.persistenceService = persistenceService;
}
public void setDefinitionsService(DefinitionsService definitionsService) {
this.definitionsService = definitionsService;
}
public void setRulesService(RulesService rulesService) {
this.rulesService = rulesService;
}
public void postConstruct() {
logger.debug("postConstruct {" + bundleContext.getBundle() + "}");
loadPredefinedGoals(bundleContext);
loadPredefinedCampaigns(bundleContext);
for (Bundle bundle : bundleContext.getBundles()) {
if (bundle.getBundleContext() != null && bundle.getBundleId() != bundleContext.getBundle().getBundleId()) {
loadPredefinedGoals(bundle.getBundleContext());
loadPredefinedCampaigns(bundle.getBundleContext());
}
}
bundleContext.addBundleListener(this);
logger.info("Goal service initialized.");
}
public void preDestroy() {
bundleContext.removeBundleListener(this);
logger.info("Goal service shutdown.");
}
private void processBundleStartup(BundleContext bundleContext) {
if (bundleContext == null) {
return;
}
loadPredefinedGoals(bundleContext);
loadPredefinedCampaigns(bundleContext);
}
private void processBundleStop(BundleContext bundleContext) {
}
private void loadPredefinedGoals(BundleContext bundleContext) {
Enumeration<URL> predefinedRuleEntries = bundleContext.getBundle().findEntries("META-INF/cxs/goals", "*.json", true);
if (predefinedRuleEntries == null) {
return;
}
while (predefinedRuleEntries.hasMoreElements()) {
URL predefinedGoalURL = predefinedRuleEntries.nextElement();
logger.debug("Found predefined goals at " + predefinedGoalURL + ", loading... ");
try {
Goal goal = CustomObjectMapper.getObjectMapper().readValue(predefinedGoalURL, Goal.class);
if (goal.getMetadata().getScope() == null) {
goal.getMetadata().setScope("systemscope");
}
setGoal(goal);
logger.info("Predefined goal with id {} registered", goal.getMetadata().getId());
} catch (IOException e) {
logger.error("Error while loading segment definition " + predefinedGoalURL, e);
}
}
}
private void createRule(Goal goal, Condition event, String id, boolean testStart) {
Rule rule = new Rule(new Metadata(goal.getMetadata().getScope(), goal.getMetadata().getId() + id + "Event", "Auto generated rule for goal " + goal.getMetadata().getName(), ""));
Condition res = new Condition();
List<Condition> subConditions = new ArrayList<Condition>();
res.setConditionType(definitionsService.getConditionType("booleanCondition"));
res.setParameter("operator", "and");
res.setParameter("subConditions", subConditions);
subConditions.add(event);
Condition notExist = new Condition();
notExist.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
notExist.setParameter("propertyName", "systemProperties.goals." + goal.getMetadata().getId() + id + "Reached");
notExist.setParameter("comparisonOperator", "missing");
subConditions.add(notExist);
if (testStart) {
Condition startExists = new Condition();
startExists.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
startExists.setParameter("propertyName", "systemProperties.goals." + goal.getMetadata().getId() + "StartReached");
startExists.setParameter("comparisonOperator", "exists");
subConditions.add(startExists);
}
if (goal.getCampaignId() != null) {
Condition engagedInCampaign = new Condition();
engagedInCampaign.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
engagedInCampaign.setParameter("propertyName", "systemProperties.campaigns." + goal.getCampaignId() + "Engaged");
engagedInCampaign.setParameter("comparisonOperator", "exists");
subConditions.add(engagedInCampaign);
}
rule.setCondition(res);
rule.getMetadata().setHidden(true);
Action action1 = new Action();
action1.setActionType(definitionsService.getActionType("setPropertyAction"));
String name = "systemProperties.goals." + goal.getMetadata().getId() + id + "Reached";
action1.setParameter("setPropertyName", name);
action1.setParameter("setPropertyValue", "now");
action1.setParameter("storeInSession", true);
Action action2 = new Action();
action2.setActionType(definitionsService.getActionType("setPropertyAction"));
action2.setParameter("setPropertyName", name);
action2.setParameter("setPropertyValue", "script::profile.properties.?"+name+" != null ? (profile.properties."+name+") : 'now'");
action2.setParameter("storeInSession", false);
rule.setActions(Arrays.asList(action1, action2));
if (id.equals("Target")) {
Action action3 = new Action();
action3.setActionType(definitionsService.getActionType("sendEventAction"));
action3.setParameter("eventType", "goal");
action3.setParameter("eventTarget", goal);
action3.setParameter("eventProperties", new HashMap<String, Object>());
action3.setParameter("toBePersisted", false);
rule.setActions(Arrays.asList(action1,action2,action3));
}
rulesService.setRule(rule);
}
public Set<Metadata> getGoalMetadatas() {
Set<Metadata> descriptions = new HashSet<Metadata>();
for (Goal definition : persistenceService.getAllItems(Goal.class, 0, 50, null).getList()) {
descriptions.add(definition.getMetadata());
}
return descriptions;
}
public Set<Metadata> getGoalMetadatas(Query query) {
definitionsService.resolveConditionType(query.getCondition());
Set<Metadata> descriptions = new LinkedHashSet<>();
List<Goal> goals = persistenceService.query(query.getCondition(), query.getSortby(), Goal.class, query.getOffset(), query.getLimit()).getList();
for (Goal definition : goals) {
descriptions.add(definition.getMetadata());
}
return descriptions;
}
public Goal getGoal(String goalId) {
Goal goal = persistenceService.load(goalId, Goal.class);
if (goal != null) {
ParserHelper.resolveConditionType(definitionsService, goal.getStartEvent(), "goal "+goalId+" start event");
ParserHelper.resolveConditionType(definitionsService, goal.getTargetEvent(), "goal "+goalId+" target event");
}
return goal;
}
@Override
public void removeGoal(String goalId) {
persistenceService.remove(goalId, Goal.class);
rulesService.removeRule(goalId + "StartEvent");
rulesService.removeRule(goalId + "TargetEvent");
}
@Override
public void setGoal(Goal goal) {
if (goal == null) {
logger.warn("Trying to save null goal, aborting...");
return;
}
ParserHelper.resolveConditionType(definitionsService, goal.getStartEvent(), "goal "+goal.getItemId()+" start event");
ParserHelper.resolveConditionType(definitionsService, goal.getTargetEvent(), "goal "+goal.getItemId()+" start event");
if (goal.getMetadata().isEnabled()) {
if (goal.getStartEvent() != null) {
createRule(goal, goal.getStartEvent(), "Start", false);
}
if (goal.getTargetEvent() != null) {
createRule(goal, goal.getTargetEvent(), "Target", goal.getStartEvent() != null);
}
} else {
rulesService.removeRule(goal.getMetadata().getId() + "StartEvent");
rulesService.removeRule(goal.getMetadata().getId() + "TargetEvent");
}
persistenceService.save(goal);
}
public Set<Metadata> getCampaignGoalMetadatas(String campaignId) {
Set<Metadata> descriptions = new HashSet<Metadata>();
for (Goal definition : persistenceService.query("campaignId", campaignId, null, Goal.class,0,50).getList()) {
descriptions.add(definition.getMetadata());
}
return descriptions;
}
private void loadPredefinedCampaigns(BundleContext bundleContext) {
Enumeration<URL> predefinedRuleEntries = bundleContext.getBundle().findEntries("META-INF/cxs/campaigns", "*.json", true);
if (predefinedRuleEntries == null) {
return;
}
while (predefinedRuleEntries.hasMoreElements()) {
URL predefinedCampaignURL = predefinedRuleEntries.nextElement();
logger.debug("Found predefined campaigns at " + predefinedCampaignURL + ", loading... ");
try {
Campaign campaign = CustomObjectMapper.getObjectMapper().readValue(predefinedCampaignURL, Campaign.class);
setCampaign(campaign);
logger.info("Predefined campaign with id {} registered", campaign.getMetadata().getId());
} catch (IOException e) {
logger.error("Error while loading segment definition " + predefinedCampaignURL, e);
}
}
}
private void createRule(Campaign campaign, Condition event) {
Rule rule = new Rule(new Metadata(campaign.getMetadata().getScope(), campaign.getMetadata().getId() + "EntryEvent", "Auto generated rule for campaign " + campaign.getMetadata().getName(), ""));
Condition res = new Condition();
List<Condition> subConditions = new ArrayList<Condition>();
res.setConditionType(definitionsService.getConditionType("booleanCondition"));
res.setParameter("operator", "and");
res.setParameter("subConditions", subConditions);
if (campaign.getStartDate() != null) {
Condition startCondition = new Condition();
startCondition.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
startCondition.setParameter("propertyName", "timeStamp");
startCondition.setParameter("comparisonOperator", "greaterThan");
startCondition.setParameter("propertyValueDate", campaign.getStartDate());
subConditions.add(startCondition);
}
if (campaign.getEndDate() != null) {
Condition endCondition = new Condition();
endCondition.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
endCondition.setParameter("propertyName", "timeStamp");
endCondition.setParameter("comparisonOperator", "lessThan");
endCondition.setParameter("propertyValueDate", campaign.getEndDate());
subConditions.add(endCondition);
}
rule.setPriority(-5);
subConditions.add(event);
rule.setCondition(res);
rule.getMetadata().setHidden(true);
Action action1 = new Action();
action1.setActionType(definitionsService.getActionType("setPropertyAction"));
String name = "systemProperties.campaigns." + campaign.getMetadata().getId() + "Engaged";
action1.setParameter("setPropertyName", name);
action1.setParameter("setPropertyValue", "now");
action1.setParameter("storeInSession", true);
Action action2 = new Action();
action2.setActionType(definitionsService.getActionType("setPropertyAction"));
action2.setParameter("setPropertyName", name);
action2.setParameter("setPropertyValue", "script::profile.properties.?"+name+" != null ? (profile.properties."+name+") : 'now'");
action2.setParameter("storeInSession", false);
rule.setActions(Arrays.asList(action1,action2));
rulesService.setRule(rule);
}
public Set<Metadata> getCampaignMetadatas() {
Set<Metadata> descriptions = new HashSet<Metadata>();
for (Campaign definition : persistenceService.getAllItems(Campaign.class, 0, 50, null).getList()) {
descriptions.add(definition.getMetadata());
}
return descriptions;
}
public Set<Metadata> getCampaignMetadatas(Query query) {
definitionsService.resolveConditionType(query.getCondition());
Set<Metadata> descriptions = new HashSet<Metadata>();
for (Campaign definition : persistenceService.query(query.getCondition(), query.getSortby(), Campaign.class, query.getOffset(), query.getLimit()).getList()) {
descriptions.add(definition.getMetadata());
}
return descriptions;
}
public PartialList<CampaignDetail> getCampaignDetails(Query query) {
definitionsService.resolveConditionType(query.getCondition());
PartialList<Campaign> campaigns = persistenceService.query(query.getCondition(), query.getSortby(), Campaign.class, query.getOffset(), query.getLimit());
List<CampaignDetail> details = new LinkedList<>();
for (Campaign definition : campaigns.getList()) {
final CampaignDetail campaignDetail = getCampaignDetail(definition);
if (campaignDetail != null) {
details.add(campaignDetail);
}
}
return new PartialList<>(details, campaigns.getOffset(), campaigns.getPageSize(), campaigns.getTotalSize(), campaigns.getTotalSizeRelation());
}
public CampaignDetail getCampaignDetail(String id) {
return getCampaignDetail(getCampaign(id));
}
private CampaignDetail getCampaignDetail(Campaign campaign) {
if (campaign == null) {
return null;
}
CampaignDetail campaignDetail = new CampaignDetail(campaign);
// engaged profile
Condition profileEngagedCondition = new Condition(definitionsService.getConditionType("profilePropertyCondition"));
profileEngagedCondition.setParameter("propertyName", "systemProperties.campaigns." + campaign.getMetadata().getId() + "Engaged");
profileEngagedCondition.setParameter("comparisonOperator", "exists");
campaignDetail.setEngagedProfiles(persistenceService.queryCount(profileEngagedCondition, Profile.ITEM_TYPE));
// number of goals
Condition campaignGoalsCondition = new Condition(definitionsService.getConditionType("sessionPropertyCondition"));
campaignGoalsCondition.setParameter("propertyName", "campaignId");
campaignGoalsCondition.setParameter("comparisonOperator", "equals");
campaignGoalsCondition.setParameter("propertyValue", campaign.getMetadata().getId());
campaignDetail.setNumberOfGoals(persistenceService.queryCount(campaignGoalsCondition, Goal.ITEM_TYPE));
// sessions
Condition sessionEngagedCondition = new Condition(definitionsService.getConditionType("sessionPropertyCondition"));
sessionEngagedCondition.setParameter("propertyName", "systemProperties.campaigns." + campaign.getMetadata().getId() + "Engaged");
sessionEngagedCondition.setParameter("comparisonOperator", "exists");
campaignDetail.setCampaignSessionViews(persistenceService.queryCount(sessionEngagedCondition, Session.ITEM_TYPE));
// sessions
Condition sessionConvertedCondition = new Condition(definitionsService.getConditionType("sessionPropertyCondition"));
sessionConvertedCondition.setParameter("propertyName", "systemProperties.goals." + campaign.getPrimaryGoal() + "TargetReached");
sessionConvertedCondition.setParameter("comparisonOperator", "exists");
campaignDetail.setCampaignSessionSuccess(persistenceService.queryCount(sessionConvertedCondition, Session.ITEM_TYPE));
// conversion
campaignDetail.setConversionRate((double) campaignDetail.getCampaignSessionSuccess() / (campaignDetail.getCampaignSessionViews() > 0 ? (double) campaignDetail.getCampaignSessionViews() : 1));
return campaignDetail;
}
public Campaign getCampaign(String id) {
Campaign campaign = persistenceService.load(id, Campaign.class);
if (campaign != null) {
ParserHelper.resolveConditionType(definitionsService, campaign.getEntryCondition(), "campaign " + id);
}
return campaign;
}
public void removeCampaign(String id) {
for(Metadata m : getCampaignGoalMetadatas(id)) {
removeGoal(m.getId());
}
rulesService.removeRule(id + "EntryEvent");
persistenceService.remove(id, Campaign.class);
}
public void setCampaign(Campaign campaign) {
ParserHelper.resolveConditionType(definitionsService, campaign.getEntryCondition(), "campaign " + campaign.getItemId());
if(rulesService.getRule(campaign.getMetadata().getId() + "EntryEvent") != null) {
rulesService.removeRule(campaign.getMetadata().getId() + "EntryEvent");
}
if (campaign.getMetadata().isEnabled()) {
if (campaign.getEntryCondition() != null) {
createRule(campaign, campaign.getEntryCondition());
}
}
persistenceService.save(campaign);
}
public GoalReport getGoalReport(String goalId) {
return getGoalReport(goalId, null);
}
public GoalReport getGoalReport(String goalId, AggregateQuery query) {
Condition condition = new Condition(definitionsService.getConditionType("booleanCondition"));
final ArrayList<Condition> list = new ArrayList<Condition>();
condition.setParameter("operator", "and");
condition.setParameter("subConditions", list);
Goal g = getGoal(goalId);
Condition goalTargetCondition = new Condition(definitionsService.getConditionType("sessionPropertyCondition"));
goalTargetCondition.setParameter("propertyName", "systemProperties.goals." + goalId+ "TargetReached");
goalTargetCondition.setParameter("comparisonOperator", "exists");
Condition goalStartCondition;
if (g.getStartEvent() == null && g.getCampaignId() != null) {
goalStartCondition = new Condition(definitionsService.getConditionType("sessionPropertyCondition"));
goalStartCondition.setParameter("propertyName", "systemProperties.campaigns." + g.getCampaignId() + "Engaged");
goalStartCondition.setParameter("comparisonOperator", "exists");
} else if (g.getStartEvent() == null) {
goalStartCondition = new Condition(definitionsService.getConditionType("matchAllCondition"));
} else {
goalStartCondition = new Condition(definitionsService.getConditionType("sessionPropertyCondition"));
goalStartCondition.setParameter("propertyName", "systemProperties.goals." + goalId + "StartReached");
goalStartCondition.setParameter("comparisonOperator", "exists");
}
if (query != null && query.getCondition() != null) {
ParserHelper.resolveConditionType(definitionsService, query.getCondition(), "goal " + goalId + " report");
list.add(query.getCondition());
}
Map<String, Long> all;
Map<String, Long> match;
// resolve aggregate
BaseAggregate aggregate = null;
String property = query.getAggregate().getProperty();
if(query != null && query.getAggregate() != null && property != null) {
if (query.getAggregate().getType() != null){
// try to guess the aggregate type
if(query.getAggregate().getType().equals("date")) {
String interval = (String) query.getAggregate().getParameters().get("interval");
String format = (String) query.getAggregate().getParameters().get("format");
aggregate = new DateAggregate(property, interval, format);
} else if (query.getAggregate().getType().equals("dateRange") && query.getAggregate().getDateRanges() != null && query.getAggregate().getDateRanges().size() > 0) {
String format = (String) query.getAggregate().getParameters().get("format");
aggregate = new DateRangeAggregate(property, format, query.getAggregate().getDateRanges());
} else if (query.getAggregate().getType().equals("numericRange") && query.getAggregate().getNumericRanges() != null && query.getAggregate().getNumericRanges().size() > 0) {
aggregate = new NumericRangeAggregate(property, query.getAggregate().getNumericRanges());
} else if (query.getAggregate().getType().equals("ipRange") && query.getAggregate().ipRanges() != null && query.getAggregate().ipRanges().size() > 0) {
aggregate = new IpRangeAggregate(property, query.getAggregate().ipRanges());
}
}
if(aggregate == null){
aggregate = new TermsAggregate(property);
}
}
if (aggregate != null) {
list.add(goalStartCondition);
all = persistenceService.aggregateWithOptimizedQuery(condition, aggregate, Session.ITEM_TYPE);
list.remove(goalStartCondition);
list.add(goalTargetCondition);
match = persistenceService.aggregateWithOptimizedQuery(condition, aggregate, Session.ITEM_TYPE);
} else {
list.add(goalStartCondition);
all = new HashMap<String, Long>();
all.put("_filtered", persistenceService.queryCount(condition, Session.ITEM_TYPE));
list.remove(goalStartCondition);
list.add(goalTargetCondition);
match = new HashMap<String, Long>();
match.put("_filtered", persistenceService.queryCount(condition, Session.ITEM_TYPE));
}
GoalReport report = new GoalReport();
GoalReport.Stat stat = new GoalReport.Stat();
Long allFiltered = all.remove("_filtered");
Long matchFiltered = match.remove("_filtered");
stat.setStartCount(allFiltered != null ? allFiltered : 0);
stat.setTargetCount(matchFiltered != null ? matchFiltered : 0);
stat.setConversionRate(stat.getStartCount() > 0 ? (float) stat.getTargetCount() / (float) stat.getStartCount() : 0);
report.setGlobalStats(stat);
all.remove("_all");
report.setSplit(new LinkedList<GoalReport.Stat>());
for (Map.Entry<String, Long> entry : all.entrySet()) {
GoalReport.Stat dateStat = new GoalReport.Stat();
dateStat.setKey(entry.getKey());
dateStat.setStartCount(entry.getValue());
dateStat.setTargetCount(match.containsKey(entry.getKey()) ? match.get(entry.getKey()) : 0);
dateStat.setConversionRate(dateStat.getStartCount() > 0 ? (float) dateStat.getTargetCount() / (float) dateStat.getStartCount() : 0);
dateStat.setPercentage(stat.getTargetCount() > 0 ? (float) dateStat.getTargetCount() / (float) stat.getTargetCount() : 0);
report.getSplit().add(dateStat);
}
return report;
}
// Campaign Event management methods
@Override
public PartialList<CampaignEvent> getEvents(Query query) {
if(query.isForceRefresh()){
persistenceService.refresh();
}
definitionsService.resolveConditionType(query.getCondition());
return persistenceService.query(query.getCondition(), query.getSortby(), CampaignEvent.class, query.getOffset(), query.getLimit());
}
@Override
public void setCampaignEvent(CampaignEvent event) {
persistenceService.save(event);
}
@Override
public void removeCampaignEvent(String campaignEventId) {
persistenceService.remove(campaignEventId, CampaignEvent.class);
}
public void bundleChanged(BundleEvent event) {
switch (event.getType()) {
case BundleEvent.STARTED:
processBundleStartup(event.getBundle().getBundleContext());
break;
case BundleEvent.STOPPING:
processBundleStop(event.getBundle().getBundleContext());
break;
}
}
}