com.espertech.esper.epl.expression.ExprNodeUtility.java Source code

Java tutorial

Introduction

Here is the source code for com.espertech.esper.epl.expression.ExprNodeUtility.java

Source

/**************************************************************************************
 * Copyright (C) 2008 EsperTech, Inc. All rights reserved.                            *
 * http://esper.codehaus.org                                                          *
 * http://www.espertech.com                                                           *
 * ---------------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the GPL license       *
 * a copy of which has been included with this distribution in the license.txt file.  *
 **************************************************************************************/
package com.espertech.esper.epl.expression;

import com.espertech.esper.client.EventBean;
import com.espertech.esper.client.EventType;
import com.espertech.esper.client.hook.AggregationFunctionFactory;
import com.espertech.esper.client.hook.EPLMethodInvocationContext;
import com.espertech.esper.collection.Pair;
import com.espertech.esper.core.context.util.ContextPropertyRegistry;
import com.espertech.esper.core.service.ExprEvaluatorContextStatement;
import com.espertech.esper.core.service.StatementContext;
import com.espertech.esper.epl.agg.service.AggregationSupport;
import com.espertech.esper.epl.core.*;
import com.espertech.esper.epl.declexpr.ExprDeclaredNode;
import com.espertech.esper.epl.enummethod.dot.ExprDeclaredOrLambdaNode;
import com.espertech.esper.epl.enummethod.dot.ExprLambdaGoesNode;
import com.espertech.esper.epl.spec.OnTriggerSetAssignment;
import com.espertech.esper.event.EventAdapterService;
import com.espertech.esper.event.EventBeanUtility;
import com.espertech.esper.schedule.ScheduleParameterException;
import com.espertech.esper.schedule.ScheduleSpec;
import com.espertech.esper.schedule.ScheduleSpecUtil;
import com.espertech.esper.util.CollectionUtil;
import com.espertech.esper.util.JavaClassHelper;
import net.sf.cglib.reflect.FastClass;
import net.sf.cglib.reflect.FastMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.*;

public class ExprNodeUtility {

    public static final ExprNode[] EMPTY_EXPR_ARRAY = new ExprNode[0];
    public static final ExprDeclaredNode[] EMPTY_DECLARED_ARR = new ExprDeclaredNode[0];

    public static Pair<String, ExprNode> checkGetAssignmentToProp(ExprNode node) {
        if (!(node instanceof ExprEqualsNode)) {
            return null;
        }
        ExprEqualsNode equals = (ExprEqualsNode) node;
        if (!(equals.getChildNodes()[0] instanceof ExprIdentNode)) {
            return null;
        }
        ExprIdentNode identNode = (ExprIdentNode) equals.getChildNodes()[0];
        return new Pair<String, ExprNode>(identNode.getFullUnresolvedName(), equals.getChildNodes()[1]);
    }

    public static Pair<String, ExprNode> checkGetAssignmentToVariableOrProp(ExprNode node) {
        Pair<String, ExprNode> prop = checkGetAssignmentToProp(node);
        if (prop != null) {
            return prop;
        }
        if (!(node instanceof ExprEqualsNode)) {
            return null;
        }
        ExprEqualsNode equals = (ExprEqualsNode) node;
        if (!(equals.getChildNodes()[0] instanceof ExprVariableNode)) {
            return null;
        }
        ExprVariableNode variableNode = (ExprVariableNode) equals.getChildNodes()[0];
        return new Pair<String, ExprNode>(variableNode.getVariableNameWithSubProp(), equals.getChildNodes()[1]);
    }

    public static void applyFilterExpressionsIterable(Iterable<EventBean> iterable,
            List<ExprNode> filterExpressions, ExprEvaluatorContext exprEvaluatorContext,
            Collection<EventBean> eventsInWindow) {
        ExprEvaluator[] evaluators = ExprNodeUtility.getEvaluators(filterExpressions);
        EventBean[] events = new EventBean[1];
        for (EventBean theEvent : iterable) {
            events[0] = theEvent;
            boolean add = true;
            for (ExprEvaluator filter : evaluators) {
                Object result = filter.evaluate(events, true, exprEvaluatorContext);
                if ((result == null) || (!((Boolean) result))) {
                    add = false;
                    break;
                }
            }
            if (add) {
                eventsInWindow.add(events[0]);
            }
        }
    }

    public static void applyFilterExpressionIterable(Iterable<EventBean> iterable, ExprEvaluator filterExpression,
            ExprEvaluatorContext exprEvaluatorContext, Collection<EventBean> eventsInWindow) {
        EventBean[] events = new EventBean[1];
        for (EventBean theEvent : iterable) {
            events[0] = theEvent;
            Object result = filterExpression.evaluate(events, true, exprEvaluatorContext);
            if ((result == null) || (!((Boolean) result))) {
                continue;
            }
            eventsInWindow.add(events[0]);
        }
    }

    public static ExprAndNode connectExpressionsByLogicalAnd(List<ExprNode> nodes) {
        if (nodes.size() < 2) {
            throw new IllegalArgumentException("Invalid empty or 1-element list of nodes");
        }
        ExprAndNode andNode = new ExprAndNodeImpl();
        for (ExprNode node : nodes) {
            andNode.addChildNode(node);
        }
        return andNode;
    }

    /**
     * Walk expression returning properties used.
     * @param exprNode to walk
     * @param visitAggregateNodes true to visit aggregation nodes
     * @return list of props
     */
    public static List<Pair<Integer, String>> getExpressionProperties(ExprNode exprNode,
            boolean visitAggregateNodes) {
        ExprNodeIdentifierVisitor visitor = new ExprNodeIdentifierVisitor(visitAggregateNodes);
        exprNode.accept(visitor);
        return visitor.getExprProperties();
    }

