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