/* *************************************************************************************** * Copyright (C) 2006 EsperTech, Inc. All rights reserved. * * http://www.espertech.com/esper * * 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.funcs; import com.espertech.esper.client.EventBean; import com.espertech.esper.collection.UniformPair; import com.espertech.esper.epl.expression.core.*; import com.espertech.esper.event.map.MapEventType; import com.espertech.esper.metrics.instrumentation.InstrumentationHelper; import com.espertech.esper.util.CoercionException; import com.espertech.esper.util.JavaClassHelper; import com.espertech.esper.util.SimpleNumberCoercer; import com.espertech.esper.util.SimpleNumberCoercerFactory; import java.io.StringWriter; import java.util.*; /** * Represents the case-when-then-else control flow function is an expression tree. */ public class ExprCaseNode extends ExprNodeBase implements ExprEvaluator, ExprEvaluatorTypableReturn { private static final long serialVersionUID = 792538321520346459L; private final boolean isCase2; private Class resultType; private transient LinkedHashMap<String, Object> mapResultType; private boolean isNumericResult; private boolean mustCoerce; private transient SimpleNumberCoercer coercer; private transient List<UniformPair<ExprEvaluator>> whenThenNodeList; private transient ExprEvaluator optionalCompareExprNode; private transient ExprEvaluator optionalElseExprNode; /** * Ctor. * * @param isCase2 is an indicator of which Case statement we are working on. * <p> True indicates a 'Case2' statement with syntax "case a when a1 then b1 else b2". * <p> False indicates a 'Case1' statement with syntax "case when a=a1 then b1 else b2". */ public ExprCaseNode(boolean isCase2) { this.isCase2 = isCase2; } public ExprEvaluator getExprEvaluator() { return this; } /** * Returns true if this is a switch-type case. * * @return true for switch-type case, or false for when-then type */ public boolean isCase2() { return isCase2; } public ExprNode validate(ExprValidationContext validationContext) throws ExprValidationException { CaseAnalysis analysis = analyzeCase(); whenThenNodeList = new ArrayList<UniformPair<ExprEvaluator>>(); for (UniformPair<ExprNode> pair : analysis.getWhenThenNodeList()) { if (!isCase2) { if (pair.getFirst().getExprEvaluator().getType() != Boolean.class) { throw new ExprValidationException("Case node 'when' expressions must return a boolean value"); } } whenThenNodeList.add(new UniformPair<ExprEvaluator>(pair.getFirst().getExprEvaluator(), pair.getSecond().getExprEvaluator())); } if (analysis.getOptionalCompareExprNode() != null) { optionalCompareExprNode = analysis.getOptionalCompareExprNode().getExprEvaluator(); } if (analysis.getOptionalElseExprNode() != null) { optionalElseExprNode = analysis.getOptionalElseExprNode().getExprEvaluator(); } if (isCase2) { validateCaseTwo(); } // Determine type of each result (then-node and else node) child node expression List<Class> childTypes = new LinkedList<Class>(); List<LinkedHashMap<String, Object>> childMapTypes = new LinkedList<LinkedHashMap<String, Object>>(); for (UniformPair<ExprEvaluator> pair : whenThenNodeList) { if (pair.getSecond() instanceof ExprEvaluatorTypableReturn) { ExprEvaluatorTypableReturn typableReturn = (ExprEvaluatorTypableReturn) pair.getSecond(); LinkedHashMap<String, Object> rowProps = typableReturn.getRowProperties(); if (rowProps != null) { childMapTypes.add(rowProps); continue; } } childTypes.add(pair.getSecond().getType()); } if (optionalElseExprNode != null) { if (optionalElseExprNode instanceof ExprEvaluatorTypableReturn) { ExprEvaluatorTypableReturn typableReturn = (ExprEvaluatorTypableReturn) optionalElseExprNode; LinkedHashMap<String, Object> rowProps = typableReturn.getRowProperties(); if (rowProps != null) { childMapTypes.add(rowProps); } else { childTypes.add(optionalElseExprNode.getType()); } } else { childTypes.add(optionalElseExprNode.getType()); } } if (!childMapTypes.isEmpty() && !childTypes.isEmpty()) { String message = "Case node 'when' expressions require that all results either return a single value or a Map-type (new-operator) value"; String check; int count = -1; for (UniformPair<ExprEvaluator> pair : whenThenNodeList) { count++; if (pair.getSecond().getType() != Map.class && pair.getSecond().getType() != null) { check = ", check when-condition number " + count; throw new ExprValidationException(message + check); } } if (optionalElseExprNode != null) { if (optionalElseExprNode.getType() != Map.class && optionalElseExprNode.getType() != null) { check = ", check the else-condition"; throw new ExprValidationException(message + check); } } throw new ExprValidationException(message); } if (childMapTypes.isEmpty()) { // Determine common denominator type try { resultType = JavaClassHelper.getCommonCoercionType(childTypes.toArray(new Class[childTypes.size()])); if (JavaClassHelper.isNumeric(resultType)) { isNumericResult = true; } } catch (CoercionException ex) { throw new ExprValidationException("Implicit conversion not allowed: " + ex.getMessage()); } } else { mapResultType = childMapTypes.get(0); for (int i = 1; i < childMapTypes.size(); i++) { Map<String, Object> other = childMapTypes.get(i); String messageEquals = MapEventType.isDeepEqualsProperties("Case-when number " + i, mapResultType, other); if (messageEquals != null) { throw new ExprValidationException("Incompatible case-when return types by new-operator in case-when number " + i + ": " + messageEquals); } } } return null; } public boolean isConstantResult() { return false; } public Class getType() { return resultType; } public LinkedHashMap<String, Object> getRowProperties() throws ExprValidationException { return mapResultType; } public Object evaluate(EventBean[] eventsPerStream, boolean isNewData, ExprEvaluatorContext exprEvaluatorContext) { if (InstrumentationHelper.ENABLED) { InstrumentationHelper.get().qExprCase(this); Object result; if (!isCase2) { result = evaluateCaseSyntax1(eventsPerStream, isNewData, exprEvaluatorContext); } else { result = evaluateCaseSyntax2(eventsPerStream, isNewData, exprEvaluatorContext); } InstrumentationHelper.get().aExprCase(result); return result; } if (!isCase2) { return evaluateCaseSyntax1(eventsPerStream, isNewData, exprEvaluatorContext); } else { return evaluateCaseSyntax2(eventsPerStream, isNewData, exprEvaluatorContext); } } public Boolean isMultirow() { return mapResultType == null ? null : false; } public Object[] evaluateTypableSingle(EventBean[] eventsPerStream, boolean isNewData, ExprEvaluatorContext context) { Map<String, Object> map = (Map<String, Object>) evaluate(eventsPerStream, isNewData, context); Object[] row = new Object[map.size()]; int index = -1; for (Map.Entry<String, Object> entry : mapResultType.entrySet()) { index++; row[index] = map.get(entry.getKey()); } return row; } public Object[][] evaluateTypableMulti(EventBean[] eventsPerStream, boolean isNewData, ExprEvaluatorContext context) { return null; // always single-row } public boolean equalsNode(ExprNode node, boolean ignoreStreamPrefix) { if (!(node instanceof ExprCaseNode)) { return false; } ExprCaseNode otherExprCaseNode = (ExprCaseNode) node; return this.isCase2 == otherExprCaseNode.isCase2; } public void toPrecedenceFreeEPL(StringWriter writer) { CaseAnalysis analysis; try { analysis = analyzeCase(); } catch (ExprValidationException e) { throw new RuntimeException(e.getMessage(), e); } writer.append("case"); if (isCase2) { writer.append(' '); analysis.getOptionalCompareExprNode().toEPL(writer, getPrecedence()); } for (UniformPair<ExprNode> p : analysis.getWhenThenNodeList()) { writer.append(" when "); p.getFirst().toEPL(writer, getPrecedence()); writer.append(" then "); p.getSecond().toEPL(writer, getPrecedence()); } if (analysis.getOptionalElseExprNode() != null) { writer.append(" else "); analysis.getOptionalElseExprNode().toEPL(writer, getPrecedence()); } writer.append(" end"); } public ExprPrecedenceEnum getPrecedence() { return ExprPrecedenceEnum.CASE; } private CaseAnalysis analyzeCaseOne() throws ExprValidationException { // Case 1 expression example: // case when a=b then x [when c=d then y...] [else y] // ExprNode[] children = this.getChildNodes(); if (children.length < 2) { throw new ExprValidationException("Case node must have at least 2 parameters"); } List<UniformPair<ExprNode>> whenThenNodeList = new LinkedList<UniformPair<ExprNode>>(); int numWhenThen = children.length >> 1; for (int i = 0; i < numWhenThen; i++) { ExprNode whenExpr = children[i << 1]; ExprNode thenExpr = children[(i << 1) + 1]; whenThenNodeList.add(new UniformPair<ExprNode>(whenExpr, thenExpr)); } ExprNode optionalElseExprNode = null; if (children.length % 2 != 0) { optionalElseExprNode = children[children.length - 1]; } return new CaseAnalysis(whenThenNodeList, null, optionalElseExprNode); } private CaseAnalysis analyzeCaseTwo() throws ExprValidationException { // Case 2 expression example: // case p when p1 then x [when p2 then y...] [else z] // ExprNode[] children = this.getChildNodes(); if (children.length < 3) { throw new ExprValidationException("Case node must have at least 3 parameters"); } ExprNode optionalCompareExprNode = children[0]; List<UniformPair<ExprNode>> whenThenNodeList = new LinkedList<UniformPair<ExprNode>>(); int numWhenThen = (children.length - 1) / 2; for (int i = 0; i < numWhenThen; i++) { whenThenNodeList.add(new UniformPair<ExprNode>(children[i * 2 + 1], children[i * 2 + 2])); } ExprNode optionalElseExprNode = null; if (numWhenThen * 2 + 1 < children.length) { optionalElseExprNode = children[children.length - 1]; } return new CaseAnalysis(whenThenNodeList, optionalCompareExprNode, optionalElseExprNode); } private void validateCaseTwo() throws ExprValidationException { // validate we can compare result types List<Class> comparedTypes = new LinkedList<Class>(); comparedTypes.add(optionalCompareExprNode.getType()); for (UniformPair<ExprEvaluator> pair : whenThenNodeList) { comparedTypes.add(pair.getFirst().getType()); } // Determine common denominator type try { Class coercionType = JavaClassHelper.getCommonCoercionType(comparedTypes.toArray(new Class[comparedTypes.size()])); // Determine if we need to coerce numbers when one type doesn't match any other type if (JavaClassHelper.isNumeric(coercionType)) { mustCoerce = false; for (Class comparedType : comparedTypes) { if (comparedType != coercionType) { mustCoerce = true; } } if (mustCoerce) { coercer = SimpleNumberCoercerFactory.getCoercer(null, coercionType); } } } catch (CoercionException ex) { throw new ExprValidationException("Implicit conversion not allowed: " + ex.getMessage()); } } private Object evaluateCaseSyntax1(EventBean[] eventsPerStream, boolean isNewData, ExprEvaluatorContext exprEvaluatorContext) { // Case 1 expression example: // case when a=b then x [when c=d then y...] [else y] Object caseResult = null; boolean matched = false; for (UniformPair<ExprEvaluator> p : whenThenNodeList) { Boolean whenResult = (Boolean) p.getFirst().evaluate(eventsPerStream, isNewData, exprEvaluatorContext); // If the 'when'-expression returns true if ((whenResult != null) && whenResult) { caseResult = p.getSecond().evaluate(eventsPerStream, isNewData, exprEvaluatorContext); matched = true; break; } } if ((!matched) && (optionalElseExprNode != null)) { caseResult = optionalElseExprNode.evaluate(eventsPerStream, isNewData, exprEvaluatorContext); } if (caseResult == null) { return null; } if ((caseResult.getClass() != resultType) && isNumericResult) { return JavaClassHelper.coerceBoxed((Number) caseResult, resultType); } return caseResult; } private Object evaluateCaseSyntax2(EventBean[] eventsPerStream, boolean isNewData, ExprEvaluatorContext exprEvaluatorContext) { // Case 2 expression example: // case p when p1 then x [when p2 then y...] [else z] Object checkResult = optionalCompareExprNode.evaluate(eventsPerStream, isNewData, exprEvaluatorContext); Object caseResult = null; boolean matched = false; for (UniformPair<ExprEvaluator> p : whenThenNodeList) { Object whenResult = p.getFirst().evaluate(eventsPerStream, isNewData, exprEvaluatorContext); if (compare(checkResult, whenResult)) { caseResult = p.getSecond().evaluate(eventsPerStream, isNewData, exprEvaluatorContext); matched = true; break; } } if ((!matched) && (optionalElseExprNode != null)) { caseResult = optionalElseExprNode.evaluate(eventsPerStream, isNewData, exprEvaluatorContext); } if (caseResult == null) { return null; } if ((caseResult.getClass() != resultType) && isNumericResult) { return JavaClassHelper.coerceBoxed((Number) caseResult, resultType); } return caseResult; } private boolean compare(Object leftResult, Object rightResult) { if (leftResult == null) { return rightResult == null; } if (rightResult == null) { return false; } if (!mustCoerce) { return leftResult.equals(rightResult); } else { Number left = coercer.coerceBoxed((Number) leftResult); Number right = coercer.coerceBoxed((Number) rightResult); return left.equals(right); } } private CaseAnalysis analyzeCase() throws ExprValidationException { if (isCase2) { return analyzeCaseTwo(); } else { return analyzeCaseOne(); } } public static class CaseAnalysis { private List<UniformPair<ExprNode>> whenThenNodeList; private ExprNode optionalCompareExprNode; private ExprNode optionalElseExprNode; public CaseAnalysis(List<UniformPair<ExprNode>> whenThenNodeList, ExprNode optionalCompareExprNode, ExprNode optionalElseExprNode) { this.whenThenNodeList = whenThenNodeList; this.optionalCompareExprNode = optionalCompareExprNode; this.optionalElseExprNode = optionalElseExprNode; } public List<UniformPair<ExprNode>> getWhenThenNodeList() { return whenThenNodeList; } public ExprNode getOptionalCompareExprNode() { return optionalCompareExprNode; } public ExprNode getOptionalElseExprNode() { return optionalElseExprNode; } } }