    public static boolean isConstantValueExpr(ExprNode exprNode) {
        if (!(exprNode instanceof ExprConstantNode)) {
            return false;
        }
        ExprConstantNode constantNode = (ExprConstantNode) exprNode;
        return constantNode.isConstantValue();
    }

    /**
     * Validates the expression node subtree that has this
     * node as root. Some of the nodes of the tree, including the
     * root, might be replaced in the process.
     * @throws com.espertech.esper.epl.expression.ExprValidationException when the validation fails
     * @return the root node of the validated subtree, possibly
     *         different than the root node of the unvalidated subtree
     */
    public static ExprNode getValidatedSubtree(ExprNode exprNode, ExprValidationContext validationContext)
            throws ExprValidationException {
        if (exprNode instanceof ExprLambdaGoesNode) {
            return exprNode;
        }

        return getValidatedSubtreeInternal(exprNode, validationContext, true);
    }

    public static ExprNode getValidatedAssignment(OnTriggerSetAssignment assignment,
            ExprValidationContext validationContext) throws ExprValidationException {
        Pair<String, ExprNode> strictAssignment = checkGetAssignmentToVariableOrProp(assignment.getExpression());
        if (strictAssignment != null) {
            ExprNode validatedRightSide = getValidatedSubtreeInternal(strictAssignment.getSecond(),
                    validationContext, true);
            assignment.getExpression().setChildNode(1, validatedRightSide);
            return assignment.getExpression();
        } else {
            return getValidatedSubtreeInternal(assignment.getExpression(), validationContext, true);
        }
    }

    private static ExprNode getValidatedSubtreeInternal(ExprNode exprNode, ExprValidationContext validationContext,
            boolean isTopLevel) throws ExprValidationException {
        ExprNode result = exprNode;
        if (exprNode instanceof ExprLambdaGoesNode) {
            return exprNode;
        }

        for (int i = 0; i < exprNode.getChildNodes().length; i++) {
            ExprNode childNode = exprNode.getChildNodes()[i];
            if (childNode instanceof ExprDeclaredOrLambdaNode) {
                ExprDeclaredOrLambdaNode node = (ExprDeclaredOrLambdaNode) childNode;
                if (node.validated()) {
                    continue;
                }
            }
            ExprNode childNodeValidated = getValidatedSubtreeInternal(childNode, validationContext, false);
            exprNode.setChildNode(i, childNodeValidated);
        }

        try {
            exprNode.validate(validationContext);
        } catch (ExprValidationException e) {
            if (exprNode instanceof ExprIdentNode) {
                ExprIdentNode identNode = (ExprIdentNode) exprNode;
                try {
                    result = resolveStaticMethodOrField(identNode, e, validationContext);
                } catch (ExprValidationException ex) {
                    e = ex;
                    result = resolveAsStreamName(identNode, e, validationContext);
                }
            } else {
                throw e;
            }
        }

        // For top-level expressions check if we perform audit
        if (isTopLevel) {
            if (validationContext.isExpressionAudit()) {
                return (ExprNode) ExprNodeProxy.newInstance(
                        validationContext.getStreamTypeService().getEngineURIQualifier(),
                        validationContext.getStatementName(), result);
            }
        } else {
            if (validationContext.isExpressionNestedAudit() && !(result instanceof ExprIdentNode)
                    && !(ExprNodeUtility.isConstantValueExpr(result))) {
                return (ExprNode) ExprNodeProxy.newInstance(
                        validationContext.getStreamTypeService().getEngineURIQualifier(),
                        validationContext.getStatementName(), result);
            }
        }

        return result;
    }

    private static ExprNode resolveAsStreamName(ExprIdentNode identNode, ExprValidationException existingException,
            ExprValidationContext validationContext) throws ExprValidationException {
        ExprStreamUnderlyingNode exprStream = new ExprStreamUnderlyingNodeImpl(
                identNode.getUnresolvedPropertyName(), false);

        try {
            exprStream.validate(validationContext);
        } catch (ExprValidationException ex) {
            throw existingException;
        }

        return exprStream;
    }

