View Javadoc

1   /**
2    * Copyright 2005-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krms.util;
17  
18  import org.kuali.rice.krms.api.repository.LogicalOperator;
19  import org.kuali.rice.krms.api.repository.proposition.PropositionType;
20  import org.kuali.rice.krms.dto.PropositionEditor;
21  import org.kuali.rice.krms.dto.RuleEditor;
22  import org.kuali.rice.krms.service.RuleViewHelperService;
23  
24  import java.util.ArrayList;
25  import java.util.LinkedHashMap;
26  import java.util.LinkedList;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Queue;
30  import java.util.Stack;
31  
32  /**
33   * This is a helper class that validates logical expressions and rebuild a tree based on a valid expression.
34   *
35   * @author Kuali Student Team
36   */
37  public class RuleLogicExpressionParser {
38  
39      private String expression;
40      private boolean isOtiose;
41  
42      private List<ExpressionToken> tokenList;
43  
44      private void checkExpressionSet() {
45          if (tokenList == null || expression == null) {
46              throw new java.lang.IllegalStateException("setExpression must be called first");
47          }
48      }
49  
50      public boolean validateExpression(List<String> errorMessages, List<String> propsAlpha) {
51          checkExpressionSet();
52          return doValidateExpression(errorMessages, tokenList, propsAlpha);
53      }
54  
55      private boolean doValidateExpression(List<String> errorMessages, final List<ExpressionToken> tokens, List<String> propsAlpha) {
56          boolean valid = true;
57          List<String> seenConditonValues = new ArrayList<String>();
58          // allow empty expression. ie. user wants the entire rule deleted.
59          if (tokens.size() == 0) {
60              return true;
61          }
62  
63          int firstTokenType = tokens.get(0).getType();
64          if ((firstTokenType != ExpressionToken.PARENTHESIS_START)
65                  && (firstTokenType != ExpressionToken.CONDITION)) {
66              errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_LOGIC_EXPRESSION_START);
67              return false;
68          }
69          if (countToken(tokens, ExpressionToken.PARENTHESIS_START) != countToken(tokens, ExpressionToken.PARENTHESIS_END)) {
70              errorMessages.add(KRMSConstants.KRMS_MSG_ERROR_PARENTHESIS_NOT_PAIR);
71              return false;
72          }
73          if (!validateAndOr(errorMessages, tokens)) {
74              return false;
75          }
76          // condition cannot duplicate
77          for (int i = 0; i < tokens.size(); i++) {
78              ExpressionToken token = tokens.get(i);
79  
80              ExpressionToken prevToken = null;
81              ExpressionToken nextToken = null;
82              if(tokens != null){
83                  prevToken = i - 1 < 0 ? null : tokens.get(i - 1);
84                  nextToken = i + 1 >= tokens.size() ? null : tokens.get(i + 1);
85              }
86  
87              if (token.getType() == ExpressionToken.OPERATOR_AND) {
88                  if (!checkAnd(errorMessages, prevToken, nextToken)) {
89                      return false;
90                  }
91              } else if (token.getType() == ExpressionToken.OPERATOR_OR) {
92                  if (!checkOr(errorMessages, prevToken, nextToken)) {
93                      return false;
94                  }
95              } else if (token.getType() == ExpressionToken.PARENTHESIS_START) {
96                  if (!checkStartParenthesis(errorMessages, prevToken, nextToken)) {
97                      return false;
98                  }
99              } 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 }