View Javadoc
1   /**
2    * Copyright 2005-2016 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.krad.uif.util;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.commons.logging.Log;
20  import org.apache.commons.logging.LogFactory;
21  import org.kuali.rice.core.api.exception.RiceRuntimeException;
22  import org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean;
23  
24  import java.util.ArrayList;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  
29  /**
30   * Utility class for UIF expressions
31   *
32   * @author Kuali Rice Team (rice.collab@kuali.org)
33   */
34  public class ExpressionUtils {
35      private static final Log LOG = LogFactory.getLog(ExpressionUtils.class);
36  
37      /**
38       * Pulls expressions within the expressionConfigurable's expression graph and moves them to the property
39       * expressions
40       * map for the expressionConfigurable or a nested expressionConfigurable (for the case of nested expression
41       * property
42       * names)
43       *
44       * <p>
45       * Expressions that are configured on properties and pulled out by the {@link org.kuali.rice.krad.datadictionary.uif.UifBeanFactoryPostProcessor}
46       * and put in the {@link org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean#getExpressionGraph()} for the
47       * bean
48       * that is
49       * at root (non nested) level. Before evaluating the expressions, they need to be moved to the
50       * {@link org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean#getPropertyExpressions()} map for the
51       * expressionConfigurable that
52       * property
53       * is on.
54       * </p>
55       *
56       * @param expressionConfigurable expressionConfigurable instance to process expressions for
57       */
58      public static void populatePropertyExpressionsFromGraph(UifDictionaryBean expressionConfigurable) {
59          if (expressionConfigurable == null || expressionConfigurable.getExpressionGraph() == null) {
60              return;
61          }
62  
63          // will hold graphs to populate the refreshExpressionGraph property on each expressionConfigurable
64          // key is the path to the expressionConfigurable and value is the map of nested property names to expressions
65          Map<String, Map<String, String>> refreshExpressionGraphs = new HashMap<String, Map<String, String>>();
66  
67          Map<String, String> expressionGraph = expressionConfigurable.getExpressionGraph();
68          for (Map.Entry<String, String> expressionEntry : expressionGraph.entrySet()) {
69              String propertyName = expressionEntry.getKey();
70              String expression = expressionEntry.getValue();
71  
72              // by default assume expression belongs with passed in expressionConfigurable
73              UifDictionaryBean configurableWithExpression = expressionConfigurable;
74  
75              // if property name is nested, we need to move the expression to the last expressionConfigurable
76              String adjustedPropertyName = propertyName;
77              if (StringUtils.contains(propertyName, ".")) {
78                  String configurablePath = StringUtils.substringBeforeLast(propertyName, ".");
79                  adjustedPropertyName = StringUtils.substringAfterLast(propertyName, ".");
80  
81                  Object nestedObject = ObjectPropertyUtils.getPropertyValue(expressionConfigurable, configurablePath);
82  
83                  // skip missing expression object for components skipping their lifecycle because objects
84                  // in these components may be missing (and are expected to be missing)
85                  if (nestedObject == null
86                          && expressionConfigurable instanceof LifecycleElement
87                          && ((LifecycleElement) expressionConfigurable).skipLifecycle()) {
88                      continue;
89                  }
90  
91                  if ((nestedObject == null) || !(nestedObject instanceof UifDictionaryBean)) {
92                      throw new RiceRuntimeException("Object for which expression is configured on is null or does not "
93                              + "implement UifDictionaryBean: '"
94                              + configurablePath
95                              + "' on class "
96                              + expressionConfigurable.getClass().getName()
97                              + " while evaluating "
98                              + "expression for "
99                              + propertyName);
100                 }
101 
102                 // use nested object as the expressionConfigurable which will get the property expression
103                 configurableWithExpression = (UifDictionaryBean) nestedObject;
104             }
105 
106             configurableWithExpression.getPropertyExpressions().put(adjustedPropertyName, expression);
107         }
108     }
109 
110     /**
111      * Takes in an expression and a list to be filled in with names(property names)
112      * of controls found in the expression. This method returns a js expression which can
113      * be executed on the client to determine if the original exp was satisfied before
114      * interacting with the server - ie, this js expression is equivalent to the one passed in.
115      *
116      * There are limitations on the Spring expression language that can be used as this method.
117      * It is only used to parse expressions which are valid case statements for determining if
118      * some action/processing should be performed.  ONLY Properties, comparison operators, booleans,
119      * strings, matches expression, and boolean logic are supported.  Properties must
120      * be a valid property on the form, and should have a visible control within the view.
121      *
122      * Example valid exp: account.name == 'Account Name'
123      *
124      * @param exp
125      * @param controlNames
126      * @return parsed expression, expressed as JS for client side evaluation
127      */
128     public static String parseExpression(String exp, List<String> controlNames) {
129         // clean up expression to ease parsing
130         exp = exp.trim();
131         if (exp.startsWith("@{")) {
132             exp = StringUtils.removeStart(exp, "@{");
133             if (exp.endsWith("}")) {
134                 exp = StringUtils.removeEnd(exp, "}");
135             }
136         }
137 
138         exp = StringUtils.replace(exp, "!=", " != ");
139         exp = StringUtils.replace(exp, "==", " == ");
140         exp = StringUtils.replace(exp, ">", " > ");
141         exp = StringUtils.replace(exp, "<", " < ");
142         exp = StringUtils.replace(exp, "<=", " <= ");
143         exp = StringUtils.replace(exp, ">=", " >= ");
144 
145         String conditionJs = exp;
146         String stack = "";
147 
148         boolean expectingSingleQuote = false;
149         boolean ignoreNext = false;
150         for (int i = 0; i < exp.length(); i++) {
151             char c = exp.charAt(i);
152             if (!expectingSingleQuote && !ignoreNext && (c == '(' || c == ' ' || c == ')')) {
153                 evaluateCurrentStack(stack.trim(), controlNames);
154                 //reset stack
155                 stack = "";
156                 continue;
157             } else if (!ignoreNext && c == '\'') {
158                 stack = stack + c;
159                 expectingSingleQuote = !expectingSingleQuote;
160             } else if (c == '\\') {
161                 stack = stack + c;
162                 ignoreNext = !ignoreNext;
163             } else {
164                 stack = stack + c;
165                 ignoreNext = false;
166             }
167         }
168 
169         if (StringUtils.isNotEmpty(stack)) {
170             evaluateCurrentStack(stack.trim(), controlNames);
171         }
172 
173         conditionJs = conditionJs.replaceAll("\\s(?i:ne)\\s", " != ").replaceAll("\\s(?i:eq)\\s", " == ").replaceAll(
174                 "\\s(?i:gt)\\s", " > ").replaceAll("\\s(?i:lt)\\s", " < ").replaceAll("\\s(?i:lte)\\s", " <= ")
175                 .replaceAll("\\s(?i:gte)\\s", " >= ").replaceAll("\\s(?i:and)\\s", " && ").replaceAll("\\s(?i:or)\\s",
176                         " || ").replaceAll("\\s(?i:not)\\s", " != ").replaceAll("\\s(?i:null)\\s?", " '' ").replaceAll(
177                         "\\s?(?i:#empty)\\((.*?)\\)", "isValueEmpty($1)").replaceAll(
178                         "\\s?(?i:#listContains)\\((.*?)\\)", "listContains($1)").replaceAll(
179                         "\\s?(?i:#emptyList)\\((.*?)\\)", "emptyList($1)");
180 
181         if (conditionJs.contains("matches")) {
182             conditionJs = conditionJs.replaceAll("\\s+(?i:matches)\\s+'.*'", ".match(/" + "$0" + "/) != null ");
183             conditionJs = conditionJs.replaceAll("\\(/\\s+(?i:matches)\\s+'", "(/");
184             conditionJs = conditionJs.replaceAll("'\\s*/\\)", "/)");
185         }
186 
187         List<String> removeControlNames = new ArrayList<String>();
188         List<String> addControlNames = new ArrayList<String>();
189         //convert property names to use coerceValue function and convert arrays to js arrays
190         for (String propertyName : controlNames) {
191             //array definitions are caught in controlNames because of the nature of the parse - convert them and remove
192             if (propertyName.trim().startsWith("{") && propertyName.trim().endsWith("}")) {
193                 String array = propertyName.trim().replace('{', '[');
194                 array = array.replace('}', ']');
195                 conditionJs = conditionJs.replace(propertyName, array);
196                 removeControlNames.add(propertyName);
197                 continue;
198             }
199 
200             //handle not
201             if (propertyName.startsWith("!")) {
202                 String actualPropertyName = StringUtils.removeStart(propertyName, "!");
203                 conditionJs = conditionJs.replace(propertyName, "!coerceValue(\"" + actualPropertyName + "\")");
204                 removeControlNames.add(propertyName);
205                 addControlNames.add(actualPropertyName);
206             } else {
207                 conditionJs = conditionJs.replace(propertyName, "coerceValue(\"" + propertyName + "\")");
208             }
209         }
210 
211         controlNames.removeAll(removeControlNames);
212         controlNames.addAll(addControlNames);
213 
214         return conditionJs;
215     }
216 
217     /**
218      * Used internally by parseExpression to evalute if the current stack is a property
219      * name (ie, will be a control on the form)
220      *
221      * @param stack
222      * @param controlNames
223      */
224     public static void evaluateCurrentStack(String stack, List<String> controlNames) {
225         if (StringUtils.isNotBlank(stack)) {
226             if (!(stack.equals("==")
227                     || stack.equals("!=")
228                     || stack.equals(">")
229                     || stack.equals("<")
230                     || stack.equals(">=")
231                     || stack.equals("<=")
232                     || stack.equalsIgnoreCase("ne")
233                     || stack.equalsIgnoreCase("eq")
234                     || stack.equalsIgnoreCase("gt")
235                     || stack.equalsIgnoreCase("lt")
236                     || stack.equalsIgnoreCase("lte")
237                     || stack.equalsIgnoreCase("gte")
238                     || stack.equalsIgnoreCase("matches")
239                     || stack.equalsIgnoreCase("null")
240                     || stack.equalsIgnoreCase("false")
241                     || stack.equalsIgnoreCase("true")
242                     || stack.equalsIgnoreCase("and")
243                     || stack.equalsIgnoreCase("or")
244                     || stack.contains("#empty")
245                     || stack.equals("!")
246                     || stack.contains("#emptyList")
247                     || stack.contains("#listContains")
248                     || stack.startsWith("'")
249                     || stack.endsWith("'"))) {
250 
251                 boolean isNumber = false;
252                 if ((StringUtils.isNumeric(stack.substring(0, 1)) || stack.substring(0, 1).equals("-"))) {
253                     try {
254                         Double.parseDouble(stack);
255                         isNumber = true;
256                     } catch (NumberFormatException e) {
257                         isNumber = false;
258                     }
259                 }
260 
261                 if (!(isNumber)) {
262                     //correct argument of a custom function ending in comma
263                     if (StringUtils.endsWith(stack, ",")) {
264                         stack = StringUtils.removeEnd(stack, ",").trim();
265                     }
266 
267                     if (!controlNames.contains(stack)) {
268                         controlNames.add(stack);
269                     }
270                 }
271             }
272         }
273     }
274 
275 }