    // Since static method calls such as "Class.method('a')" and mapped properties "Stream.property('key')"
    // look the same, however as the validation could not resolve "Stream.property('key')" before calling this method,
    // this method tries to resolve the mapped property as a static method.
    // Assumes that this is an ExprIdentNode.
    private static ExprNode resolveStaticMethodOrField(ExprIdentNode identNode,
            ExprValidationException propertyException, ExprValidationContext validationContext)
            throws ExprValidationException {
        // Reconstruct the original string
        StringBuilder mappedProperty = new StringBuilder(identNode.getUnresolvedPropertyName());
        if (identNode.getStreamOrPropertyName() != null) {
            mappedProperty.insert(0, identNode.getStreamOrPropertyName() + '.');
        }

        // Parse the mapped property format into a class name, method and single string parameter
        MappedPropertyParseResult parse = parseMappedProperty(mappedProperty.toString());
        if (parse == null) {
            ExprConstantNode constNode = resolveIdentAsEnumConst(mappedProperty.toString(),
                    validationContext.getMethodResolutionService());
            if (constNode == null) {
                throw propertyException;
            } else {
                return constNode;
            }
        }

        // If there is a class name, assume a static method is possible.
        if (parse.getClassName() != null) {
            List<ExprNode> parameters = Collections
                    .singletonList((ExprNode) new ExprConstantNodeImpl(parse.getArgString()));
            List<ExprChainedSpec> chain = new ArrayList<ExprChainedSpec>();
            chain.add(new ExprChainedSpec(parse.getClassName(), Collections.<ExprNode>emptyList(), false));
            chain.add(new ExprChainedSpec(parse.getMethodName(), parameters, false));
            ExprNode result = new ExprDotNode(chain, validationContext.getMethodResolutionService().isDuckType(),
                    validationContext.getMethodResolutionService().isUdfCache());

            // Validate
            try {
                result.validate(validationContext);
            } catch (ExprValidationException e) {
                throw new ExprValidationException(
                        "Failed to resolve enumeration method, date-time method or mapped property '"
                                + mappedProperty + "': " + e.getMessage());
            }

            return result;
        }

        // There is no class name, try a single-row function
        String functionName = parse.getMethodName();
        try {
            Pair<Class, EngineImportSingleRowDesc> classMethodPair = validationContext.getMethodResolutionService()
                    .resolveSingleRow(functionName);
            List<ExprNode> parameters = Collections
                    .singletonList((ExprNode) new ExprConstantNodeImpl(parse.getArgString()));
            List<ExprChainedSpec> chain = Collections.singletonList(
                    new ExprChainedSpec(classMethodPair.getSecond().getMethodName(), parameters, false));
            ExprNode result = new ExprPlugInSingleRowNode(functionName, classMethodPair.getFirst(), chain,
                    classMethodPair.getSecond());

            // Validate
            try {
                result.validate(validationContext);
            } catch (RuntimeException e) {
                throw new ExprValidationException("Plug-in aggregation function '" + parse.getMethodName()
                        + "' failed validation: " + e.getMessage());
            }

            return result;
        } catch (EngineImportUndefinedException e) {
            // Not an single-row function
        } catch (EngineImportException e) {
            throw new IllegalStateException("Error resolving single-row function: " + e.getMessage(), e);
        }

        // Try an aggregation function factory
        try {
            AggregationFunctionFactory aggregationFactory = validationContext.getMethodResolutionService()
                    .resolveAggregationFactory(parse.getMethodName());
            ExprNode result = new ExprPlugInAggFunctionFactoryNode(false, aggregationFactory,
                    parse.getMethodName());
            result.addChildNode(new ExprConstantNodeImpl(parse.getArgString()));

            // Validate
            try {
                result.validate(validationContext);
            } catch (RuntimeException e) {
                throw new ExprValidationException("Plug-in aggregation function '" + parse.getMethodName()
                        + "' failed validation: " + e.getMessage());
            }

            return result;
        } catch (EngineImportUndefinedException e) {
            // Not an aggregation function
        } catch (EngineImportException e) {
            throw new IllegalStateException("Error resolving aggregation: " + e.getMessage(), e);
        }

        // There is no class name, try an aggregation function (AggregationSupport version, deprecated)
        try {
            AggregationSupport aggregation = validationContext.getMethodResolutionService()
                    .resolveAggregation(parse.getMethodName());
            ExprNode result = new ExprPlugInAggFunctionNode(false, aggregation, parse.getMethodName());
            result.addChildNode(new ExprConstantNodeImpl(parse.getArgString()));

            // Validate
            try {
                result.validate(validationContext);
            } catch (RuntimeException e) {
                throw new ExprValidationException("Plug-in aggregation function '" + parse.getMethodName()
                        + "' failed validation: " + e.getMessage());
            }

            return result;
        } catch (EngineImportUndefinedException e) {
            // Not an aggregation function
        } catch (EngineImportException e) {
            throw new IllegalStateException("Error resolving aggregation: " + e.getMessage(), e);
        }

        // absolutly cannot be resolved
        throw propertyException;
    }

    private static ExprConstantNode resolveIdentAsEnumConst(String constant,
            MethodResolutionService methodResolutionService) throws ExprValidationException {
        Object enumValue = JavaClassHelper.resolveIdentAsEnumConst(constant, methodResolutionService, null);
        if (enumValue != null) {
            return new ExprConstantNodeImpl(enumValue);
        }
        return null;
    }

