diff --git a/dotCMS/src/enterprise/java/com/dotcms/enterprise/rules/RulesAPIImpl.java b/dotCMS/src/enterprise/java/com/dotcms/enterprise/rules/RulesAPIImpl.java index a624c7f7ac50..2da253938042 100644 --- a/dotCMS/src/enterprise/java/com/dotcms/enterprise/rules/RulesAPIImpl.java +++ b/dotCMS/src/enterprise/java/com/dotcms/enterprise/rules/RulesAPIImpl.java @@ -70,6 +70,7 @@ import com.dotmarketing.portlets.rules.actionlet.PersonaActionlet; import com.dotmarketing.portlets.rules.actionlet.RuleActionlet; import com.dotmarketing.portlets.rules.actionlet.RuleActionletOSGIService; +import com.dotmarketing.portlets.rules.actionlet.RuleAnalyticsFireUserEventActionlet; import com.dotmarketing.portlets.rules.actionlet.SendRedirectActionlet; import com.dotmarketing.portlets.rules.actionlet.SetRequestAttributeActionlet; import com.dotmarketing.portlets.rules.actionlet.SetResponseHeaderActionlet; @@ -195,6 +196,7 @@ public class RulesAPIImpl implements RulesAPI { .add(VisitorTagsActionlet.class) .add(SendRedirectActionlet.class) .add(StopProcessingActionlet.class) + .add(RuleAnalyticsFireUserEventActionlet.class) .build(); public RulesAPIImpl() { diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/CustomerEventCollector.java b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/CustomerEventCollector.java new file mode 100644 index 000000000000..520e42603530 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/CustomerEventCollector.java @@ -0,0 +1,38 @@ +package com.dotcms.analytics.track.collectors; + +import com.dotcms.analytics.track.matchers.UserCustomDefinedRequestMatcher; +import com.dotmarketing.beans.Host; + +import java.util.HashMap; +import java.util.Objects; + +/** + * This event collector creator basically allows to send a message for a customer event. + * These events are fired by rest, wf and rules. + * @author jsanca + */ +public class CustomerEventCollector implements Collector { + @Override + public boolean test(final CollectorContextMap collectorContextMap) { + + return UserCustomDefinedRequestMatcher.USER_CUSTOM_EVENT_MATCHER_ID.equals(collectorContextMap.getRequestMatcher().getId()) ; // should compare with the id + } + + @Override + public CollectorPayloadBean collect(final CollectorContextMap collectorContextMap, + final CollectorPayloadBean collectorPayloadBean) { + final String uri = (String)collectorContextMap.get("uri"); + final String host = (String)collectorContextMap.get("host"); + final Host site = (Host) collectorContextMap.get("currentHost"); + final String language = (String)collectorContextMap.get("lang"); + collectorPayloadBean.put("url", uri); + collectorPayloadBean.put("host", host); + collectorPayloadBean.put("language", language); + collectorPayloadBean.put("site", null != site?site.getIdentifier():"unknown"); + final String eventType = (String)collectorContextMap.get("eventType") == null? + EventType.CUSTOM_USER_EVENT.getType():(String)collectorContextMap.get("eventType"); + collectorPayloadBean.put("event_type", eventType); + + return collectorPayloadBean; + } +} diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/EventType.java b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/EventType.java index 7035ed75feae..dfd453b0ec23 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/EventType.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/EventType.java @@ -4,6 +4,7 @@ public enum EventType { VANITY_REQUEST("VANITY_REQUEST"), FILE_REQUEST("FILE_REQUEST"), PAGE_REQUEST("PAGE_REQUEST"), + CUSTOM_USER_EVENT("CUSTOM_USER_EVENT"), URL_MAP("URL_MAP"); diff --git a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/WebEventsCollectorServiceFactory.java b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/WebEventsCollectorServiceFactory.java index ec9103110cfa..1b70eb83f244 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/WebEventsCollectorServiceFactory.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/track/collectors/WebEventsCollectorServiceFactory.java @@ -63,7 +63,8 @@ private static class WebEventsCollectorServiceImpl implements WebEventsCollector WebEventsCollectorServiceImpl () { addCollector(new BasicProfileCollector(), new FilesCollector(), new PagesCollector(), - new PageDetailCollector(), new SyncVanitiesCollector(), new AsyncVanitiesCollector()); + new PageDetailCollector(), new SyncVanitiesCollector(), new AsyncVanitiesCollector(), + new CustomerEventCollector()); } @Override diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/site/SiteResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/site/SiteResource.java index aad606422b19..caf32cdeb390 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/site/SiteResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/site/SiteResource.java @@ -884,6 +884,7 @@ private void copySitePropertiesFromForm(final SiteForm siteForm, final Host site if (UtilMethods.isSet(siteForm.getTagStorage())) { final Host tagStorageSite = + Try.of(() -> this.siteHelper.getSite(APILocator.systemUser(), siteForm.getTagStorage())).getOrNull(); if (null == tagStorageSite) { throw new IllegalArgumentException(String.format("Tag Storage Site '%s' was not found", siteForm.getTagStorage())); diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/rules/actionlet/RuleAnalyticsFireUserEventActionlet.java b/dotCMS/src/main/java/com/dotmarketing/portlets/rules/actionlet/RuleAnalyticsFireUserEventActionlet.java new file mode 100644 index 000000000000..9ddb5e134035 --- /dev/null +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/rules/actionlet/RuleAnalyticsFireUserEventActionlet.java @@ -0,0 +1,105 @@ +package com.dotmarketing.portlets.rules.actionlet; + +import com.dotcms.analytics.track.collectors.WebEventsCollectorService; +import com.dotcms.analytics.track.collectors.WebEventsCollectorServiceFactory; +import com.dotcms.analytics.track.matchers.UserCustomDefinedRequestMatcher; +import com.dotcms.util.DotPreconditions; +import com.dotmarketing.portlets.rules.RuleComponentInstance; +import com.dotmarketing.portlets.rules.model.ParameterModel; +import com.dotmarketing.portlets.rules.parameter.ParameterDefinition; +import com.dotmarketing.portlets.rules.parameter.display.TextInput; +import com.dotmarketing.portlets.rules.parameter.type.TextType; +import com.dotmarketing.util.UUIDUtil; +import com.dotmarketing.util.WebKeys; +import com.liferay.util.StringPool; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * This actionlet allows to fire an user event to the analytics backend. + * @author jsanca + */ +public class RuleAnalyticsFireUserEventActionlet extends RuleActionlet { + + public static final UserCustomDefinedRequestMatcher USER_CUSTOM_DEFINED_REQUEST_MATCHER = new UserCustomDefinedRequestMatcher(); + private final transient WebEventsCollectorService webEventsCollectorService; + + private static final long serialVersionUID = 1L; + public static final String RULE_EVENT_TYPE = "eventType"; + public static final String RULE_OBJECT_TYPE = "objectType"; + public static final String RULE_OBJECT_ID = "objectId"; + public static final String RULE_REQUEST_ID = "requestId"; + public static final String RULE_CONTENT = "CONTENT"; + public static final String RULE_ID = "id"; + public static final String RULE_OBJECT_CONTENT_TYPE_VAR_NAME = "object_content_type_var_name"; + public static final String RULE_OBJECT = "object"; + public static final String RULE_EVENT_TYPE1 = "event_type"; + + public RuleAnalyticsFireUserEventActionlet() { + this(WebEventsCollectorServiceFactory.getInstance().getWebEventsCollectorService()); + } + + public RuleAnalyticsFireUserEventActionlet(final WebEventsCollectorService webEventsCollectorService) { + super("api.system.ruleengine.actionlet.analytics_user_event", + new ParameterDefinition<>(0, RULE_EVENT_TYPE, new TextInput<>(new TextType().required())), + new ParameterDefinition<>(1, RULE_OBJECT_TYPE, new TextInput<>(new TextType())), + new ParameterDefinition<>(2, RULE_OBJECT_ID, new TextInput<>(new TextType())) + ); + + this.webEventsCollectorService = webEventsCollectorService; + } + + @Override + public Instance instanceFrom(Map parameters) { + return new Instance(parameters); + } + + @Override + public boolean evaluate(final HttpServletRequest request, final HttpServletResponse response, final Instance instance) { + + final String identifier = getRuleId(request); + + request.setAttribute(RULE_REQUEST_ID, Objects.nonNull(request.getAttribute(RULE_REQUEST_ID)) ? + request.getAttribute(RULE_REQUEST_ID) : UUIDUtil.uuid()); + final HashMap objectDetail = new HashMap<>(); + final Map userEventPayload = new HashMap<>(); + + userEventPayload.put(RULE_ID, Objects.nonNull(instance.objectId) ? instance.objectId : identifier); + + objectDetail.put(RULE_ID, identifier); + objectDetail.put(RULE_OBJECT_CONTENT_TYPE_VAR_NAME, Objects.nonNull(instance.objectType) ? instance.objectType : RULE_CONTENT); + userEventPayload.put(RULE_OBJECT, objectDetail); + userEventPayload.put(RULE_EVENT_TYPE1, instance.eventType); + webEventsCollectorService.fireCollectorsAndEmitEvent(request, response, USER_CUSTOM_DEFINED_REQUEST_MATCHER, userEventPayload); + + return true; + } + + private String getRuleId(final HttpServletRequest request) { + + return Optional.of(request.getAttribute(WebKeys.RULES_ENGINE_PARAM_CURRENT_RULE_ID)).orElseGet(()-> StringPool.UNKNOWN).toString(); + } + + public class Instance implements RuleComponentInstance { + + private final String eventType; + private final String objectType; + private final String objectId; + + public Instance(final Map parameters) { + + DotPreconditions.checkNotNull(parameters, "parameters can't be null"); + this.eventType = parameters.getOrDefault(RULE_EVENT_TYPE, new ParameterModel()).getValue(); + this.objectType = parameters.getOrDefault(RULE_OBJECT_TYPE, new ParameterModel()).getValue(); + this.objectId = parameters.getOrDefault(RULE_OBJECT_ID, new ParameterModel()).getValue(); + } + } + + +} diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/rules/business/RulesEngine.java b/dotCMS/src/main/java/com/dotmarketing/portlets/rules/business/RulesEngine.java index bf9672c1e5df..2a1fb378dd25 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/rules/business/RulesEngine.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/rules/business/RulesEngine.java @@ -148,9 +148,9 @@ public static void fireRules(final HttpServletRequest request, final HttpServlet * Triggers a specific category of Rules associated to the specified parent * object based on the requested resource. * - * @param req + * @param request * - The {@link HttpServletRequest} object. - * @param res + * @param response * - The {@link HttpServletResponse} object. * @param parent * - The object whose associated rules will be fired. For @@ -159,62 +159,65 @@ public static void fireRules(final HttpServletRequest request, final HttpServlet * - The category of the rules that will be fired: Every page, * every request, etc. */ - public static void fireRules(HttpServletRequest req, HttpServletResponse res, Ruleable parent, Rule.FireOn fireOn) { + public static void fireRules(final HttpServletRequest request, final HttpServletResponse response, + final Ruleable parent, final Rule.FireOn fireOn) { //Check for the proper license level, the rules engine is an enterprise feature only if ( LicenseUtil.getLevel() < LicenseLevel.STANDARD.level ) { return; } - if(res.isCommitted()) { + if(response.isCommitted()) { return; } - if (!UtilMethods.isSet(req)) { + if (!UtilMethods.isSet(request)) { throw new DotRuntimeException("ERROR: HttpServletRequest is null"); } // do not run rules in admin mode - PageMode mode= PageMode.get(req); + PageMode mode= PageMode.get(request); if(mode.isAdmin) { final boolean fireRulesFromParameter =Try.of(()->Boolean.valueOf - (req.getParameter("fireRules"))).getOrElse(false); + (request.getParameter("fireRules"))).getOrElse(false); final boolean fireRulesFromAttribute =Try.of(()-> Boolean.valueOf((Boolean) - req.getAttribute("fireRules"))).getOrElse(false); + request.getAttribute("fireRules"))).getOrElse(false); if(!fireRulesFromParameter && !fireRulesFromAttribute) { return; } } - final Set alreadyFiredRulesFor =req.getAttribute(DOT_RULES_FIRED_ALREADY)!=null?(Set)req.getAttribute(DOT_RULES_FIRED_ALREADY):new HashSet(); + final Set alreadyFiredRulesFor =request.getAttribute(DOT_RULES_FIRED_ALREADY)!=null?(Set)request.getAttribute(DOT_RULES_FIRED_ALREADY):new HashSet(); final String ruleRunKey = parent.getIdentifier() +"_"+ fireOn.name(); if(alreadyFiredRulesFor.contains(ruleRunKey)) { Logger.warn(RulesEngine.class, "we have already run the rules for:" + ruleRunKey); return; } alreadyFiredRulesFor.add(ruleRunKey); - req.setAttribute(DOT_RULES_FIRED_ALREADY,alreadyFiredRulesFor); + request.setAttribute(DOT_RULES_FIRED_ALREADY,alreadyFiredRulesFor); - if (SKIP_RULES_EXECUTION.equalsIgnoreCase(req.getParameter(WebKeys.RULES_ENGINE_PARAM)) - || SKIP_RULES_EXECUTION.equalsIgnoreCase(String.valueOf(req.getParameter(WebKeys.RULES_ENGINE_PARAM)))) { + if (SKIP_RULES_EXECUTION.equalsIgnoreCase(request.getParameter(WebKeys.RULES_ENGINE_PARAM)) + || SKIP_RULES_EXECUTION.equalsIgnoreCase(String.valueOf(request.getParameter(WebKeys.RULES_ENGINE_PARAM)))) { return; } if (!UtilMethods.isSet(parent)) { return; } - User systemUser = APILocator.systemUser(); + final User systemUser = APILocator.systemUser(); try { - Set rules = APILocator.getRulesAPI().getRulesByParentFireOn(parent.getIdentifier(), systemUser, false, + final Set rules = APILocator.getRulesAPI().getRulesByParentFireOn(parent.getIdentifier(), systemUser, false, fireOn); - for (Rule rule : rules) { + for (final Rule rule : rules) { try { + + request.setAttribute(WebKeys.RULES_ENGINE_PARAM_CURRENT_RULE_ID, rule.getId()); long before = System.currentTimeMillis(); rule.checkValid(); // @todo ggranum: this should actually be done on writing to the DB, or at worst reading from. - boolean evaled = rule.evaluate(req, res); + boolean evaled = rule.evaluate(request, response); - if(res.isCommitted()) { + if(response.isCommitted()) { return; } if (evaled) { @@ -228,7 +231,7 @@ public static void fireRules(HttpServletRequest req, HttpServletResponse res, Ru rCopy.setShortCircuit(rule.isShortCircuit()); rCopy.setModDate(rule.getModDate()); - trackFiredRule(rCopy, req); + trackFiredRule(rCopy, request); } long after = System.currentTimeMillis(); if((after - before) > SLOW_RULE_LOG_MIN) { diff --git a/dotCMS/src/main/java/com/dotmarketing/util/WebKeys.java b/dotCMS/src/main/java/com/dotmarketing/util/WebKeys.java index 328f4e687340..d0c502a30397 100644 --- a/dotCMS/src/main/java/com/dotmarketing/util/WebKeys.java +++ b/dotCMS/src/main/java/com/dotmarketing/util/WebKeys.java @@ -361,6 +361,8 @@ public static enum WorkflowStatuses { OPEN, RESOLVED, CANCELLED }; public static final String RULES_ACTIONLET_CLASSES = "RULES_ACTIONLET_CLASSES"; public static final String RULES_CONDITIONLET_VISITEDURLS = "RULES_CONDITIONLET_VISITEDURLS"; public static final String RULES_ENGINE_PARAM = "dotRules"; + // stores the rule id that is currently being evaluated + public static final String RULES_ENGINE_PARAM_CURRENT_RULE_ID = "dotCurrentRuleId"; public static final String RULES_ENGINE_FIRE_LIST = "dotRulesFired"; //ADMIN CONTROL diff --git a/dotCMS/src/main/java/com/liferay/util/StringPool.java b/dotCMS/src/main/java/com/liferay/util/StringPool.java index 478ef31f3dc6..7aae9e980f25 100644 --- a/dotCMS/src/main/java/com/liferay/util/StringPool.java +++ b/dotCMS/src/main/java/com/liferay/util/StringPool.java @@ -91,4 +91,7 @@ public class StringPool { public static final String FALSE = Boolean.FALSE.toString(); + public static final String UNKNOWN = "UNKNOWN"; + + } diff --git a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties index 20d5f8994b9d..7482843066bd 100644 --- a/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties +++ b/dotCMS/src/main/webapp/WEB-INF/messages/Language.properties @@ -275,6 +275,8 @@ api.sites.ruleengine.rules.inputs.name.placeholder=Describe the Rule api.sites.ruleengine.rules.inputs.onOff.off.label=Off api.sites.ruleengine.rules.inputs.onOff.on.label=On api.sites.ruleengine.rules.inputs.onOff.tip=Enable this rule +api.system.ruleengine.actionlet.analytics_user_event.name=Fire Analytics Event + api.system.ruleengine.actionlet.send_redirect.input.url=Redirect URL api.system.ruleengine.actionlet.StopProcessingActionlet.name=Stop Processing api.system.ruleengine.actionlet.StopProcessingActionlet.inputs.return-code.placeholder=200 OK diff --git a/dotCMS/src/test/java/com/dotmarketing/portlets/rules/actionlet/RuleAnalyticsFireUserEventActionletTest.java b/dotCMS/src/test/java/com/dotmarketing/portlets/rules/actionlet/RuleAnalyticsFireUserEventActionletTest.java new file mode 100644 index 000000000000..8cf5521fbb26 --- /dev/null +++ b/dotCMS/src/test/java/com/dotmarketing/portlets/rules/actionlet/RuleAnalyticsFireUserEventActionletTest.java @@ -0,0 +1,81 @@ +package com.dotmarketing.portlets.rules.actionlet; + +import com.dotcms.UnitTestBase; +import com.dotcms.analytics.track.collectors.Collector; +import com.dotcms.analytics.track.collectors.WebEventsCollectorService; +import com.dotcms.analytics.track.matchers.RequestMatcher; +import com.dotmarketing.portlets.rules.model.ParameterModel; +import com.dotmarketing.util.WebKeys; +import org.junit.Assert; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * This actionlet allows to fire an user event rule to the analytics backend. + * @author jsanca + */ +public class RuleAnalyticsFireUserEventActionletTest extends UnitTestBase { + + /** + * * Method to test: RuleAnalyticsFireUserEventActionlet.evaluate + * * Given Scenario: Creates the context and send the information to the rule + * * ExpectedResult: The Payload created has to have the expected values based on the inputs + * @throws Exception + */ + @Test + public void testActionletSetsFireUserEventOnHappyPath() throws Exception { + + final WebEventsCollectorService webEventsCollectorService = new WebEventsCollectorService() { + @Override + public void fireCollectors(HttpServletRequest request, HttpServletResponse response, RequestMatcher requestMatcher) { + + } + + @Override + public void addCollector(Collector... collectors) { + + } + + @Override + public void removeCollector(String collectorId) { + + } + + @Override + public void fireCollectorsAndEmitEvent(HttpServletRequest request, HttpServletResponse response, + RequestMatcher requestMatcher, Map userEventPayload) { + + Assert.assertNotNull(userEventPayload); + Assert.assertEquals("page", userEventPayload.get("event_type")); + Assert.assertEquals("345", userEventPayload.get("id")); + + final Map object = (Map) userEventPayload.get("object"); + Assert.assertNotNull(object); + Assert.assertEquals("123", object.get("id")); + Assert.assertEquals("CONTENT", object.get("object_content_type_var_name")); + } + }; + + final RuleAnalyticsFireUserEventActionlet analyticsFireUserEventActionlet = new RuleAnalyticsFireUserEventActionlet(webEventsCollectorService); + final Map params = new HashMap<>(); + params.put(RuleAnalyticsFireUserEventActionlet.RULE_EVENT_TYPE, new ParameterModel(RuleAnalyticsFireUserEventActionlet.RULE_EVENT_TYPE, "page")); + params.put(RuleAnalyticsFireUserEventActionlet.RULE_OBJECT_TYPE, new ParameterModel()); + params.put(RuleAnalyticsFireUserEventActionlet.RULE_OBJECT_ID, new ParameterModel(RuleAnalyticsFireUserEventActionlet.RULE_EVENT_TYPE, "345")); + + final HttpServletRequest request = mock(HttpServletRequest.class); + final HttpServletResponse response = mock(HttpServletResponse.class); + when(request.getAttribute(WebKeys.RULES_ENGINE_PARAM_CURRENT_RULE_ID)).thenReturn("123"); + final RuleAnalyticsFireUserEventActionlet.Instance instance = analyticsFireUserEventActionlet.instanceFrom(params); + + analyticsFireUserEventActionlet.evaluate(request, response, instance); + } + +}