001/**
002 * Copyright 2005-2013 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krms.util;
017
018import org.kuali.rice.krms.api.repository.LogicalOperator;
019import org.kuali.rice.krms.api.repository.proposition.PropositionType;
020import org.kuali.rice.krms.dto.PropositionEditor;
021import org.kuali.rice.krms.dto.RuleEditor;
022import org.kuali.rice.krms.service.RuleViewHelperService;
023
024import java.util.ArrayList;
025import java.util.LinkedHashMap;
026import java.util.LinkedList;
027import java.util.List;
028import java.util.Map;
029import java.util.Queue;
030import java.util.Stack;
031
032/**
033 * This is a helper class that validates logical expressions and rebuild a tree based on a valid expression.
034 *
035 * @author Kuali Student Team
036 */
037public class RuleLogicExpressionParser {
038
039    private String expression;
040    private boolean isOtiose;
041
042    private List<ExpressionToken> tokenList;
043
044    private void checkExpressionSet() {
045        if (tokenList == null || expression == null) {
046            throw new java.lang.IllegalStateException("setExpression must be called first");
047        }
048    }
049
050    public boolean validateExpression(List<String> errorMessages, List<String> propsAlpha) {
051        checkExpressionSet();
052        return doValidateExpression(errorMessages, tokenList, propsAlpha);
053    }
054
055    private boolean doValidateExpression(List<String> errorMessages, final List<ExpressionToken> tokens, List<String> propsAlpha) {
056        boolean valid = true;
057        List<String> seenConditonValues = new ArrayList<String>();
058        // allow empty expression. ie. user wants the entire rule deleted.
059        if (tokens.size() == 0) {
060            return true;
061        }
062
063        int firstTokenType = tokens.get(0).getType();
064        if ((firstTokenType != ExpressionToken.PARENTHESIS_START)
065                && (firstTokenType != ExpressionToken.CONDITION)) {
066            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_LOGIC_EXPRESSION_START);
067            return false;
068        }
069        if (countToken(tokens, ExpressionToken.PARENTHESIS_START) != countToken(tokens, ExpressionToken.PARENTHESIS_END)) {
070            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_PARENTHESIS_NOT_PAIR);
071            return false;
072        }
073        if (!validateAndOr(errorMessages, tokens)) {
074            return false;
075        }
076        // condition cannot duplicate
077        for (int i = 0; i < tokens.size(); i++) {
078            ExpressionToken token = tokens.get(i);
079
080            ExpressionToken prevToken = null;
081            ExpressionToken nextToken = null;
082            if(tokens != null){
083                prevToken = i - 1 < 0 ? null : tokens.get(i - 1);
084                nextToken = i + 1 >= tokens.size() ? null : tokens.get(i + 1);
085            }
086
087            if (token.getType() == ExpressionToken.OPERATOR_AND) {
088                if (!checkAnd(errorMessages, prevToken, nextToken)) {
089                    return false;
090                }
091            } else if (token.getType() == ExpressionToken.OPERATOR_OR) {
092                if (!checkOr(errorMessages, prevToken, nextToken)) {
093                    return false;
094                }
095            } else if (token.getType() == ExpressionToken.PARENTHESIS_START) {
096                if (!checkStartParenthesis(errorMessages, prevToken, nextToken)) {
097                    return false;
098                }
099            } else if (token.getType() == ExpressionToken.PARENTHESIS_END) {
100                if (!checkEndParenthesis(errorMessages, prevToken, nextToken)) {
101                    return false;
102                }
103            } else if (token.getType() == ExpressionToken.CONDITION) {
104                if (seenConditonValues.contains(token.getValue())) {
105                    errorMessages.add("Condition " + token.getValue() + " is duplicated.");
106                    return false;
107                } else {
108                    seenConditonValues.add(token.getValue());
109                }
110                if (!checkCondition(errorMessages, prevToken, nextToken)) {
111                    return false;
112                }
113            }
114        }
115        return valid;
116    }
117
118    private boolean validateAndOr(List<String> errorMessages, List<ExpressionToken> nodeList) {
119        Stack<ExpressionToken> operatorStack = new Stack<ExpressionToken>();
120
121        for (ExpressionToken token : nodeList) {
122            if (token.getType() == ExpressionToken.PARENTHESIS_END) {
123                ExpressionToken operator = operatorStack.pop();
124
125                //Check if first type is a OR or a AND
126                if (operator.getType() != ExpressionToken.PARENTHESIS_START) {
127
128                    //Check if all other types are the same as the first type.
129                    while (operatorStack.peek().getType() != ExpressionToken.PARENTHESIS_START) {
130                        ExpressionToken next = operatorStack.pop();
131                        if (next.getType() != operator.getType()) {
132                            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_OPERATOR_TYPE);
133                            return false;
134                        }
135                    }
136
137                    operatorStack.pop();// pop the (
138                }
139            } else if (token.getType() != ExpressionToken.CONDITION) {
140                operatorStack.push(token);
141            }
142        }
143
144        return true;
145    }
146
147    private int countToken(List<ExpressionToken> tokenList, int type) {
148        int count = 0;
149        for (ExpressionToken t : tokenList) {
150            if (t.getType() == type) {
151                count++;
152            }
153        }
154        return count;
155    }
156
157    private boolean checkAnd(List<String> errorMessages, ExpressionToken prevToken, ExpressionToken nextToken) {
158        boolean validToken = true;
159        if ((prevToken != null) && (prevToken.getType() != ExpressionToken.CONDITION) && (prevToken.getType() != ExpressionToken.PARENTHESIS_END)) {
160                errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_PRECEDE_OPERATOR);
161                validToken = false;
162        }
163        if ((nextToken != null) && (nextToken.getType() != ExpressionToken.CONDITION) && (nextToken.getType() != ExpressionToken.PARENTHESIS_START)) {
164            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_FOLLOW_OPERATOR);
165            validToken = false;
166        }
167        return validToken;
168    }
169
170    private boolean checkOr(List<String> errorMessages, ExpressionToken prevToken, ExpressionToken nextToken) {
171        boolean validToken = true;
172        if ((prevToken != null) && (prevToken.getType() != ExpressionToken.CONDITION) && (prevToken.getType() != ExpressionToken.PARENTHESIS_END)) {
173            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_PRECEDE_OR);
174            validToken = false;
175        }
176        if ((nextToken != null) && (nextToken.getType() != ExpressionToken.CONDITION) && (nextToken.getType() != ExpressionToken.PARENTHESIS_START)) {
177            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_FOLLOW_OR);
178            validToken = false;
179        }
180        return validToken;
181    }
182
183    private boolean checkStartParenthesis(List<String> errorMessages, ExpressionToken prevToken, ExpressionToken nextToken) {
184        boolean validToken = true;
185        if ((nextToken != null) && (nextToken.getType() != ExpressionToken.CONDITION) && (nextToken.getType() != ExpressionToken.PARENTHESIS_START)) {
186                errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_FOLLOW_START_PARENTHESIS);
187                validToken = false;
188        }
189        return validToken;
190    }
191
192    private boolean checkEndParenthesis(List<String> errorMessages, ExpressionToken prevToken, ExpressionToken nextToken) {
193        boolean validToken = true;
194        if ((prevToken != null) && (prevToken.getType() != ExpressionToken.CONDITION) && (prevToken.getType() == ExpressionToken.PARENTHESIS_END)) {
195            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_PRECEDE_END_PARENTHESIS);
196            validToken = false;
197        }
198        if ((nextToken != null) && (nextToken.getType() != ExpressionToken.OPERATOR_OR) && (nextToken.getType() != ExpressionToken.OPERATOR_AND) && (nextToken.getType() != ExpressionToken.PARENTHESIS_END)) {
199            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_FOLLOW_END_PARENTHESIS);
200            validToken = false;
201        }
202        return validToken;
203    }
204
205    private boolean checkCondition(List<String> errorMessages, ExpressionToken prevToken, ExpressionToken nextToken) {
206        boolean validToken = true;
207        if ((prevToken != null) && (prevToken.getType() != ExpressionToken.OPERATOR_AND) && (prevToken.getType() != ExpressionToken.OPERATOR_OR) && (prevToken.getType() != ExpressionToken.PARENTHESIS_START)) {
208            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_PRECEDE_CONDITION);
209            validToken = false;
210        }
211        if ((nextToken != null) && (nextToken.getType() != ExpressionToken.OPERATOR_OR) && (nextToken.getType() != ExpressionToken.OPERATOR_AND) && (nextToken.getType() != ExpressionToken.PARENTHESIS_END) && (nextToken.getType() != ExpressionToken.PARENTHESIS_START)) {
212            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_FOLLOW_CONDITION);
213            validToken = false;
214        }
215        return validToken;
216    }
217
218    private List<ExpressionToken> getTokenList(List<String> tokenValueList) {
219        List<ExpressionToken> returnList = new ArrayList<ExpressionToken>();
220        for (String value : tokenValueList) {
221            if (value.isEmpty()) {
222                continue;
223            }
224            if ("(".equals(value)) {
225                ExpressionToken t = new ExpressionToken();
226                t.setType(ExpressionToken.PARENTHESIS_START);
227                returnList.add(t);
228            } else if (")".equals(value)) {
229                ExpressionToken t = new ExpressionToken();
230                t.setType(ExpressionToken.PARENTHESIS_END);
231                returnList.add(t);
232
233            } else if ("and".equals(value)) {
234                ExpressionToken t = new ExpressionToken();
235                t.setType(ExpressionToken.OPERATOR_AND);
236                returnList.add(t);
237
238            } else if ("or".equals(value)) {
239                ExpressionToken t = new ExpressionToken();
240                t.setType(ExpressionToken.OPERATOR_OR);
241                returnList.add(t);
242
243            } else {
244                ExpressionToken t = new ExpressionToken();
245                t.setType(ExpressionToken.CONDITION);
246                t.setValue(value);
247                returnList.add(t);
248
249            }
250        }
251        return returnList;
252    }
253
254    private List<String> getTokenValue(String expression) {
255        expression = expression.toLowerCase();
256        List<String> tokenValueList = new ArrayList<String>();
257        StringBuffer tokenValue = new StringBuffer();
258        for (int i = 0; i < expression.length(); i++) {
259
260            char ch = expression.charAt(i);
261            if (Character.isSpaceChar(ch)) {
262                tokenValueList.add(tokenValue.toString());
263                tokenValue = new StringBuffer();
264            } else if (ch == '(' || ch == ')') {
265                tokenValueList.add(tokenValue.toString());
266                tokenValue = new StringBuffer();
267                tokenValueList.add(String.valueOf(ch));
268            } else {
269                tokenValue.append(ch);
270            }
271        }
272        tokenValueList.add(tokenValue.toString());
273        return tokenValueList;
274    }
275
276    public void setExpression(String expression) {
277        this.expression = expression;
278        List<String> tokenValueList = getTokenValue(expression);
279        this.tokenList = getTokenList(tokenValueList);
280    }
281
282    public String getExpression() {
283        return expression;
284    }
285
286    public List<ExpressionToken> getTokenList() {
287        return tokenList;
288    }
289
290    /**
291     * Method to rebuild the tree based on the set token list that was built from given expression.
292     *
293     * @param ruleEditor
294     * @param viewHelper
295     * @return PropositionEditor
296     */
297    public PropositionEditor parseExpressionIntoRule(RuleEditor ruleEditor, RuleViewHelperService viewHelper) {
298
299        Stack<ExpressionToken> tokenStack = new Stack<ExpressionToken>();
300        int index = 0;
301        isOtiose = false;
302        for (ExpressionToken token : tokenList) {
303            if(isOtioseCompound(token, tokenList, index)) {
304                index++;
305                continue;
306            }
307
308            tokenStack.push(token);
309            index++;
310        }
311
312        Map<String, PropositionEditor> simplePropositions = new LinkedHashMap<String, PropositionEditor>();
313        Queue<PropositionEditor> compoundPropositions = new LinkedList<PropositionEditor>();
314        this.setupPropositions(simplePropositions, compoundPropositions, ruleEditor.getPropositionEditor());
315
316        return ruleFromStack(tokenStack, simplePropositions, compoundPropositions, ruleEditor, viewHelper);
317    }
318
319    private boolean isOtioseCompound(ExpressionToken token, List<ExpressionToken> tokenList, int index) {
320
321        if(token.getType() == ExpressionToken.PARENTHESIS_END && isOtiose) {
322            isOtiose = false;
323            return true;
324        }
325
326        if(token.getType() == ExpressionToken.PARENTHESIS_START) {
327            if(tokenList.size() > index + 2) {
328                if(tokenList.get(index+1).getType() == ExpressionToken.CONDITION && tokenList.get(index+2).getType() == ExpressionToken.PARENTHESIS_END) {
329                    isOtiose = true;
330                    return true;
331                }
332            }
333        }
334        return false;
335    }
336
337    /**
338     * This is a recursive method that loop thru the proposition tree to add all simple proposition in a map so that
339     * they can easily be retrieved by their keys when we rebuild the proposition tree.
340     *
341     * It also creates a queue with all the existing compound propositions so that the root proposition could be
342     * retrieved first.
343     *
344     * @param simplePropositions
345     * @param compoundPropositions
346     * @param propositionEditor
347     */
348    private void setupPropositions(Map<String, PropositionEditor> simplePropositions, Queue<PropositionEditor> compoundPropositions, PropositionEditor propositionEditor) {
349        if (propositionEditor.getPropositionTypeCode().equals(PropositionType.SIMPLE.getCode())) {
350            simplePropositions.put(propositionEditor.getKey(), propositionEditor);
351        } else {
352            compoundPropositions.add(propositionEditor);
353            for (PropositionEditor child : propositionEditor.getCompoundEditors()) {
354                this.setupPropositions(simplePropositions, compoundPropositions, child);
355            }
356            propositionEditor.setCompoundEditors(new ArrayList<PropositionEditor>());
357        }
358
359    }
360
361    /**
362     * Build the proposition tree from the token stack in reverse order. If the token is a end parenthesis,
363     * recursively call this method again to resolve the inner group (compound proposition). When the next token
364     * is a start parenthesis, return the group.     *
365     *
366     * @param tokenStack
367     * @param simplePropositions
368     * @param compoundPropositions
369     * @param rule
370     * @return
371     */
372    public PropositionEditor ruleFromStack(Stack<ExpressionToken> tokenStack, Map<String, PropositionEditor> simplePropositions,
373                                         Queue<PropositionEditor> compoundPropositions, RuleEditor rule, RuleViewHelperService viewHelper) {
374
375        //Get the first compound from the queue, if nothing left create new one.
376        PropositionEditor currentCompound = null;
377        if(compoundPropositions.peek()==null){
378            currentCompound = new PropositionEditor();
379            currentCompound.setPropositionTypeCode(PropositionType.COMPOUND.getCode());
380            currentCompound.setRuleId(rule.getId());
381            currentCompound.setKey((String) rule.getCompoundKeys().next());
382            currentCompound.setCompoundEditors(new ArrayList<PropositionEditor>());
383        } else {
384            currentCompound = compoundPropositions.remove();
385        }
386
387        //Loop thru tokens and recreate the proposition tree.
388        while (!tokenStack.empty()) {
389            ExpressionToken token = tokenStack.pop();
390            if(token.getType() == ExpressionToken.PARENTHESIS_END){
391                PropositionEditor compound = ruleFromStack(tokenStack, simplePropositions, compoundPropositions, rule, viewHelper);
392                currentCompound.getCompoundEditors().add(0,compound);
393            } else if (token.getType() == ExpressionToken.PARENTHESIS_START) {
394                return currentCompound;
395            } else if (token.getType() == ExpressionToken.CONDITION){
396                currentCompound.getCompoundEditors().add(0,simplePropositions.remove(token.getValue().toUpperCase()));
397            } else if (token.getType() == ExpressionToken.OPERATOR_AND){
398                viewHelper.setTypeForCompoundOpCode(currentCompound, LogicalOperator.AND.getCode());
399            } else if (token.getType() == ExpressionToken.OPERATOR_OR){
400                viewHelper.setTypeForCompoundOpCode(currentCompound, LogicalOperator.OR.getCode());
401            }
402        }
403
404        return currentCompound;
405    }
406
407}