    /**
     * Parse the mapped property into classname, method and string argument.
     * Mind this has been parsed already and is a valid mapped property.
     * @param property is the string property to be passed as a static method invocation
     * @return descriptor object
     */
    protected static MappedPropertyParseResult parseMappedProperty(String property) {
        // get argument
        int indexFirstDoubleQuote = property.indexOf("\"");
        int indexFirstSingleQuote = property.indexOf("'");
        int startArg;
        if ((indexFirstSingleQuote == -1) && (indexFirstDoubleQuote == -1)) {
            return null;
        }
        if ((indexFirstSingleQuote != -1) && (indexFirstDoubleQuote != -1)) {
            if (indexFirstSingleQuote < indexFirstDoubleQuote) {
                startArg = indexFirstSingleQuote;
            } else {
                startArg = indexFirstDoubleQuote;
            }
        } else if (indexFirstSingleQuote != -1) {
            startArg = indexFirstSingleQuote;
        } else {
            startArg = indexFirstDoubleQuote;
        }

        int indexLastDoubleQuote = property.lastIndexOf("\"");
        int indexLastSingleQuote = property.lastIndexOf("'");
        int endArg;
        if ((indexLastSingleQuote == -1) && (indexLastDoubleQuote == -1)) {
            return null;
        }
        if ((indexLastSingleQuote != -1) && (indexLastDoubleQuote != -1)) {
            if (indexLastSingleQuote > indexLastDoubleQuote) {
                endArg = indexLastSingleQuote;
            } else {
                endArg = indexLastDoubleQuote;
            }
        } else if (indexLastSingleQuote != -1) {
            if (indexLastSingleQuote == indexFirstSingleQuote) {
                return null;
            }
            endArg = indexLastSingleQuote;
        } else {
            if (indexLastDoubleQuote == indexFirstDoubleQuote) {
                return null;
            }
            endArg = indexLastDoubleQuote;
        }
        String argument = property.substring(startArg + 1, endArg);

        // get method
        String splitDots[] = property.split("[\\.]");
        if (splitDots.length == 0) {
            return null;
        }

        // find which element represents the method, its the element with the parenthesis
        int indexMethod = -1;
        for (int i = 0; i < splitDots.length; i++) {
            if (splitDots[i].contains("(")) {
                indexMethod = i;
                break;
            }
        }
        if (indexMethod == -1) {
            return null;
        }

        String method = splitDots[indexMethod];
        int indexParan = method.indexOf("(");
        method = method.substring(0, indexParan);
        if (method.length() == 0) {
            return null;
        }

        if (splitDots.length == 1) {
            // no class name
            return new MappedPropertyParseResult(null, method, argument);
        }

        // get class
        StringBuilder clazz = new StringBuilder();
        for (int i = 0; i < indexMethod; i++) {
            if (i > 0) {
                clazz.append('.');
            }
            clazz.append(splitDots[i]);
        }

        return new MappedPropertyParseResult(clazz.toString(), method, argument);
    }

    public static boolean isAllConstants(List<ExprNode> parameters) {
        for (ExprNode node : parameters) {
            if (!node.isConstantResult()) {
                return false;
            }
        }
        return true;
    }

    public static ExprIdentNode getExprIdentNode(EventType[] typesPerStream, int streamId, String property) {
        return new ExprIdentNodeImpl(typesPerStream[streamId], property, streamId);
    }

    public static Class[] getExprResultTypes(ExprEvaluator[] evaluators) {
        Class[] returnTypes = new Class[evaluators.length];
        for (int i = 0; i < evaluators.length; i++) {
            returnTypes[i] = evaluators[i].getType();
        }
        return returnTypes;
    }

    public static Class[] getExprResultTypes(List<ExprNode> expressions) {
        Class[] returnTypes = new Class[expressions.size()];
        for (int i = 0; i < expressions.size(); i++) {
            returnTypes[i] = expressions.get(i).getExprEvaluator().getType();
        }
        return returnTypes;
    }

    public static ExprNodeUtilMethodDesc resolveMethodAllowWildcardAndStream(String className, Class optionalClass,
            String methodName, List<ExprNode> parameters, MethodResolutionService methodResolutionService,
            EventAdapterService eventAdapterService, String statementId, boolean allowWildcard,
            final EventType wildcardType, ExprNodeUtilResolveExceptionHandler exceptionHandler, String functionName)
            throws ExprValidationException {
        Class[] paramTypes = new Class[parameters.size()];
        ExprEvaluator[] childEvals = new ExprEvaluator[parameters.size()];
        int count = 0;
        boolean[] allowEventBeanType = new boolean[parameters.size()];
        boolean[] allowEventBeanCollType = new boolean[parameters.size()];
        ExprEvaluator[] childEvalsEventBeanReturnTypes = new ExprEvaluator[parameters.size()];
        boolean allConstants = true;
        for (ExprNode childNode : parameters) {
            if (childNode instanceof ExprLambdaGoesNode) {
                throw new ExprValidationException(
                        "Unexpected lambda-expression encountered as parameter to UDF or static method '"
                                + methodName + "'");
            }
            if (childNode instanceof ExprNumberSetWildcardMarker) {
                if (wildcardType == null || !allowWildcard) {
                    throw new ExprValidationException("Failed to resolve wildcard parameter to a given event type");
                }
                childEvals[count] = new ExprNodeUtilExprEvalStreamNumUnd(0, wildcardType.getUnderlyingType());
                childEvalsEventBeanReturnTypes[count] = new ExprNodeUtilExprEvalStreamNumEvent(0);
                paramTypes[count] = wildcardType.getUnderlyingType();
                allowEventBeanType[count] = true;
                allConstants = false;
                count++;
                continue;
            }
            if (childNode instanceof ExprStreamUnderlyingNode) {
                ExprStreamUnderlyingNode und = (ExprStreamUnderlyingNode) childNode;
                childEvals[count] = childNode.getExprEvaluator();
                childEvalsEventBeanReturnTypes[count] = new ExprNodeUtilExprEvalStreamNumEvent(und.getStreamId());
                paramTypes[count] = childEvals[count].getType();
                allowEventBeanType[count] = true;
                allConstants = false;
                count++;
                continue;
            }
            if (childNode instanceof ExprEvaluatorEnumeration) {
                ExprEvaluatorEnumeration enumeration = (ExprEvaluatorEnumeration) childNode;
                EventType eventType = enumeration.getEventTypeSingle(eventAdapterService, statementId);
                childEvals[count] = childNode.getExprEvaluator();
                paramTypes[count] = childEvals[count].getType();
                allConstants = false;
                if (eventType != null) {
                    childEvalsEventBeanReturnTypes[count] = new ExprNodeUtilExprEvalStreamNumEnumSingle(
                            enumeration);
                    allowEventBeanType[count] = true;
                    count++;
                    continue;
                }
                EventType eventTypeColl = enumeration.getEventTypeCollection(eventAdapterService);
                if (eventTypeColl != null) {
                    childEvalsEventBeanReturnTypes[count] = new ExprNodeUtilExprEvalStreamNumEnumColl(enumeration);
                    allowEventBeanCollType[count] = true;
                    count++;
                    continue;
                }
            }
            ExprEvaluator eval = childNode.getExprEvaluator();
            childEvals[count] = eval;
            paramTypes[count] = eval.getType();
            count++;
            if (!(childNode.isConstantResult())) {
                allConstants = false;
            }
        }

        // Try to resolve the method
        final FastMethod staticMethod;
        Method method;
        try {
            if (optionalClass != null) {
                method = methodResolutionService.resolveMethod(optionalClass, methodName, paramTypes,
                        allowEventBeanType, allowEventBeanCollType);
            } else {
                method = methodResolutionService.resolveMethod(className, methodName, paramTypes,
                        allowEventBeanType, allowEventBeanCollType);
            }
            FastClass declaringClass = FastClass.create(Thread.currentThread().getContextClassLoader(),
                    method.getDeclaringClass());
            staticMethod = declaringClass.getMethod(method);
        } catch (Exception e) {
            throw exceptionHandler.handle(e);
        }

        // rewrite those evaluator that should return the event itself
        if (CollectionUtil.isAnySet(allowEventBeanType)) {
            for (int i = 0; i < parameters.size(); i++) {
                if (allowEventBeanType[i] && method.getParameterTypes()[i] == EventBean.class) {
                    childEvals[i] = childEvalsEventBeanReturnTypes[i];
                }
            }
        }

        // rewrite those evaluators that should return the event collection
        if (CollectionUtil.isAnySet(allowEventBeanCollType)) {
            for (int i = 0; i < parameters.size(); i++) {
                if (allowEventBeanCollType[i] && method.getParameterTypes()[i] == Collection.class) {
                    childEvals[i] = childEvalsEventBeanReturnTypes[i];
                }
            }
        }

        // add an evaluator if the method expects a context object
        if (method.getParameterTypes().length > 0 && method.getParameterTypes()[method.getParameterTypes().length
                - 1] == EPLMethodInvocationContext.class) {
            childEvals = (ExprEvaluator[]) CollectionUtil.arrayExpandAddSingle(childEvals,
                    new ExprNodeUtilExprEvalMethodContext(functionName));
        }

        return new ExprNodeUtilMethodDesc(allConstants, paramTypes, childEvals, method, staticMethod);
    }

