/* * Copyright 2015-2017 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed 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.hawkular.alerts.extensions; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.hawkular.alerts.api.model.condition.ExternalCondition; import org.hawkular.alerts.api.model.trigger.FullTrigger; import org.hawkular.alerts.api.model.trigger.Trigger; /** * Represent a DSL expression coming from an ExternalCondition which is parsed into a DRL format understandable * by the CEP engine. * * Expression syntax: * * <expression> ::= "event:groupBy(" <field> ")" [ ":window(" <window> ")" ] [ ":filter(" <filter> ] * [ ":having(" <having> ")" ] * <field> ::= [ "tag." | "context." ] <field name> * <window> ::= ( "time," <time_value> | "length," <numeric_value> ) * <time_value> ::= [ <numeric_value> "d" ][ <numeric_value> "h" ][ <numeric_value> "m" ][ <numeric_value> "s" ] * [ <numeric_value> [ "ms" ]] * <filter> ::= <drools_expression> * <having> ::= <drools_expression> * * @author Jay Shaughnessy * @author Lucas Ponce */ public class Expression { private static final String DRL_HEADER = " package org.hawkular.alerts.extension \n" + " import org.hawkular.alerts.api.model.event.Event; \n" + " import org.hawkular.alerts.api.json.JsonUtil; \n" + " import org.hawkular.alerts.extensions.CepEngine; \n" + " import org.kie.api.time.SessionClock; \n" + " import org.jboss.logging.Logger; \n" + " import java.util.List; \n" + " import java.util.UUID; \n\n" + " global Logger log; \n" + " global CepEngine results; \n" + " global SessionClock clock;\n" + " \n"; private static final String BLANK = " "; private static final String CONTEXT = "context"; private static final String DEFAULT_EXPIRATION = "30m"; private static final String FUNCTION_COUNT = "$count : count( $event )"; private static final String FUNCTION_EVENTS = "$events : collectList( $event )"; private static final int GROUP_INDEX = 1; private static final Pattern SEARCH_CONTEXT = Pattern.compile("context\\.(\\w+)\\s"); private static final Pattern SEARCH_TAGS = Pattern.compile("tags\\.(\\w+)\\s"); private static final String TAGS = "tags"; private static final String TOKEN_COMMA = ","; private static final String TOKEN_CONTEXT = CONTEXT + "."; private static final String TOKEN_COUNT = "count "; private static final String TOKEN_COUNT_CONTEXT = "count.context."; private static final String TOKEN_COUNT_TAGS = "count.tags."; private static final int TOKEN_END_PARENTHESIS = ')'; private static final String TOKEN_EVENT = "event"; private static final String TOKEN_FILTER = "filter("; private static final String TOKEN_GROUP_BY = "groupBy("; private static final String TOKEN_HAVING = "having("; private static final String TOKEN_LENGTH = "length,"; private static final String TOKEN_SEPARATOR = ":"; private static final String TOKEN_TAGS = TAGS + "."; private static final String TOKEN_TIME = "time,"; private static final String TOKEN_WINDOW = "window("; private static final String VARIABLE_COUNT = "\\$count "; private String expRuleName; private String alerterId; private String expression; private String tenantId; private String source; private String dataId; private Set<String> declareFields = new HashSet<>(); private Set<String> ruleNames = new HashSet<>(); private String drlGroupByDeclare; private String drlGroupByObject; private String drlGroupByConstraint; private String drlGroupByResult; private String drlWindow; private Set<String> drlEventConstraints = new HashSet<>(); private Set<String> drlFunctions = new HashSet<>(); private Set<String> drlFunctionsConstraints = new HashSet<>(); private String drl; public Expression(Collection<FullTrigger> activeTriggers) { this(null, activeTriggers); } public Expression(String expiration, Collection<FullTrigger> activeTriggers) { if (isEmpty(expiration)) { expiration = DEFAULT_EXPIRATION; } if (isEmpty(activeTriggers)) { throw new IllegalArgumentException("ActiveTriggers must be not empty"); } drl = DRL_HEADER + "\n"; drl += " declare Event \n" + " @role( event ) \n" + " @expires( " + expiration + " ) \n" + " @timestamp( ctime ) \n" + " end \n\n"; activeTriggers.stream().forEach(fullTrigger -> { fullTrigger.getConditions().forEach(condition -> { if (condition instanceof ExternalCondition) { buildTriggerDrl(fullTrigger.getTrigger(), (ExternalCondition) condition); drl += "\n"; drlEventConstraints.clear(); drlFunctions.clear(); drlFunctionsConstraints.clear(); } }); }); } private void buildTriggerDrl(Trigger trigger, ExternalCondition condition) { if (trigger == null || condition == null) { throw new IllegalArgumentException("Trigger or Condition must be not null"); } expRuleName = trigger.getName() + "-" + condition.getConditionId(); alerterId = condition.getAlerterId(); expression = condition.getExpression(); tenantId = trigger.getTenantId(); source = trigger.getSource(); dataId = condition.getDataId(); drlWindow = ""; if (isEmpty(expression)) { throw new IllegalArgumentException("Expression must be not null"); } String[] section = expression.split(TOKEN_SEPARATOR); if (section.length < 2 || section.length > 5) { throw new IllegalArgumentException("Wrong sections for expression [" + expression + "]"); } if (!section[0].equals(TOKEN_EVENT)) { throw new IllegalArgumentException("Expression [" + expression + "] must start with 'event'"); } if (!section[1].startsWith(TOKEN_GROUP_BY)) { throw new IllegalArgumentException("Expression [" + expression + "] must contain a 'groupBy()' section"); } parseGroupBy(section[1]); drlFunctions.add(FUNCTION_EVENTS); for (int i = 2; i < section.length; i++) { if (section[i].startsWith(TOKEN_WINDOW)) { parseWindow(section[i]); } else if (section[i].startsWith(TOKEN_FILTER)) { parseFilter(section[i]); } else if (section[i].startsWith(TOKEN_HAVING)) { parseHaving(section[i]); } else { throw new IllegalArgumentException("Expression [" + expression + "] contains an invalid '" + section[i] + "' section"); } } if (!ruleNames.contains(expRuleName)) { ruleNames.add(expRuleName); addTriggerDrl(expRuleName); } } private void parseGroupBy(String section) { int endSection = section.lastIndexOf(TOKEN_END_PARENTHESIS); if (endSection == -1) { throw new IllegalArgumentException("Expression [" + section + " must contain a valid 'groupBy()'"); } String innerSection = section.substring(TOKEN_GROUP_BY.length(), endSection).trim(); boolean tags = false; boolean context = false; if (innerSection.startsWith(TOKEN_TAGS)) { tags = true; } if (innerSection.startsWith(TOKEN_CONTEXT)) { context = true; } String field; if (tags) { field = innerSection.substring(TOKEN_TAGS.length()); } else if (context) { field = innerSection.substring(TOKEN_CONTEXT.length()); } else { field = innerSection; } String type = makeType(field); drlGroupByObject = type + " ( $tenantId : tenantId == \"" + tenantId + "\"," + "$source : source == \"" + source + "\", " + "$dataId : dataId == \"" + dataId + "\", $" + field + " : " + field + " )"; if (tags) { drlGroupByConstraint = " tags[ \"" + field + "\" ] == $" + field + " "; } else if (context) { drlGroupByConstraint = " context[ \"" + field + "\" ] == $" + field + " "; } else { drlGroupByConstraint = " " + field + " == $" + field + " "; } drlGroupByResult = " result.addContext(\"" + field + "\", $" + field + "); \n"; drlEventConstraints.add(drlGroupByConstraint); if (declareFields.contains(field)) { drlGroupByDeclare = ""; } else { declareFields.add(field); drlGroupByDeclare = " declare " + type + " \n" + " tenantId : String \n" + " source : String \n" + " dataId : String \n" + " " + field + " : String \n" + " end \n\n"; } String extractRuleName = "Extract " + field + " from " + tenantId + "-" + source +"-" + dataId; if (!ruleNames.contains(extractRuleName)) { ruleNames.add(extractRuleName); drlGroupByDeclare += " rule \"" + extractRuleName + "\" \n" + " when \n" + " Event ( $tenantId : tenantId == \"" + tenantId + "\", \n" + " $dataSource : dataSource == \"" + source + "\", \n" + " $dataId : dataId == \"" + dataId + "\", \n" + " $" + field + " : "; if (tags) { drlGroupByDeclare += "tags[ \"" + field + "\" ] != null ) \n"; } else if (context) { drlGroupByDeclare += "context[ \"" + field + "\" ] != null ) \n"; } else { drlGroupByDeclare += field + " != null ) \n"; } drlGroupByDeclare += " not " + type + " ( tenantId == $tenantId, " + "source == $dataSource, dataId == $dataId, " + "" + field + " == $" + field + " ) \n" + " then \n" + " insert ( new " + type + " ( $tenantId, $dataSource, $dataId, $" + field + " ) ); \n" + " end \n\n"; } } private void parseWindow(String section) { int endSection = section.lastIndexOf(TOKEN_END_PARENTHESIS); if (endSection == -1) { throw new IllegalArgumentException("Expression [" + section + " must contain a valid 'window()'"); } String innerSection = section.substring(TOKEN_WINDOW.length(), endSection).trim(); if (innerSection.startsWith(TOKEN_TIME)) { drlWindow += " over window:time(" + innerSection.substring(TOKEN_TIME.length()) + ")"; } else if (innerSection.startsWith(TOKEN_LENGTH)) { drlWindow += " over window:length(" + innerSection.substring(TOKEN_LENGTH.length()) + ")"; } else { new IllegalArgumentException("Expresion [" + section + " must contain a valid 'time' or 'length' token"); } } private void parseFilter(String section) { int endSection = section.lastIndexOf(TOKEN_END_PARENTHESIS); if (endSection == -1) { throw new IllegalArgumentException("Expression [" + section + " must contain a valid 'filter()'"); } String innerSection = section.substring(TOKEN_FILTER.length(), endSection).trim(); String[] filterConstraints = innerSection.split(TOKEN_COMMA); for (int i = 0; i < filterConstraints.length; i++) { if (filterConstraints[i].contains(TOKEN_CONTEXT)) { filterConstraints[i] = replaceMap(filterConstraints[i], SEARCH_CONTEXT, CONTEXT); } if (filterConstraints[i].contains(TOKEN_TAGS)) { filterConstraints[i] = replaceMap(filterConstraints[i], SEARCH_TAGS, TAGS); } drlEventConstraints.add(filterConstraints[i]); } } private void parseHaving(String section) { int endSection = section.lastIndexOf(')'); if (endSection == -1) { throw new IllegalArgumentException("Expression [" + section + " must contain a valid 'having()'"); } String innerSection = section.substring(TOKEN_HAVING.length(), endSection).trim(); String[] havingConstraints = innerSection.split(TOKEN_COMMA); for (int i = 0; i < havingConstraints.length; i++) { if (havingConstraints[i].contains(TOKEN_COUNT)) { havingConstraints[i] = havingConstraints[i].replaceAll(TOKEN_COUNT, VARIABLE_COUNT); drlFunctions.add(FUNCTION_COUNT); } if (havingConstraints[i].contains(TOKEN_COUNT_CONTEXT)) { havingConstraints[i] = processCountContext(havingConstraints[i]); } if (havingConstraints[i].contains(TOKEN_COUNT_TAGS)) { havingConstraints[i] = processCountTags(havingConstraints[i]); } drlFunctionsConstraints.add(havingConstraints[i].trim()); } } private void addTriggerDrl(String name) { drl += drlGroupByDeclare + " rule \"" + name + "\" \n" + " when \n " + " " + drlGroupByObject + " \n" + " accumulate( $event : Event( tenantId == $tenantId, \n" + " dataSource == $source, \n" + " dataId == $dataId, \n"; Iterator<String> it = drlEventConstraints.iterator(); while (it.hasNext()) { String eventConstraint = it.next(); drl += BLANK + BLANK + eventConstraint; if (it.hasNext()) { drl += ", \n"; } } drl += ") " + drlWindow + "; \n"; it = drlFunctions.iterator(); while (it.hasNext()) { drl += BLANK + it.next(); if (it.hasNext()) { drl += ", \n"; } } drl += "; \n"; it = drlFunctionsConstraints.iterator(); while (it.hasNext()) { drl += BLANK + it.next(); if (it.hasNext()) { drl += ", \n"; } } drl += ") \n"; drl += " then \n" + " Event result = new Event(\"" + tenantId + "\", \n" + " UUID.randomUUID().toString(), \n" + " \"" + dataId +"\", \n" + " \"" + alerterId + "\", \n" + " \"" + expression.replaceAll("\"", "'") + "\"); \n" + " result.addContext(\"events\", JsonUtil.toJson($events)); \n" + " result.addContext(\"processed\", \"true\"); \n" + drlGroupByResult + " results.sendResult( result ); \n" + " end \n"; } public String getDrl() { return drl; } private String processCountContext(String str) { int start = str.indexOf(TOKEN_COUNT_CONTEXT); int end = str.indexOf(' ', start); String countContext = str.substring(start, end); String field = countContext.substring(TOKEN_COUNT_CONTEXT.length()); drlFunctions.add("$" + field + "ContextSet : collectSet($event.getContext().get(\"" + field + "\") )"); return str.replaceAll(countContext, "\\$" + field + "ContextSet.size"); } private String processCountTags(String str) { int start = str.indexOf(TOKEN_COUNT_TAGS); int end = str.indexOf(' ', start); String countTags = str.substring(start, end); String field = countTags.substring(TOKEN_COUNT_TAGS.length()); drlFunctions.add("$" + field + "TagsSet : collectSet($event.getTags().get(\"" + field + "\") )"); return str.replaceAll(countTags, "\\$" + field + "TagsSet.size"); } private static String makeType(String field) { return field.substring(0, 1).toUpperCase() + field.substring(1); } private static String replaceMap(String str, Pattern pattern, String map) { String newStr = str; Matcher matcher = pattern.matcher(str); int index = 0; while (matcher.find(index)) { int end = matcher.end(); String original = matcher.group(); String field = matcher.group(GROUP_INDEX); index = end; newStr = newStr.replaceAll(original, map + "[\"" + field + "\"]"); } return newStr; } private static boolean isEmpty(String s) { return null == s || s.trim().isEmpty(); } private static boolean isEmpty(Collection c) { return null == c || c.isEmpty(); } @Override public String toString() { return getDrl(); } }