package org.ff4j.strategy.el; /* * #%L ff4j-core $Id:$ $HeadURL:$ %% Copyright (C) 2013 Ff4J %% 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. #L% */ import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; /** * Syntax Parser for expression as ( (sampleA|sampleB) & (C|D|!B) & !(A|D) ) | ( (A&B&C)|(C&D)|(A|B&D) ) * * to be converted as : * * <pre> * +------------ OR ---------------+ * / \ * +--------- AND ------+ +------ OR ----+ * / | \ / | \ * OR OR NOT AND AND OR * / \ / | \ | / | \ / \ / \ * sampleA sampleB C D NOT OR A B C C D A AND * | / \ / \ * B A D B D * </pre> * * @author Cedrick Lunven (@clunven) */ public final class ExpressionParser { /** caracter parsed. */ private static final char OPEN_BRACKET = '('; /** caracter parsed. */ private static final char CLOSE_BRACKET = ')'; /** caracter parsed. */ private static final char OR = ExpressionOperator.OR.getChar(); /** caracter parsed. */ private static final char AND = ExpressionOperator.AND.getChar(); /** caracter parsed. */ private static final char NOT = ExpressionOperator.NOT.getChar(); /** * Hide constructor for utility class. */ private ExpressionParser() {} /** * Utility class to store expression and offset of it to avoid multimap * * @author <a href="mailto:cedrick.lunven@gmail.com">Cedrick LUNVEN</a> */ private static class ExpressionBracket { /** store expression. */ private final String expr; /** offset of end of this expression in parent expression */ private final int offsetEnd; /** * Convenient constructor for initialization. * * @param e * target expression * @param o * target offset */ public ExpressionBracket(String e, int o) { this.expr = e; this.offsetEnd = o; } /** * Getter accessor for attribute 'expr'. * * @return current value of 'expr' */ public String getExpr() { return expr; } /** * Getter accessor for attribute 'offsetEnd'. * * @return current value of 'offsetEnd' */ public int getOffsetEnd() { return offsetEnd; } } /** * Brackets have been detected, extract expression to be recursively parsed. * * @param workingExpr * current working expression * @return expression within brackets */ private static ExpressionBracket extractExprWithinParenthesis(String workingExpr) { char[] caracteres = workingExpr.toCharArray(); int offsetOpenBracket = workingExpr.indexOf(OPEN_BRACKET); int offsetEnd = offsetOpenBracket + 1; int bracketLevel = 0; boolean closeBrackFound = false; while (!closeBrackFound) { if (caracteres[offsetEnd] == OPEN_BRACKET) { bracketLevel++; } if (caracteres[offsetEnd] == CLOSE_BRACKET) { if (bracketLevel == 0) { closeBrackFound = true; } bracketLevel--; } offsetEnd++; } // Expression Within parenthesis String withinBracketExpr = workingExpr.substring(offsetOpenBracket + 1, offsetEnd - 1); return new ExpressionBracket(withinBracketExpr, offsetEnd); } /** * Build Expression tree from string. * * @param expression * expression. */ public static ExpressionNode parseExpression(String expressionInput) { ExpressionNode returnednode; String expression = expressionInput.replaceAll(" ", ""); // Only use opetator priorities to build. if (!expression.contains(String.valueOf(OPEN_BRACKET))) { return parseExpressionWithoutParenthesis(expression); } int idx = 0; String initExpr = expression; String workingExpr = initExpr; Map<String, String> exprMap = new HashMap<String, String>(); Map<String, ExpressionNode> exprNodes = new HashMap<String, ExpressionNode>(); // Embedded Parenthesis while (workingExpr.contains(String.valueOf(OPEN_BRACKET))) { // Find expression in parenthesis ExpressionBracket eb = extractExprWithinParenthesis(workingExpr); // Escaping operators exprMap.put("P" + idx, eb.getExpr().replaceAll("\\|", " OR ").replaceAll("\\&", " AND ")); // Saving subnode exprNodes.put("P" + idx, parseExpression(eb.getExpr())); idx++; // Is there any expression with same level of parenthesis ? if ((eb.getOffsetEnd() + 1) <= workingExpr.length()) { workingExpr = workingExpr.substring(eb.getOffsetEnd() + 1, workingExpr.length()); } else { // stop workingExpr = ""; } } // LOG.debug("Embedded Parenthesis finished : INITIAL=" + initExpr + " WITH NODES=" + exprNodes); // Take Initial Expression and replace expression within first parenthesis level to constants String exp = initExpr.replaceAll("\\|", " OR ").replaceAll("\\&", " AND "); for (Entry<String, String> entr : exprMap.entrySet()) { // Escaping parenthesis String val = entr.getValue().replaceAll("\\(", "\\\\\\(").replaceAll("\\)", "\\\\\\)"); exp = exp.replaceAll("\\(" + val + "\\)", entr.getKey()); } // Place real operator for recursive calls exp = exp.replaceAll(" AND ", "&").replaceAll(" OR ", "|").replace(" ", ""); // LOG.debug("Embedded Parenthesis finished with : " + exp + " : " + exprMap.keySet()); returnednode = parseExpressionWithoutParenthesis(exp); // LOG.debug("Expression parsed with parenthesis subtitutions : " + returnednode); // Process substitution ExpressionNode tmpNode = new ExpressionNode(returnednode.getOperator()); tmpNode.getSubNodes().addAll(returnednode.getSubNodes()); for (ExpressionNode exprNode : returnednode.getSubNodes()) { processSubstitution(exprNode, tmpNode, exprNodes); } return tmpNode; } /** * Sub expression have been store and substitute by constant, remove constant and evaluate. * * @param exprNode * current node * @param tmpNode * substituted node * @param exprNodes * availables nodes */ private static void processSubstitution(ExpressionNode exprNode, ExpressionNode tmpNode, Map<String, ExpressionNode> exprNodes) { // Only sheets : (there are only NOT operators with sub levels) if (exprNode.getValue() != null && !exprNode.getValue().isEmpty()) { if (exprNodes.containsKey(exprNode.getValue())) { ExpressionNode storedNode = exprNodes.get(exprNode.getValue()); // LOG.info("Replacing '" + exprNode + "' by '" + storedNode + "'"); tmpNode.getSubNodes().add(storedNode); tmpNode.getSubNodes().remove(exprNode); } } else { // Not with SUB LEVEL ExpressionNode subNodeNot = exprNode.getSubNodes().get(0); if (exprNodes.containsKey(subNodeNot.getValue())) { ExpressionNode notNode = new ExpressionNode(ExpressionOperator.NOT); notNode.getSubNodes().add(exprNodes.get(subNodeNot.getValue())); // LOG.info("Replacing '" + subNodeNot + "' by '" + exprNodes.get(subNodeNot.getValue()) + "'"); tmpNode.getSubNodes().add(notNode); tmpNode.getSubNodes().remove(exprNode); } } } /** * If no bracket nore OR operator contains AND/NOT. * * @param currentNode * current OR Node * @param expr * current expression */ private static ExpressionNode parseExpressionAndNot(ExpressionNode currentNode, String expr) { String[] andOperArray = expr.replaceAll(" ", "").split("\\" + AND); if (andOperArray.length > 1) { // There is AND operation, loop over elements // LOG.debug("NoBracket [" + expr + "] : Operator AND"); ExpressionNode subNodeAND = new ExpressionNode(ExpressionOperator.AND); for (String andOper : andOperArray) { if (andOper.startsWith("!")) { // Handle NOT SHET // LOG.debug("Adding NOT subnode [" + andOper.substring(1) + "]"); ExpressionNode node = new ExpressionNode(ExpressionOperator.NOT); node.getSubNodes().add(new ExpressionNode(andOper.substring(1))); subNodeAND.getSubNodes().add(node); } else { // Handle sheet // LOG.debug("Adding sheet [" + andOper + "]"); subNodeAND.getSubNodes().add(new ExpressionNode(andOper)); } } if (currentNode != null) { currentNode.getSubNodes().add(subNodeAND); return currentNode; } else { return subNodeAND; } } else if (expr.startsWith("!")) { return parseOperatorNot(currentNode, expr); } return parseSheet(currentNode, expr); } /** * Convenient method to parse operator NOT expressions. * * @param currentNode * current node * @param expr * current expression * @return parsed expression as node */ private static ExpressionNode parseOperatorNot(ExpressionNode currentNode, String expr) { // LOG.debug("NoBracket NoOR [" + expr + "] : Operator NOT"); ExpressionNode subNodeNot = new ExpressionNode(ExpressionOperator.NOT); subNodeNot.getSubNodes().add(new ExpressionNode(expr.substring(1))); if (currentNode != null) { currentNode.getSubNodes().add(subNodeNot); } else { return subNodeNot; } return currentNode; } /** * Convenient method to parse operator SHEETS expressions. * * @param currentNode * current node * @param expr * current expression * @return parsed expression as node */ private static ExpressionNode parseSheet(ExpressionNode currentNode, String expr) { // LOG.debug("Adding sheet [" + expr + "]"); if (currentNode != null) { currentNode.getSubNodes().add(new ExpressionNode(expr)); } else { return new ExpressionNode(expr); } return currentNode; } /** * The expression does not contains any parenthesis. We will split through operator Should not be without any opera * * @param expr * expression to be parsed */ private static ExpressionNode parseExpressionWithoutParenthesis(String expr) { // Expression without operator => Sheet if (!expr.contains(String.valueOf(OR)) && !expr.contains(String.valueOf(AND)) && !expr.contains(String.valueOf(NOT))) { // LOG.info("Unique sheet : " + expr); return new ExpressionNode(expr); } else { // First priority to & then to | String[] orOperArray = expr.split("\\" + OR); // current ExpressionNode currentNode; // less priority operator is OR if (orOperArray.length > 1) { // LOG.debug("NoBracket [" + expr + "] : Operator OR"); currentNode = new ExpressionNode(ExpressionOperator.OR); for (String oper : orOperArray) { currentNode = parseExpressionAndNot(currentNode, oper); } } else { // Check next priority operator AND // LOG.debug("NoBracket [" + expr + "] : No Operator OR"); currentNode = parseExpressionAndNot(null, expr); } return currentNode; } } }