    public static void validatePlainExpression(String expressionTextualName, ExprNode expression)
            throws ExprValidationException {
        ExprNodeSummaryVisitor summaryVisitor = new ExprNodeSummaryVisitor();
        expression.accept(summaryVisitor);
        if (summaryVisitor.isHasAggregation() || summaryVisitor.isHasSubselect()
                || summaryVisitor.isHasStreamSelect() || summaryVisitor.isHasPreviousPrior()) {
            throw new ExprValidationException("Invalid expression '" + expressionTextualName
                    + "': Aggregation, sub-select, previous or prior functions are not supported in this context");
        }
    }

    public static ExprNode validateSimpleGetSubtree(ExprNode expression, StatementContext statementContext,
            EventType optionalEventType) throws ExprValidationException {

        ExprNodeUtility.validatePlainExpression(expression.toExpressionString(), expression);

        StreamTypeServiceImpl streamTypes;
        if (optionalEventType != null) {
            streamTypes = new StreamTypeServiceImpl(optionalEventType, null, true, statementContext.getEngineURI());
        } else {
            streamTypes = new StreamTypeServiceImpl(statementContext.getEngineURI(), false);
        }

        ExprValidationContext validationContext = new ExprValidationContext(streamTypes,
                statementContext.getMethodResolutionService(), null, statementContext.getSchedulingService(),
                statementContext.getVariableService(), new ExprEvaluatorContextStatement(statementContext),
                statementContext.getEventAdapterService(), statementContext.getStatementName(),
                statementContext.getStatementId(), statementContext.getAnnotations(),
                statementContext.getContextDescriptor());
        return ExprNodeUtility.getValidatedSubtree(expression, validationContext);
    }

    public static Set<String> getPropertyNamesIfAllProps(ExprNode[] expressions) {
        for (ExprNode expression : expressions) {
            if (!(expression instanceof ExprIdentNode)) {
                return null;
            }
        }
        Set<String> uniquePropertyNames = new HashSet<String>();
        for (ExprNode expression : expressions) {
            ExprIdentNode identNode = (ExprIdentNode) expression;
            uniquePropertyNames.add(identNode.getUnresolvedPropertyName());
        }
        return uniquePropertyNames;
    }

    /**
     * Encapsulates the parse result parsing a mapped property as a class and method name with args.
     */
    protected static class MappedPropertyParseResult {
        private String className;
        private String methodName;
        private String argString;

        /**
         * Returns class name.
         * @return name of class
         */
        public String getClassName() {
            return className;
        }

        /**
         * Returns the method name.
         * @return method name
         */
        public String getMethodName() {
            return methodName;
        }

        /**
         * Returns the method argument.
         * @return arg
         */
        public String getArgString() {
            return argString;
        }

        /**
         * Returns the parse result of the mapped property.
         * @param className is the class name, or null if there isn't one
         * @param methodName is the method name
         * @param argString is the argument
         */
        public MappedPropertyParseResult(String className, String methodName, String argString) {
            this.className = className;
            this.methodName = methodName;
            this.argString = argString;
        }
    }

