blob: a3cc0d0d537d6823a2405898e3974d2adb008aca [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.plugins.baseplugin.conditions;
import ognl.*;
import ognl.enhance.ExpressionAccessor;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.unomi.api.*;
import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.plugins.baseplugin.conditions.accessors.HardcodedPropertyAccessor;
import org.apache.unomi.scripting.ExpressionFilterFactory;
import org.apache.unomi.scripting.SecureFilteringClassLoader;
import org.apache.unomi.persistence.elasticsearch.conditions.ConditionContextHelper;
import org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluator;
import org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluatorDispatcher;
import org.apache.unomi.persistence.spi.PropertyHelper;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.joda.Joda;
import org.elasticsearch.common.joda.JodaDateMathParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Member;
import java.lang.reflect.Modifier;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Pattern;
/**
* Evaluator for property comparison conditions
*/
public class PropertyConditionEvaluator implements ConditionEvaluator {
private static final Logger logger = LoggerFactory.getLogger(PropertyConditionEvaluator.class.getName());
private static final SimpleDateFormat yearMonthDayDateFormat = new SimpleDateFormat("yyyyMMdd");
private final Map<String, Map<String, ExpressionAccessor>> expressionCache = new HashMap<>(64);
private boolean usePropertyConditionOptimizations = true;
private static final ClassLoader secureFilteringClassLoader = new SecureFilteringClassLoader(PropertyConditionEvaluator.class.getClassLoader());
private static final HardcodedPropertyAccessorRegistry hardcodedPropertyAccessorRegistry = new HardcodedPropertyAccessorRegistry();
private ExpressionFilterFactory expressionFilterFactory;
private final boolean useOGNLScripting = Boolean.parseBoolean(System.getProperty("org.apache.unomi.security.properties.useOGNLScripting", "false"));
public void setUsePropertyConditionOptimizations(boolean usePropertyConditionOptimizations) {
this.usePropertyConditionOptimizations = usePropertyConditionOptimizations;
}
public void setExpressionFilterFactory(ExpressionFilterFactory expressionFilterFactory) {
this.expressionFilterFactory = expressionFilterFactory;
}
private int compare(Object actualValue, String expectedValue, Object expectedValueDate, Object expectedValueInteger, Object expectedValueDateExpr, Object expectedValueDouble) {
if (expectedValue == null && expectedValueDate == null && expectedValueInteger == null && getDate(expectedValueDateExpr) == null) {
return actualValue == null ? 0 : 1;
} else if (actualValue == null) {
return -1;
}
if (expectedValueInteger != null) {
return PropertyHelper.getInteger(actualValue).compareTo(PropertyHelper.getInteger(expectedValueInteger));
} else if (expectedValueDouble != null) {
return PropertyHelper.getDouble(actualValue).compareTo(PropertyHelper.getDouble(expectedValueDouble));
} else if (expectedValueDate != null) {
return getDate(actualValue).compareTo(getDate(expectedValueDate));
} else if (expectedValueDateExpr != null) {
return getDate(actualValue).compareTo(getDate(expectedValueDateExpr));
} else {
return actualValue.toString().compareTo(expectedValue);
}
}
private boolean compareMultivalue(Object actualValue, List<?> expectedValues, List<?> expectedValuesDate, List<?> expectedValuesNumber, List<?> expectedValuesDateExpr, String op) {
@SuppressWarnings("unchecked")
List<?> expected = ObjectUtils.firstNonNull(expectedValues, expectedValuesDate, expectedValuesNumber);
if (actualValue == null) {
return expected == null;
} else if (expected == null) {
return false;
}
List<Object> actual = ConditionContextHelper.foldToASCII(getValueSet(actualValue));
boolean result = true;
switch (op) {
case "in":
result = false;
for (Object a : actual) {
if (expected.contains(a)) {
result = true;
break;
}
}
break;
case "inContains":
result = false;
for (Object a : actual) {
for (Object b : expected)
if (((String) a).contains((String) b)) {
result = true;
break;
}
}
break;
case "notIn":
for (Object a : actual) {
if (expected.contains(a)) {
result = false;
break;
}
}
break;
case "all":
for (Object e : expected) {
if (!actual.contains(e)) {
result = false;
break;
}
}
break;
case "hasNoneOf":
if (!Collections.disjoint(actual, expected)) {
return false;
}
break;
case "hasSomeOf":
if (Collections.disjoint(actual, expected)) {
return false;
}
break;
default:
throw new IllegalArgumentException("Unknown comparison operator " + op);
}
return result;
}
@Override
public boolean eval(Condition condition, Item item, Map<String, Object> context, ConditionEvaluatorDispatcher dispatcher) {
String op = (String) condition.getParameter("comparisonOperator");
String name = (String) condition.getParameter("propertyName");
String expectedValue = ConditionContextHelper.foldToASCII((String) condition.getParameter("propertyValue"));
Object expectedValueInteger = condition.getParameter("propertyValueInteger");
Object expectedValueDouble = condition.getParameter("propertyValueDouble");
Object expectedValueDate = condition.getParameter("propertyValueDate");
Object expectedValueDateExpr = condition.getParameter("propertyValueDateExpr");
Object actualValue;
if (item instanceof Event && "eventType".equals(name)) {
actualValue = ((Event) item).getEventType();
} else {
try {
long time = System.nanoTime();
//actualValue = beanUtilsBean.getPropertyUtils().getProperty(item, name);
actualValue = getPropertyValue(item, name);
time = System.nanoTime() - time;
if (time > 5000000L) {
logger.info("eval took {} ms for {} {}", time / 1000000L, item.getClass().getName(), name);
}
} catch (NullPointerException e) {
// property not found
actualValue = null;
} catch (Exception e) {
if (!(e instanceof OgnlException)
|| (!StringUtils.startsWith(e.getMessage(),
"source is null for getProperty(null"))) {
logger.warn("Error evaluating value for " + item.getClass().getName() + " " + name, e);
}
actualValue = null;
}
}
if (actualValue instanceof String) {
actualValue = ConditionContextHelper.foldToASCII((String) actualValue);
}
if (op == null) {
return false;
} else if (actualValue == null) {
return op.equals("missing");
} else if (op.equals("exists")) {
return true;
} else if (op.equals("equals")) {
if (actualValue instanceof Collection) {
for (Object o : ((Collection<?>) actualValue)) {
if (o instanceof String) {
o = ConditionContextHelper.foldToASCII((String) o);
}
if (compare(o, expectedValue, expectedValueDate, expectedValueInteger, expectedValueDateExpr, expectedValueDouble) == 0) {
return true;
}
}
return false;
}
return compare(actualValue, expectedValue, expectedValueDate, expectedValueInteger, expectedValueDateExpr, expectedValueDouble) == 0;
} else if (op.equals("notEquals")) {
return compare(actualValue, expectedValue, expectedValueDate, expectedValueInteger, expectedValueDateExpr, expectedValueDouble) != 0;
} else if (op.equals("greaterThan")) {
return compare(actualValue, expectedValue, expectedValueDate, expectedValueInteger, expectedValueDateExpr, expectedValueDouble) > 0;
} else if (op.equals("greaterThanOrEqualTo")) {
return compare(actualValue, expectedValue, expectedValueDate, expectedValueInteger, expectedValueDateExpr, expectedValueDouble) >= 0;
} else if (op.equals("lessThan")) {
return compare(actualValue, expectedValue, expectedValueDate, expectedValueInteger, expectedValueDateExpr, expectedValueDouble) < 0;
} else if (op.equals("lessThanOrEqualTo")) {
return compare(actualValue, expectedValue, expectedValueDate, expectedValueInteger, expectedValueDateExpr, expectedValueDouble) <= 0;
} else if (op.equals("between")) {
List<?> expectedValuesInteger = (List<?>) condition.getParameter("propertyValuesInteger");
List<?> expectedValuesDouble = (List<?>) condition.getParameter("propertyValuesDouble");
List<?> expectedValuesDate = (List<?>) condition.getParameter("propertyValuesDate");
List<?> expectedValuesDateExpr = (List<?>) condition.getParameter("propertyValuesDateExpr");
return compare(actualValue, null,
(expectedValuesDate != null && expectedValuesDate.size() >= 1) ? getDate(expectedValuesDate.get(0)) : null,
(expectedValuesInteger != null && expectedValuesInteger.size() >= 1) ? (Integer) expectedValuesInteger.get(0) : null,
(expectedValuesDateExpr != null && expectedValuesDateExpr.size() >= 1) ? (String) expectedValuesDateExpr.get(0) : null,
(expectedValuesDouble != null && expectedValuesDouble.size() >= 1) ? (String) expectedValuesDouble.get(0) : null) >= 0
&&
compare(actualValue, null,
(expectedValuesDate != null && expectedValuesDate.size() >= 2) ? getDate(expectedValuesDate.get(1)) : null,
(expectedValuesInteger != null && expectedValuesInteger.size() >= 2) ? (Integer) expectedValuesInteger.get(1) : null,
(expectedValuesDateExpr != null && expectedValuesDateExpr.size() >= 2) ? (String) expectedValuesDateExpr.get(1) : null,
(expectedValuesDouble != null && expectedValuesDouble.size() >= 2) ? (String) expectedValuesDouble.get(1) : null) <= 0;
} else if (op.equals("contains")) {
return actualValue.toString().contains(expectedValue);
} else if (op.equals("notContains")) {
return !actualValue.toString().contains(expectedValue);
} else if (op.equals("startsWith")) {
return actualValue.toString().startsWith(expectedValue);
} else if (op.equals("endsWith")) {
return actualValue.toString().endsWith(expectedValue);
} else if (op.equals("matchesRegex")) {
return expectedValue != null && Pattern.compile(expectedValue).matcher(actualValue.toString()).matches();
} else if (op.equals("in") || op.equals("inContains") || op.equals("notIn") || op.equals("hasSomeOf") || op.equals("hasNoneOf") || op.equals("all")) {
List<?> expectedValues = ConditionContextHelper.foldToASCII((List<?>) condition.getParameter("propertyValues"));
List<?> expectedValuesInteger = (List<?>) condition.getParameter("propertyValuesInteger");
List<?> expectedValuesDate = (List<?>) condition.getParameter("propertyValuesDate");
List<?> expectedValuesDateExpr = (List<?>) condition.getParameter("propertyValuesDateExpr");
return compareMultivalue(actualValue, expectedValues, expectedValuesDate, expectedValuesInteger, expectedValuesDateExpr, op);
} else if (op.equals("isDay") && expectedValueDate != null) {
return yearMonthDayDateFormat.format(getDate(actualValue)).equals(yearMonthDayDateFormat.format(getDate(expectedValueDate)));
} else if (op.equals("isNotDay") && expectedValueDate != null) {
return !yearMonthDayDateFormat.format(getDate(actualValue)).equals(yearMonthDayDateFormat.format(getDate(expectedValueDate)));
}
return false;
}
protected Object getPropertyValue(Item item, String expression) throws Exception {
if (usePropertyConditionOptimizations) {
Object result = getHardcodedPropertyValue(item, expression);
if (!HardcodedPropertyAccessor.PROPERTY_NOT_FOUND_MARKER.equals(result)) {
return result;
}
}
if (useOGNLScripting) {
return getOGNLPropertyValue(item, expression);
} else {
logger.warn("OGNL Off. Expression not evaluated on item {} : {}", item.getClass().getName(), expression);
return null;
}
}
protected Object getHardcodedPropertyValue(Item item, String expression) {
// the following are optimizations to avoid using the expressions that are slower. The main objective here is
// to avoid the most used expression that may also trigger calls to the Java Reflection API.
return hardcodedPropertyAccessorRegistry.getProperty(item, expression);
}
protected Object getOGNLPropertyValue(Item item, String expression) throws Exception {
if (expressionFilterFactory.getExpressionFilter("ognl").filter(expression) == null) {
logger.warn("Expression {} is not allowed !", expression);
return null;
}
OgnlContext ognlContext = getOgnlContext(secureFilteringClassLoader);
ExpressionAccessor accessor = getPropertyAccessor(item, expression, ognlContext, secureFilteringClassLoader);
if (accessor != null) {
try {
return accessor.get(ognlContext, item);
} catch (Throwable t) {
logger.error("Error evaluating expression {} on item {} : {}", expression, item.getClass().getName(), t);
return null;
}
}
return null;
}
private class ClassLoaderClassResolver extends DefaultClassResolver {
private ClassLoader classLoader;
public ClassLoaderClassResolver(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
protected Class toClassForName(String className) throws ClassNotFoundException {
return Class.forName(className, true, classLoader);
}
}
private OgnlContext getOgnlContext(ClassLoader classLoader) {
return (OgnlContext) Ognl.createDefaultContext(null, new MemberAccess() {
@Override
public Object setup(Map context, Object target, Member member, String propertyName) {
return null;
}
@Override
public void restore(Map context, Object target, Member member, String propertyName, Object state) {
}
@Override
public boolean isAccessible(Map context, Object target, Member member, String propertyName) {
int modifiers = member.getModifiers();
if (target instanceof Item) {
if ("getClass".equals(member.getName())) {
logger.warn("Target {} and member {} for property {} are not allowed by OGNL security filter", target, member, propertyName);
return false;
}
return Modifier.isPublic(modifiers);
}
logger.warn("Target {} and member {} for property {} are not allowed by OGNL security filter", target, member, propertyName);
return false;
}
}, new ClassLoaderClassResolver(classLoader),
null);
}
private ExpressionAccessor getPropertyAccessor(Item item, String expression, OgnlContext ognlContext, ClassLoader classLoader) throws Exception {
ExpressionAccessor accessor = null;
String clazz = item.getClass().getName();
Map<String, ExpressionAccessor> expressions = expressionCache.get(clazz);
if (expressions == null) {
expressions = new HashMap<>();
expressionCache.put(clazz, expressions);
} else {
accessor = expressions.get(expression);
}
if (accessor == null) {
long time = System.nanoTime();
Thread current = Thread.currentThread();
ClassLoader contextCL = current.getContextClassLoader();
try {
current.setContextClassLoader(classLoader);
Node node = Ognl.compileExpression(ognlContext, item, expression);
accessor = node.getAccessor();
} finally {
current.setContextClassLoader(contextCL);
}
if (accessor != null) {
expressions.put(expression, accessor);
} else {
logger.warn("Unable to compile expression for {} and {}", clazz, expression);
}
if (logger.isInfoEnabled()) {
time = System.nanoTime() - time;
logger.info("Expression compilation for item={} expression={} took {}", item.getClass().getName(), expression, time / 1000000L);
}
}
return accessor;
}
private Date getDate(Object value) {
if (value == null) {
return null;
}
if (value instanceof Date) {
return ((Date) value);
} else {
JodaDateMathParser parser = new JodaDateMathParser(Joda.forPattern("strictDateOptionalTime||epoch_millis"));
try {
return Date.from(parser.parse(value.toString(), System::currentTimeMillis));
} catch (ElasticsearchParseException e) {
logger.warn("unable to parse date " + value.toString(), e);
}
}
return null;
}
@SuppressWarnings("unchecked")
private List<Object> getValueSet(Object expectedValue) {
if (expectedValue instanceof List) {
return (List<Object>) expectedValue;
} else if (expectedValue instanceof Collection) {
return new ArrayList<Object>((Collection<?>) expectedValue);
} else {
return Collections.singletonList(expectedValue);
}
}
}