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> keyList) {
051        checkExpressionSet();
052        return doValidateExpression(errorMessages, tokenList, keyList);
053    }
054
055    private boolean doValidateExpression(List<String> errorMessages, final List<ExpressionToken> tokens, List<String> keyList) {
056        boolean valid = true;
057        List<String> seenConditonValues = new ArrayList<String>();
058
059        // allow empty expression. ie. user wants the entire rule deleted.
060        if (tokens.size() > 0) {
061            //Code Changed for JIRA-9075 - SONAR Critical issues - Use get(0) with caution - 5
062            int firstToken = 0;
063            int firstTokenType = tokens.get(firstToken).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        } else {
070            return true;
071        }
072        if (countToken(tokens, ExpressionToken.PARENTHESIS_START) != countToken(tokens, ExpressionToken.PARENTHESIS_END)) {
073            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_PARENTHESIS_NOT_PAIR);
074            return false;
075        }
076        if (!validateAndOr(errorMessages, tokens)) {
077            return false;
078        }
079        // condition cannot duplicate
080        for (int i = 0; i < tokens.size(); i++) {
081            ExpressionToken token = tokens.get(i);
082
083            ExpressionToken prevToken = null;
084            ExpressionToken nextToken = null;
085            if (tokens != null) {
086                prevToken = i - 1 < 0 ? null : tokens.get(i - 1);
087                nextToken = i + 1 >= tokens.size() ? null : tokens.get(i + 1);
088            }
089
090            if (token.getType() == ExpressionToken.OPERATOR_AND) {
091                if (!checkAnd(errorMessages, prevToken, nextToken)) {
092                    return false;
093                }
094            } else if (token.getType() == ExpressionToken.OPERATOR_OR) {
095                if (!checkOr(errorMessages, prevToken, nextToken)) {
096                    return false;
097                }
098            } else if (token.getType() == ExpressionToken.PARENTHESIS_START) {
099                if (!checkStartParenthesis(errorMessages, nextToken)) {
100                    return false;
101                }
102            } else if (token.getType() == ExpressionToken.PARENTHESIS_END) {
103                if (!checkEndParenthesis(errorMessages, prevToken, nextToken)) {
104                    return false;
105                }
106            } else if (token.getType() == ExpressionToken.CONDITION) {
107                if (seenConditonValues.contains(token.getValue())) {
108                    errorMessages.add("Condition " + token.getValue() + " is duplicated.");
109                    return false;
110                } else {
111                    seenConditonValues.add(token.getValue());
112                }
113                if (!checkCondition(errorMessages, prevToken, nextToken, i, keyList)) {
114                    return false;
115                }
116            }
117        }
118        return valid;
119    }
120
121    private boolean validateAndOr(List<String> errorMessages, List<ExpressionToken> nodeList) {
122        Stack<ExpressionToken> operatorStack = new Stack<ExpressionToken>();
123
124        for (ExpressionToken token : nodeList) {
125            if (token.getType() == ExpressionToken.PARENTHESIS_END) {
126                ExpressionToken operator = operatorStack.pop();
127
128                //Check if first type is a OR or a AND
129                if (operator.getType() != ExpressionToken.PARENTHESIS_START) {
130
131                    //Check if all other types are the same as the first type.
132                    while (operatorStack.peek().getType() != ExpressionToken.PARENTHESIS_START) {
133                        ExpressionToken next = operatorStack.pop();
134                        if (next.getType() != operator.getType()) {
135                            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_OPERATOR_TYPE);
136                            return false;
137                        }
138                    }
139
140                    operatorStack.pop();// pop the (
141                }
142            } else if (token.getType() != ExpressionToken.CONDITION) {
143                operatorStack.push(token);
144            }
145        }
146        if (!operatorStack.isEmpty()) {
147            ExpressionToken operator = operatorStack.pop();
148            //Check if all other types are the same as the first type.
149            while (!operatorStack.isEmpty()) {
150                ExpressionToken next = operatorStack.pop();
151                if (next.getType() != operator.getType()) {
152                    errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_OPERATOR_TYPE);
153                    return false;
154                }
155            }
156        }
157
158        return true;
159    }
160
161    private int countToken(List<ExpressionToken> tokenList, int type) {
162        int count = 0;
163        for (ExpressionToken t : tokenList) {
164            if (t.getType() == type) {
165                count++;
166            }
167        }
168        return count;
169    }
170
171    private boolean checkAnd(List<String> errorMessages, ExpressionToken prevToken, ExpressionToken nextToken) {
172        boolean validToken = true;
173        if ((prevToken != null) && (prevToken.getType() != ExpressionToken.CONDITION) && (prevToken.getType() != ExpressionToken.PARENTHESIS_END)) {
174            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_PRECEDE_OPERATOR);
175            validToken = false;
176        }
177        if ((nextToken != null) && (nextToken.getType() != ExpressionToken.CONDITION) && (nextToken.getType() != ExpressionToken.PARENTHESIS_START)) {
178            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_FOLLOW_OPERATOR);
179            validToken = false;
180        }
181        return validToken;
182    }
183
184    private boolean checkOr(List<String> errorMessages, ExpressionToken prevToken, ExpressionToken nextToken) {
185        boolean validToken = true;
186        if ((prevToken != null) && (prevToken.getType() != ExpressionToken.CONDITION) && (prevToken.getType() != ExpressionToken.PARENTHESIS_END)) {
187            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_PRECEDE_OR);
188            validToken = false;
189        }
190        if ((nextToken != null) && (nextToken.getType() != ExpressionToken.CONDITION) && (nextToken.getType() != ExpressionToken.PARENTHESIS_START)) {
191            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_FOLLOW_OR);
192            validToken = false;
193        }
194        return validToken;
195    }
196
197    private boolean checkStartParenthesis(List<String> errorMessages, ExpressionToken nextToken) {
198        boolean validToken = true;
199        if ((nextToken != null) && (nextToken.getType() != ExpressionToken.CONDITION) && (nextToken.getType() != ExpressionToken.PARENTHESIS_START)) {
200            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_FOLLOW_START_PARENTHESIS);
201            validToken = false;
202        }
203        return validToken;
204    }
205
206    private boolean checkEndParenthesis(List<String> errorMessages, ExpressionToken prevToken, ExpressionToken nextToken) {
207        boolean validToken = true;
208        if ((prevToken != null) && (prevToken.getType() != ExpressionToken.CONDITION) && (prevToken.getType() == ExpressionToken.PARENTHESIS_END)) {
209            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_PRECEDE_END_PARENTHESIS);
210            validToken = false;
211        }
212        if ((nextToken != null) && (!ExpressionToken.isOperator(nextToken.getType())) && (nextToken.getType() != ExpressionToken.PARENTHESIS_END)) {
213            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_FOLLOW_END_PARENTHESIS);
214            validToken = false;
215        }
216        return validToken;
217    }
218
219    private boolean checkCondition(List<String> errorMessages, ExpressionToken prevToken, ExpressionToken nextToken, int currentIndex,
220                                   List<String> keyList) {
221        boolean validToken = true;
222        if ((prevToken != null) && (!ExpressionToken.isOperator(prevToken.getType())) && (prevToken.getType() != ExpressionToken.PARENTHESIS_START)) {
223            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_PRECEDE_CONDITION);
224            validToken = false;
225        }
226        if ((nextToken != null) && (!ExpressionToken.isOperator(nextToken.getType())) && (nextToken.getType() != ExpressionToken.PARENTHESIS_END) && (nextToken.getType() != ExpressionToken.PARENTHESIS_START)) {
227            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_FOLLOW_CONDITION);
228            validToken = false;
229        }
230
231        ExpressionToken conditionToken = tokenList.get(currentIndex);
232        if((!keyList.contains(conditionToken.getValue().toLowerCase())) && (!keyList.contains(conditionToken.getValue().toUpperCase()))){
233            errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_INVALID_CONDITION);
234            validToken = false;
235        }
236
237        return validToken;
238    }
239
240    private List<ExpressionToken> getTokenList(List<String> tokenValueList) {
241        List<ExpressionToken> returnList = new ArrayList<ExpressionToken>();
242        for (String value : tokenValueList) {
243            if (value.isEmpty()) {
244                continue;
245            }
246            if ("(".equals(value)) {
247                ExpressionToken t = new ExpressionToken();
248                t.setType(ExpressionToken.PARENTHESIS_START);
249                returnList.add(t);
250            } else if (")".equals(value)) {
251                ExpressionToken t = new ExpressionToken();
252                t.setType(ExpressionToken.PARENTHESIS_END);
253                returnList.add(t);
254
255            } else if ("and".equals(value)) {
256                ExpressionToken t = new ExpressionToken();
257                t.setType(ExpressionToken.OPERATOR_AND);
258                returnList.add(t);
259
260            } else if ("or".equals(value)) {
261                ExpressionToken t = new ExpressionToken();
262                t.setType(ExpressionToken.OPERATOR_OR);
263                returnList.add(t);
264
265            } else {
266                ExpressionToken t = new ExpressionToken();
267                t.setType(ExpressionToken.CONDITION);
268                t.setValue(value);
269                returnList.add(t);
270
271            }
272        }
273        return returnList;
274    }
275
276    private List<String> getTokenValue(String expression) {
277        List<String> tokenValueList = new ArrayList<String>();
278        StringBuilder tokenValue = new StringBuilder();
279        for (int i = 0; i < expression.length(); i++) {
280
281            char ch = expression.charAt(i);
282            if (Character.isSpaceChar(ch)) {
283                tokenValueList.add(tokenValue.toString());
284                tokenValue = new StringBuilder();
285            } else if (ch == '(' || ch == ')') {
286                tokenValueList.add(tokenValue.toString());
287                tokenValue = new StringBuilder();
288                tokenValueList.add(String.valueOf(ch));
289            } else {
290                tokenValue.append(ch);
291            }
292        }
293        tokenValueList.add(tokenValue.toString());
294        return tokenValueList;
295    }
296
297    public void setExpression(String expression) {
298        this.expression = expression;
299        List<String> tokenValueList = getTokenValue(expression.toLowerCase());
300        this.tokenList = getTokenList(tokenValueList);
301    }
302
303    public String getExpression() {
304        return expression;
305    }
306
307    public List<ExpressionToken> getTokenList() {
308        return tokenList;
309    }
310
311    /**
312     * Method to rebuild the tree based on the set token list that was built from given expression.
313     *
314     * @param ruleEditor
315     * @param viewHelper
316     * @return PropositionEditor
317     */
318    public PropositionEditor parseExpressionIntoRule(RuleEditor ruleEditor, RuleViewHelperService viewHelper) {
319
320        Stack<ExpressionToken> tokenStack = new Stack<ExpressionToken>();
321        int index = 0;
322        isOtiose = false;
323        for (ExpressionToken token : tokenList) {
324            if (isOtioseCompound(token, tokenList, index)) {
325                index++;
326                continue;
327            }
328
329            tokenStack.push(token);
330            index++;
331        }
332
333        //Return null if stack is empty, all propositions deleted from logic expression.
334        if(tokenStack.isEmpty()){
335            return null;
336        }
337
338        Map<String, PropositionEditor> simplePropositions = new LinkedHashMap<String, PropositionEditor>();
339        Queue<PropositionEditor> compoundPropositions = new LinkedList<PropositionEditor>();
340        this.setupPropositions(simplePropositions, compoundPropositions, ruleEditor.getPropositionEditor());
341
342        return ruleFromStack(tokenStack, simplePropositions, compoundPropositions, ruleEditor, viewHelper);
343    }
344
345    private boolean isOtioseCompound(ExpressionToken token, List<ExpressionToken> tokenList, int index) {
346
347        if (token.getType() == ExpressionToken.PARENTHESIS_END && isOtiose) {
348            isOtiose = false;
349            return true;
350        }
351
352        if (token.getType() == ExpressionToken.PARENTHESIS_START) {
353            if (tokenList.size() > index + 2) {
354                if (tokenList.get(index + 1).getType() == ExpressionToken.CONDITION && tokenList.get(index + 2).getType() == ExpressionToken.PARENTHESIS_END) {
355                    isOtiose = true;
356                    return true;
357                }
358            }
359        }
360        return false;
361    }
362
363    /**
364     * This is a recursive method that loop thru the proposition tree to add all simple proposition in a map so that
365     * they can easily be retrieved by their keys when we rebuild the proposition tree.
366     * <p/>
367     * It also creates a queue with all the existing compound propositions so that the root proposition could be
368     * retrieved first.
369     *
370     * @param simplePropositions
371     * @param compoundPropositions
372     * @param propositionEditor
373     */
374    private void setupPropositions(Map<String, PropositionEditor> simplePropositions, Queue<PropositionEditor> compoundPropositions, PropositionEditor propositionEditor) {
375        if (propositionEditor.getPropositionTypeCode().equals(PropositionType.SIMPLE.getCode())) {
376            simplePropositions.put(propositionEditor.getKey(), propositionEditor);
377        } else {
378            compoundPropositions.add(propositionEditor);
379            for (PropositionEditor child : propositionEditor.getCompoundEditors()) {
380                this.setupPropositions(simplePropositions, compoundPropositions, child);
381            }
382            propositionEditor.setCompoundEditors(new ArrayList<PropositionEditor>());
383        }
384
385    }
386
387    /**
388     * Build the proposition tree from the token stack in reverse order. If the token is a end parenthesis,
389     * recursively call this method again to resolve the inner group (compound proposition). When the next token
390     * is a start parenthesis, return the group.     *
391     *
392     * @param tokenStack
393     * @param simplePropositions
394     * @param compoundPropositions
395     * @param rule
396     * @return
397     */
398    public PropositionEditor ruleFromStack(Stack<ExpressionToken> tokenStack, Map<String, PropositionEditor> simplePropositions,
399                                           Queue<PropositionEditor> compoundPropositions, RuleEditor rule, RuleViewHelperService viewHelper) {
400
401        //Check for single statement.
402        if(tokenStack.size()==1){
403            ExpressionToken token = tokenStack.pop();
404            return simplePropositions.remove(token.getValue().toUpperCase());
405        }
406
407        //Get the first compound from the queue, if nothing left create new one.
408        PropositionEditor currentCompound = null;
409        if (compoundPropositions.peek() == null) {
410            currentCompound = new PropositionEditor();
411            currentCompound.setPropositionTypeCode(PropositionType.COMPOUND.getCode());
412            currentCompound.setRuleId(rule.getId());
413            currentCompound.setKey((String) rule.getCompoundKeys().next());
414            currentCompound.setCompoundEditors(new ArrayList<PropositionEditor>());
415        } else {
416            currentCompound = compoundPropositions.remove();
417        }
418
419        //Loop thru tokens and recreate the proposition tree.
420        while (!tokenStack.empty()) {
421            ExpressionToken token = tokenStack.pop();
422            if (token.getType() == ExpressionToken.PARENTHESIS_END) {
423                PropositionEditor compound = ruleFromStack(tokenStack, simplePropositions, compoundPropositions, rule, viewHelper);
424                currentCompound.getCompoundEditors().add(0, compound);
425            } else if (token.getType() == ExpressionToken.PARENTHESIS_START) {
426                return currentCompound;
427            } else if (token.getType() == ExpressionToken.CONDITION) {
428                currentCompound.getCompoundEditors().add(0, simplePropositions.remove(token.getValue().toUpperCase()));
429            } else if (token.getType() == ExpressionToken.OPERATOR_AND) {
430                viewHelper.setTypeForCompoundOpCode(currentCompound, LogicalOperator.AND.getCode());
431            } else if (token.getType() == ExpressionToken.OPERATOR_OR) {
432                viewHelper.setTypeForCompoundOpCode(currentCompound, LogicalOperator.OR.getCode());
433            }
434        }
435
436        return currentCompound;
437    }
438
439}