    public static void acceptChain(ExprNodeVisitor visitor, List<ExprChainedSpec> chainSpec) {
        for (ExprChainedSpec chain : chainSpec) {
            for (ExprNode param : chain.getParameters()) {
                param.accept(visitor);
            }
        }
    }

    public static void acceptChain(ExprNodeVisitorWithParent visitor, List<ExprChainedSpec> chainSpec) {
        for (ExprChainedSpec chain : chainSpec) {
            for (ExprNode param : chain.getParameters()) {
                param.accept(visitor);
            }
        }
    }

    public static void acceptChain(ExprNodeVisitorWithParent visitor, List<ExprChainedSpec> chainSpec,
            ExprNode parent) {
        for (ExprChainedSpec chain : chainSpec) {
            for (ExprNode param : chain.getParameters()) {
                param.acceptChildnodes(visitor, parent);
            }
        }
    }

    public static final void replaceChildNode(ExprNode parentNode, ExprNode nodeToReplace, ExprNode newNode) {
        int index = ExprNodeUtility.findChildNode(parentNode, nodeToReplace);
        if (index == -1) {
            parentNode.replaceUnlistedChildNode(nodeToReplace, newNode);
        } else {
            parentNode.setChildNode(index, newNode);
        }
    }

    private static int findChildNode(ExprNode parentNode, ExprNode childNode) {
        for (int i = 0; i < parentNode.getChildNodes().length; i++) {
            if (parentNode.getChildNodes()[i] == childNode) {
                return i;
            }
        }
        return -1;
    }

    public static void replaceChainChildNode(ExprNode nodeToReplace, ExprNode newNode,
            List<ExprChainedSpec> chainSpec) {
        for (ExprChainedSpec chained : chainSpec) {
            int index = chained.getParameters().indexOf(nodeToReplace);
            if (index != -1) {
                chained.getParameters().set(index, newNode);
            }
        }
    }

    public static Set<Pair<Integer, String>> getNonAggregatedProps(EventType[] types, List<ExprNode> exprNodes,
            ContextPropertyRegistry contextPropertyRegistry) {
        // Determine all event properties in the clause
        Set<Pair<Integer, String>> nonAggProps = new HashSet<Pair<Integer, String>>();
        for (ExprNode node : exprNodes) {
            ExprNodeIdentifierVisitor visitor = new ExprNodeIdentifierVisitor(false);
            node.accept(visitor);
            List<Pair<Integer, String>> propertiesNodes = visitor.getExprProperties();
            for (Pair<Integer, String> pair : propertiesNodes) {
                EventType originType = types.length > pair.getFirst() ? types[pair.getFirst()] : null;
                if (originType == null || contextPropertyRegistry == null
                        || !contextPropertyRegistry.isPartitionProperty(originType, pair.getSecond())) {
                    nonAggProps.add(pair);
                }
            }
        }

        return nonAggProps;
    }

    public static void addNonAggregatedProps(ExprNode exprNode, Set<Pair<Integer, String>> set) {
        ExprNodeIdentifierVisitor visitor = new ExprNodeIdentifierVisitor(false);
        exprNode.accept(visitor);
        set.addAll(visitor.getExprProperties());
    }

    public static Set<Pair<Integer, String>> getAggregatedProperties(List<ExprAggregateNode> aggregateNodes) {
        // Get a list of properties being aggregated in the clause.
        Set<Pair<Integer, String>> propertiesAggregated = new HashSet<Pair<Integer, String>>();
        for (ExprNode selectAggExprNode : aggregateNodes) {
            ExprNodeIdentifierVisitor visitor = new ExprNodeIdentifierVisitor(true);
            selectAggExprNode.accept(visitor);
            List<Pair<Integer, String>> properties = visitor.getExprProperties();
            propertiesAggregated.addAll(properties);
        }

        return propertiesAggregated;
    }

    public static ExprEvaluator[] getEvaluators(ExprNode[] exprNodes) {
        if (exprNodes == null) {
            return null;
        }
        ExprEvaluator[] eval = new ExprEvaluator[exprNodes.length];
        for (int i = 0; i < exprNodes.length; i++) {
            ExprNode node = exprNodes[i];
            if (node != null) {
                eval[i] = node.getExprEvaluator();
            }
        }
        return eval;
    }

    public static ExprEvaluator[] getEvaluators(List<ExprNode> childNodes) {
        ExprEvaluator[] eval = new ExprEvaluator[childNodes.size()];
        for (int i = 0; i < childNodes.size(); i++) {
            eval[i] = childNodes.get(i).getExprEvaluator();
        }
        return eval;
    }

    public static Set<Integer> getIdentStreamNumbers(ExprNode child) {

        Set<Integer> streams = new HashSet<Integer>();
        ExprNodeIdentifierCollectVisitor visitor = new ExprNodeIdentifierCollectVisitor();
        child.accept(visitor);
        for (ExprIdentNode node : visitor.getExprProperties()) {
            streams.add(node.getStreamId());
        }
        return streams;
    }

    /**
     * Returns true if all properties within the expression are witin data window'd streams.
     * @param child expression to interrogate
     * @param streamTypeService streams
     * @return indicator
     */
    public static boolean hasRemoveStream(ExprNode child, StreamTypeService streamTypeService) {

        // Determine whether all streams are istream-only or irstream
        boolean[] isIStreamOnly = streamTypeService.getIStreamOnly();
        boolean isAllIStream = true; // all true?
        boolean isAllIRStream = true; // all false?
        for (boolean anIsIStreamOnly : isIStreamOnly) {
            if (!anIsIStreamOnly) {
                isAllIStream = false;
            } else {
                isAllIRStream = false;
            }
        }

        // determine if a data-window applies to this max function
        boolean hasDataWindows = true;
        if (isAllIStream) {
            hasDataWindows = false;
        } else if (!isAllIRStream) {
            if (streamTypeService.getEventTypes().length > 1) {
                // In a join we assume that a data window is present or implicit via unidirectional
            } else {
                hasDataWindows = false;
                // get all aggregated properties to determine if any is from a windowed stream
                ExprNodeIdentifierCollectVisitor visitor = new ExprNodeIdentifierCollectVisitor();
                child.accept(visitor);
                for (ExprIdentNode node : visitor.getExprProperties()) {
                    if (!isIStreamOnly[node.getStreamId()]) {
                        hasDataWindows = true;
                        break;
                    }
                }
            }
        }

        return hasDataWindows;
    }

    /**
     * Apply a filter expression.
     * @param filter expression
     * @param streamZeroEvent the event that represents stream zero
     * @param streamOneEvents all events thate are stream one events
     * @param exprEvaluatorContext context for expression evaluation
     * @return filtered stream one events
     */
    public static EventBean[] applyFilterExpression(ExprEvaluator filter, EventBean streamZeroEvent,
            EventBean[] streamOneEvents, ExprEvaluatorContext exprEvaluatorContext) {
        EventBean[] eventsPerStream = new EventBean[2];
        eventsPerStream[0] = streamZeroEvent;

        EventBean[] filtered = new EventBean[streamOneEvents.length];
        int countPass = 0;

        for (EventBean eventBean : streamOneEvents) {
            eventsPerStream[1] = eventBean;

            Boolean result = (Boolean) filter.evaluate(eventsPerStream, true, exprEvaluatorContext);
            if ((result != null) && result) {
                filtered[countPass] = eventBean;
                countPass++;
            }
        }

        if (countPass == streamOneEvents.length) {
            return streamOneEvents;
        }
        return EventBeanUtility.resizeArray(filtered, countPass);
    }

    /**
     * Apply a filter expression returning a pass indicator.
     * @param filter to apply
     * @param eventsPerStream events per stream
     * @param exprEvaluatorContext context for expression evaluation
     * @return pass indicator
     */
    public static boolean applyFilterExpression(ExprEvaluator filter, EventBean[] eventsPerStream,
            ExprEvaluatorContext exprEvaluatorContext) {
        Boolean result = (Boolean) filter.evaluate(eventsPerStream, true, exprEvaluatorContext);
        return (result != null) && result;
    }

    /**
     * Compare two expression nodes and their children in exact child-node sequence,
     * returning true if the 2 expression nodes trees are equals, or false if they are not equals.
     * <p>
     * Recursive call since it uses this method to compare child nodes in the same exact sequence.
     * Nodes are compared using the equalsNode method.
     * @param nodeOne - first expression top node of the tree to compare
     * @param nodeTwo - second expression top node of the tree to compare
     * @return false if this or all child nodes are not equal, true if equal
     */
    public static boolean deepEquals(ExprNode nodeOne, ExprNode nodeTwo) {
        if (nodeOne.getChildNodes().length != nodeTwo.getChildNodes().length) {
            return false;
        }
        if (!nodeOne.equalsNode(nodeTwo)) {
            return false;
        }
        for (int i = 0; i < nodeOne.getChildNodes().length; i++) {
            ExprNode childNodeOne = nodeOne.getChildNodes()[i];
            ExprNode childNodeTwo = nodeTwo.getChildNodes()[i];

            if (!ExprNodeUtility.deepEquals(childNodeOne, childNodeTwo)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Compares two expression nodes via deep comparison, considering all
     * child nodes of either side.
     * @param one array of expressions
     * @param two array of expressions
     * @return true if the expressions are equal, false if not
     */
    public static boolean deepEquals(ExprNode[] one, ExprNode[] two) {
        if (one.length != two.length) {
            return false;
        }
        for (int i = 0; i < one.length; i++) {
            if (!ExprNodeUtility.deepEquals(one[i], two[i])) {
                return false;
            }
        }
        return true;
    }

    public static boolean deepEquals(List<ExprNode> one, List<ExprNode> two) {
        if (one.size() != two.size()) {
            return false;
        }
        for (int i = 0; i < one.size(); i++) {
            if (!ExprNodeUtility.deepEquals(one.get(i), two.get(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Check if the expression is minimal: does not have a subselect, aggregation and does not need view resources
     * @param expression to inspect
     * @return null if minimal, otherwise name of offending sub-expression
     */
    public static String isMinimalExpression(ExprNode expression) {
        ExprNodeSubselectDeclaredDotVisitor subselectVisitor = new ExprNodeSubselectDeclaredDotVisitor();
        expression.accept(subselectVisitor);
        if (subselectVisitor.getSubselects().size() > 0) {
            return "a subselect";
        }

        ExprNodeViewResourceVisitor viewResourceVisitor = new ExprNodeViewResourceVisitor();
        expression.accept(viewResourceVisitor);
        if (viewResourceVisitor.getExprNodes().size() > 0) {
            return "a function that requires view resources (prior, prev)";
        }

        List<ExprAggregateNode> aggregateNodes = new LinkedList<ExprAggregateNode>();
        ExprAggregateNodeUtil.getAggregatesBottomUp(expression, aggregateNodes);
        if (!aggregateNodes.isEmpty()) {
            return "an aggregation function";
        }
        return null;
    }

    protected static void toExpressionString(List<ExprChainedSpec> chainSpec, StringBuilder buffer,
            boolean prefixDot, String functionName) {
        String delimiterOuter = "";
        if (prefixDot) {
            delimiterOuter = ".";
        }
        boolean isFirst = true;
        for (ExprChainedSpec element : chainSpec) {
            buffer.append(delimiterOuter);
            if (functionName != null) {
                buffer.append(functionName);
            } else {
                buffer.append(element.getName());
            }

            // the first item without dot-prefix and empty parameters should not be appended with parenthesis
            if (!isFirst || prefixDot || !element.getParameters().isEmpty()) {
                toExpressionStringIncludeParen(element.getParameters(), buffer);
            }

            delimiterOuter = ".";
            isFirst = false;
        }
    }

    public static void toExpressionString(List<ExprNode> parameters, StringBuilder buffer) {
        String delimiter = "";
        for (ExprNode param : parameters) {
            buffer.append(delimiter);
            delimiter = ", ";
            buffer.append(param.toExpressionString());
        }
    }

    public static void toExpressionStringIncludeParen(List<ExprNode> parameters, StringBuilder buffer) {
        buffer.append("(");
        toExpressionString(parameters, buffer);
        buffer.append(")");
    }

    public static void validate(List<ExprChainedSpec> chainSpec, ExprValidationContext validationContext)
            throws ExprValidationException {

        // validate all parameters
        for (ExprChainedSpec chainElement : chainSpec) {
            List<ExprNode> validated = new ArrayList<ExprNode>();
            for (ExprNode expr : chainElement.getParameters()) {
                validated.add(ExprNodeUtility.getValidatedSubtree(expr, validationContext));
            }
            chainElement.setParameters(validated);
        }
    }

    public static List<ExprNode> collectChainParameters(List<ExprChainedSpec> chainSpec) {
        List<ExprNode> result = new ArrayList<ExprNode>();
        for (ExprChainedSpec chainElement : chainSpec) {
            result.addAll(chainElement.getParameters());
        }
        return result;
    }

    public static void toExpressionStringParams(StringWriter writer, ExprNode[] params, boolean isWildcard,
            String streamWildcard, boolean firstParamOnly) {
        writer.append('(');
        if (isWildcard) {
            writer.append('*');
        } else if (streamWildcard != null) {
            writer.append(streamWildcard);
            writer.append(".*");
        } else {
            String delimiter = "";
            for (ExprNode childNode : params) {
                writer.append(delimiter);
                delimiter = ",";
                writer.append(childNode.toExpressionString());
                if (firstParamOnly) {
                    break;
                }
            }
        }
        writer.append(')');
    }

    public static String printEvaluators(ExprEvaluator[] evaluators) {
        StringWriter writer = new StringWriter();
        String delimiter = "";
        for (int i = 0; i < evaluators.length; i++) {
            writer.append(delimiter);
            writer.append(evaluators[i].getClass().getSimpleName());
            delimiter = ", ";
        }
        return writer.toString();
    }

    public static ScheduleSpec toCrontabSchedule(List<ExprNode> scheduleSpecExpressionList,
            StatementContext context) throws ExprValidationException {

        // Validate the expressions
        ExprEvaluator[] expressions = new ExprEvaluator[scheduleSpecExpressionList.size()];
        int count = 0;
        ExprEvaluatorContextStatement evaluatorContextStmt = new ExprEvaluatorContextStatement(context);
        for (ExprNode parameters : scheduleSpecExpressionList) {
            ExprValidationContext validationContext = new ExprValidationContext(
                    new StreamTypeServiceImpl(context.getEngineURI(), false), context.getMethodResolutionService(),
                    null, context.getSchedulingService(), context.getVariableService(), evaluatorContextStmt,
                    context.getEventAdapterService(), context.getStatementName(), context.getStatementId(),
                    context.getAnnotations(), context.getContextDescriptor());
            ExprNode node = ExprNodeUtility.getValidatedSubtree(parameters, validationContext);
            expressions[count++] = node.getExprEvaluator();
        }

        // Build a schedule
        try {
            Object[] scheduleSpecParameterList = evaluateExpressions(expressions, evaluatorContextStmt);
            return ScheduleSpecUtil.computeValues(scheduleSpecParameterList);
        } catch (ScheduleParameterException e) {
            throw new ExprValidationException("Invalid schedule specification: " + e.getMessage(), e);
        }
    }

    public static Object[] evaluateExpressions(ExprEvaluator[] parameters,
            ExprEvaluatorContext exprEvaluatorContext) {
        Object[] results = new Object[parameters.length];
        int count = 0;
        for (ExprEvaluator expr : parameters) {
            try {
                results[count] = expr.evaluate(null, true, exprEvaluatorContext);
                count++;
            } catch (RuntimeException ex) {
                String message = "Failed expression evaluation in crontab timer-at for parameter " + count + ": "
                        + ex.getMessage();
                log.error(message, ex);
                throw new IllegalArgumentException(message);
            }
        }
        return results;
    }

    public static ExprNode[] toArray(Collection<ExprNode> expressions) {
        if (expressions.isEmpty()) {
            return EMPTY_EXPR_ARRAY;
        }
        return expressions.toArray(new ExprNode[expressions.size()]);
    }

    public static ExprDeclaredNode[] toArray(List<ExprDeclaredNode> declaredNodes) {
        if (declaredNodes.isEmpty()) {
            return EMPTY_DECLARED_ARR;
        }
        return declaredNodes.toArray(new ExprDeclaredNode[declaredNodes.size()]);
    }

    private static final Log log = LogFactory.getLog(ExprNodeUtility.class);
}